mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-12-01 17:36:35 +00:00
babaa4118d
Introduction of fake selection for single focused nodes. This change specifically makes the selection much nicer in appearance for Chrome users selecting floated FocusableNodes (block images) for example. Added ve-ce-surface-highlights DOM element to contain styled highlight elements. Made adjustments to getSelectionRect to return fake selection bounds if necessary. Replaced old uses of showSelection with model.change(). Change-Id: I96e66567cdce6455ef3eb77568e72f23140448ff
256 lines
5.5 KiB
JavaScript
256 lines
5.5 KiB
JavaScript
/*!
|
|
* VisualEditor ContentEditable ProtectedNode class.
|
|
*
|
|
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
|
|
* @license The MIT License (MIT); see LICENSE.txt
|
|
*/
|
|
|
|
/**
|
|
* ContentEditable relocatable node.
|
|
*
|
|
* @class
|
|
* @abstract
|
|
*
|
|
* @constructor
|
|
*/
|
|
ve.ce.ProtectedNode = function VeCeProtectedNode() {
|
|
// Properties
|
|
this.$phantoms = $( [] );
|
|
this.$shields = $( [] );
|
|
this.isSetup = false;
|
|
|
|
// Events
|
|
this.connect( this, {
|
|
'setup': 'onProtectedSetup',
|
|
'teardown': 'onProtectedTeardown'
|
|
} );
|
|
|
|
// Initialization
|
|
this.$.addClass( 've-ce-protectedNode' );
|
|
};
|
|
|
|
/* Static Properties */
|
|
|
|
ve.ce.ProtectedNode.static = {};
|
|
|
|
/**
|
|
* Template for shield elements.
|
|
*
|
|
* Uses data URI to inject a 1x1 transparent GIF image into the DOM.
|
|
*
|
|
* @property {jQuery}
|
|
* @static
|
|
* @inheritable
|
|
*/
|
|
ve.ce.ProtectedNode.static.$shieldTemplate = $( '<img>' )
|
|
.addClass( 've-ce-protectedNode-shield' )
|
|
.attr( 'src', 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' );
|
|
|
|
/**
|
|
* Phantom element template.
|
|
*
|
|
* @property {jQuery}
|
|
* @static
|
|
* @inheritable
|
|
*/
|
|
ve.ce.ProtectedNode.static.$phantomTemplate = $( '<div>' )
|
|
.addClass( 've-ce-protectedNode-phantom' )
|
|
.attr( 'draggable', false );
|
|
|
|
/* Methods */
|
|
|
|
/**
|
|
* Handle setup events.
|
|
*
|
|
* @method
|
|
*/
|
|
ve.ce.ProtectedNode.prototype.onProtectedSetup = function () {
|
|
var $shield, surfaceModel,
|
|
node = this,
|
|
$shieldTemplate = this.constructor.static.$shieldTemplate;
|
|
|
|
if ( this.isSetup ) {
|
|
return;
|
|
}
|
|
this.isSetup = true;
|
|
|
|
surfaceModel = this.getRoot().getSurface().getModel();
|
|
|
|
// Events
|
|
this.$.on( 'mouseenter.ve-ce-protectedNode', ve.bind( this.onProtectedMouseEnter, this ) );
|
|
surfaceModel.connect( this, { 'change': 'onSurfaceModelChange' } );
|
|
|
|
// Shields
|
|
this.$.add( this.$.find( '*' ) ).each( function () {
|
|
var $this = $( this );
|
|
if ( this.nodeType === Node.ELEMENT_NODE ) {
|
|
if (
|
|
( $this.css( 'float' ) === 'none' || $this.css( 'float' ) === '' ) &&
|
|
!$this.hasClass( 've-ce-protectedNode' )
|
|
) {
|
|
return;
|
|
}
|
|
$shield = $shieldTemplate.clone().appendTo( $this );
|
|
node.$shields = node.$shields.add( $shield );
|
|
}
|
|
} );
|
|
|
|
};
|
|
|
|
/**
|
|
* Handle teardown events.
|
|
*
|
|
* @method
|
|
*/
|
|
ve.ce.ProtectedNode.prototype.onProtectedTeardown = function () {
|
|
var surfaceModel;
|
|
|
|
if ( !this.isSetup ) {
|
|
return;
|
|
}
|
|
this.isSetup = false;
|
|
|
|
surfaceModel = this.getRoot().getSurface().getModel();
|
|
|
|
// Events
|
|
this.$.off( '.ve-ce-protectedNode' );
|
|
surfaceModel.disconnect( this, { 'change': 'onSurfaceModelChange' } );
|
|
|
|
// Shields
|
|
this.$shields.remove();
|
|
this.$shields = $( [] );
|
|
|
|
// Phantoms
|
|
this.clearPhantoms();
|
|
};
|
|
|
|
/**
|
|
* Handle phantom mouse down events.
|
|
*
|
|
* @method
|
|
* @param {jQuery.Event} e Mouse down event
|
|
*/
|
|
ve.ce.ProtectedNode.prototype.onPhantomMouseDown = function ( e ) {
|
|
var surfaceModel = this.getRoot().getSurface().getModel(),
|
|
selectionRange = surfaceModel.getSelection(),
|
|
nodeRange = this.model.getOuterRange();
|
|
|
|
surfaceModel.getFragment(
|
|
e.shiftKey ?
|
|
ve.Range.newCoveringRange(
|
|
[ selectionRange, nodeRange ], selectionRange.from > nodeRange.from
|
|
) :
|
|
nodeRange
|
|
).select();
|
|
|
|
e.preventDefault();
|
|
};
|
|
|
|
/**
|
|
* Handle mouse enter events.
|
|
*
|
|
* @method
|
|
*/
|
|
ve.ce.ProtectedNode.prototype.onProtectedMouseEnter = function () {
|
|
if ( !this.root.getSurface().dragging ) {
|
|
this.createPhantoms();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handle surface mouse move events.
|
|
*
|
|
* @method
|
|
* @param {jQuery.Event} e Mouse move event
|
|
*/
|
|
ve.ce.ProtectedNode.prototype.onSurfaceMouseMove = function ( e ) {
|
|
var $target = $( e.target );
|
|
if (
|
|
!$target.hasClass( 've-ce-protectedNode-phantom' ) &&
|
|
$target.closest( '.ve-ce-protectedNode' ).length === 0
|
|
) {
|
|
this.clearPhantoms();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handle surface mouse out events.
|
|
*
|
|
* @method
|
|
* @param {jQuery.Event} e
|
|
*/
|
|
ve.ce.ProtectedNode.prototype.onSurfaceMouseOut = function ( e ) {
|
|
if ( e.toElement === null ) {
|
|
this.clearPhantoms();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handle surface model change events
|
|
*
|
|
* @method
|
|
*/
|
|
ve.ce.ProtectedNode.prototype.onSurfaceModelChange = function () {
|
|
if ( this.$phantoms.length ) {
|
|
this.positionPhantoms();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Creates phantoms
|
|
*
|
|
* @method
|
|
*/
|
|
ve.ce.ProtectedNode.prototype.createPhantoms = function () {
|
|
var $phantomTemplate = this.constructor.static.$phantomTemplate,
|
|
surface = this.root.getSurface();
|
|
|
|
this.$.find( '.ve-ce-protectedNode-shield' ).each(
|
|
ve.bind( function () {
|
|
this.$phantoms = this.$phantoms.add(
|
|
$phantomTemplate.clone().on( 'mousedown', ve.bind( this.onPhantomMouseDown, this ) )
|
|
);
|
|
}, this )
|
|
);
|
|
this.positionPhantoms();
|
|
surface.replacePhantoms( this.$phantoms );
|
|
|
|
surface.$.on( {
|
|
'mousemove.ve-ce-protectedNode': ve.bind( this.onSurfaceMouseMove, this ),
|
|
'mouseout.ve-ce-protectedNode': ve.bind( this.onSurfaceMouseOut, this )
|
|
} );
|
|
};
|
|
|
|
/**
|
|
* Positions phantoms
|
|
*
|
|
* @method
|
|
*/
|
|
ve.ce.ProtectedNode.prototype.positionPhantoms = function () {
|
|
this.$.find( '.ve-ce-protectedNode-shield' ).each(
|
|
ve.bind( function ( i, element ) {
|
|
var $shield = $( element ),
|
|
offset = $shield.offset();
|
|
this.$phantoms.eq( i ).css( {
|
|
'top': offset.top,
|
|
'left': offset.left,
|
|
'height': $shield.height(),
|
|
'width': $shield.width(),
|
|
'background-position': -offset.left + 'px ' + -offset.top + 'px'
|
|
} );
|
|
}, this )
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Clears all phantoms and unbinds .ve-ce-protectedNode namespace event handlers
|
|
*
|
|
* @method
|
|
*/
|
|
ve.ce.ProtectedNode.prototype.clearPhantoms = function () {
|
|
var surface = this.root.getSurface();
|
|
surface.replacePhantoms( null );
|
|
surface.$.unbind( '.ve-ce-protectedNode' );
|
|
this.$phantoms = $( [] );
|
|
};
|