mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-25 14:56:20 +00:00
8cea089f3b
See also http://stackoverflow.com/a/13139830/319266: > Some are unstable and cause CSS glitches. [If] you have an > <img> and you use the tiniest transparent GIF possible, it > works fine[. if] you then want your transparent GIF to have a > background-image, then this is impossible. For some reason, > some GIFs such as the following prevent CSS backgrounds (in > some browsers). > > == Shortest (but unstable) == > data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== > > == Stable (but slightly longer) *use this one* == > > data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7 > > Also: don't ommit image/gif. This will break in several browsers. For the record, this is not limited to rare browsers. It also affects latest Chrome in some cases as confirmed by Christian (it'd be white instead of transparent in some cases when uses as a css background-image without border). Change-Id: If9ff8a0820c217b6c23e3335944907939a37bef7
267 lines
6.1 KiB
JavaScript
267 lines
6.1 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 protected node.
|
|
*
|
|
* @class
|
|
* @abstract
|
|
*
|
|
* @constructor
|
|
* @param {jQuery} [$phantomable=this.$] Element to show a phantom for
|
|
*/
|
|
ve.ce.ProtectedNode = function VeCeProtectedNode( $phantomable ) {
|
|
// Properties
|
|
this.$phantoms = $( [] );
|
|
this.$shields = $( [] );
|
|
this.$phantomable = $phantomable || this.$;
|
|
this.isSetup = false;
|
|
|
|
// Events
|
|
this.connect( this, {
|
|
'setup': 'onProtectedSetup',
|
|
'teardown': 'onProtectedTeardown'
|
|
} );
|
|
|
|
// DOM changes
|
|
this.$
|
|
.addClass( 've-ce-protectedNode' )
|
|
.prop( 'contentEditable', 'false' );
|
|
};
|
|
|
|
/* 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,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' );
|
|
|
|
/**
|
|
* 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,
|
|
node = this,
|
|
$shieldTemplate = this.constructor.static.$shieldTemplate;
|
|
|
|
// Exit if already setup or not unattached
|
|
if ( this.isSetup || !this.root ) {
|
|
return;
|
|
}
|
|
|
|
// Events
|
|
this.$.on( 'mouseenter.ve-ce-protectedNode', ve.bind( this.onProtectedMouseEnter, this ) );
|
|
this.getRoot().getSurface().getModel()
|
|
.connect( this, { 'change': 'onSurfaceModelChange' } );
|
|
this.getRoot().getSurface().getSurface()
|
|
.connect( this, { 'position': 'positionPhantoms' } );
|
|
|
|
// 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' ) &&
|
|
// Phantoms are built off shields, so make sure $phantomable has a shield
|
|
!$this.is( node.$phantomable )
|
|
) {
|
|
return;
|
|
}
|
|
$shield = $shieldTemplate.clone().appendTo( $this );
|
|
node.$shields = node.$shields.add( $shield );
|
|
}
|
|
} );
|
|
|
|
this.isSetup = true;
|
|
};
|
|
|
|
/**
|
|
* Handle teardown events.
|
|
*
|
|
* @method
|
|
*/
|
|
ve.ce.ProtectedNode.prototype.onProtectedTeardown = function () {
|
|
// Exit if not setup or not attached
|
|
if ( !this.isSetup || !this.root ) {
|
|
return;
|
|
}
|
|
|
|
// Events
|
|
this.$.off( '.ve-ce-protectedNode' );
|
|
this.root.getSurface().getModel()
|
|
.disconnect( this, { 'change': 'onSurfaceModelChange' } );
|
|
this.getRoot().getSurface().getSurface()
|
|
.disconnect( this, { 'position': 'positionPhantoms' } );
|
|
|
|
// Shields
|
|
this.$shields.remove();
|
|
this.$shields = $( [] );
|
|
|
|
// Phantoms
|
|
this.clearPhantoms();
|
|
|
|
this.isSetup = false;
|
|
};
|
|
|
|
/**
|
|
* 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.$phantomable.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.$phantomable.find( '.ve-ce-protectedNode-shield' ).each(
|
|
ve.bind( function ( i, element ) {
|
|
var $shield = $( element ),
|
|
offset = ve.Element.getRelativePosition(
|
|
$shield, this.getRoot().getSurface().getSurface().$
|
|
);
|
|
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 = $( [] );
|
|
};
|