mediawiki-extensions-Visual.../modules/ve/ce/ve.ce.ProtectedNode.js
Timo Tijhof 8cea089f3b ce: Use a better transparent pixel image
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
2013-08-30 17:51:20 -07:00

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 = $( [] );
};