/*! * 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 = $( [] ); // Events this.connect( this, { 'live': 'onProtectedLive' } ); this.$.on( 'mouseenter', ve.bind( this.onProtectedMouseEnter, this ) ); // Initialization this.$.addClass( 've-ce-protectedNode' ); }; /* Static Properties */ ve.ce.ProtectedNode.static = {}; /** * Template for shield elements. * * Uses data URI to inject a 1x1 transparent PNG image into the DOM. * * Using transparent png instead of gif because IE 10 renders gif as solid red when used as img src. * * @property {jQuery} * @static * @inheritable */ ve.ce.ProtectedNode.static.$shieldTemplate = $( '' ) .addClass( 've-ce-protectedNode-shield' ) .attr( 'src', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAFElEQVR4' + 'XgXAAQ0AAABAMP1L30IDCPwC/o5WcS4AAAAASUVORK5CYII=' ); /** * Phantom element template. * * @property {jQuery} * @static * @inheritable */ ve.ce.ProtectedNode.static.$phantomTemplate = $( '
' ) .addClass( 've-ce-protectedNode-phantom' ) .attr( 'draggable', false ); /* Methods */ /** * Handle live events. * * @method */ ve.ce.ProtectedNode.prototype.onProtectedLive = function () { var $shield, node = this, $shieldTemplate = this.constructor.static.$shieldTemplate, surfaceModel = this.getRoot().getSurface().getModel(); if ( this.isLive() ) { // Events 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 ); $this.append( $shieldTemplate.clone() ); } } ); } else { surfaceModel.disconnect( this, { 'change': 'onSurfaceModelChange' } ); this.$shields.remove(); this.$shields = $( [] ); } }; /** * Handle phantom click events. * * @method * @param {jQuery.Event} e Mouse click event */ ve.ce.ProtectedNode.prototype.onPhantomClick = 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(); }; /** * 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( 'click', ve.bind( this.onPhantomClick, 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 = $( [] ); };