Trevor Parscal has uploaded a new change for review. https://gerrit.wikimedia.org/r/62548
Change subject: ve.ce.ProtectedNode ...................................................................... ve.ce.ProtectedNode Objective: Generalize the shield and phantom magic, so we can use it for pretty much any node we like. Usually this will be used with generated content nodes, but also with aliens (of course) and possible other stuff in the future. Bonus: Also fixes a bug in DM that would crash VE when you selected to the end and hit backspace. Changes: *.php * Added links to files aliens.html * Added attributes to aliens to make them aliens again ve.ce.AlienNode.js * Moved shield and phantom functionality to ve.ce.ProtectedNode ve.ce.AlienNode.js, ve.ce.MWReferenceListNode.js, ve.ce.MWReferenceNode.js, ve.ce.MWTemplateNode.js * Mixed in ve.ce.ProtectedNode ve.ce.Node.css * Reorganized styles and updated class names * Added simple light blue hover with outline (using inset box shadow) for protected nodes, same style as before for aliens ve.ce.Surface.css * Moved phantom styles to ve.ce.Node.css ve.ce.BranchNode.js * Moved call to setLive(false) to happen before detach() so that the surface object is still available and events can be disconnected ve.ce.BranchNode.js, ve.ce.Document.js, ve.ce.js, ve.ce.Surface.js, ve.ce.SurfaceObserver.js * Adjusted CSS class names ve.ce.Node.js * Moved shield template to ve.ce.ProtectedNode ve.ce.ProtectedNode.js * New class, mix into another class to protect it from editing ve.ce.RelocatableNode.js * Renamed temporary surface property to relocatingSurface to avoid confusion when debugging ve.ce.Surface.js * Moved phantom template to ve.ce.ProtectedNode ve.dm.Transaction.js * Fixed bug where most of the internal list was being deleted when the end of the document was selected and the user pressed backspace Change-Id: I2468b16e1ba6785ad298e38190e33493135719c3 --- M VisualEditor.php M demos/ve/index.php M demos/ve/pages/aliens.html M modules/ve/ce/nodes/ve.ce.AlienNode.js M modules/ve/ce/nodes/ve.ce.MWReferenceListNode.js M modules/ve/ce/nodes/ve.ce.MWReferenceNode.js M modules/ve/ce/nodes/ve.ce.MWTemplateNode.js M modules/ve/ce/styles/ve.ce.Node.css M modules/ve/ce/styles/ve.ce.Surface.css M modules/ve/ce/ve.ce.BranchNode.js M modules/ve/ce/ve.ce.Document.js M modules/ve/ce/ve.ce.Node.js A modules/ve/ce/ve.ce.ProtectedNode.js M modules/ve/ce/ve.ce.RelocatableNode.js M modules/ve/ce/ve.ce.Surface.js M modules/ve/ce/ve.ce.SurfaceObserver.js M modules/ve/ce/ve.ce.js M modules/ve/dm/ve.dm.Transaction.js M modules/ve/test/index.php 19 files changed, 388 insertions(+), 279 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/VisualEditor refs/changes/48/62548/1 diff --git a/VisualEditor.php b/VisualEditor.php index 78119e7..8f79b6f 100644 --- a/VisualEditor.php +++ b/VisualEditor.php @@ -309,6 +309,7 @@ 've/ce/ve.ce.BranchNode.js', 've/ce/ve.ce.ContentBranchNode.js', 've/ce/ve.ce.LeafNode.js', + 've/ce/ve.ce.ProtectedNode.js', 've/ce/ve.ce.FocusableNode.js', 've/ce/ve.ce.RelocatableNode.js', 've/ce/ve.ce.ResizableNode.js', diff --git a/demos/ve/index.php b/demos/ve/index.php index a374e54..eba0931 100644 --- a/demos/ve/index.php +++ b/demos/ve/index.php @@ -187,6 +187,7 @@ <script src="../../modules/ve/ce/ve.ce.ContentBranchNode.js"></script> <script src="../../modules/ve/ce/ve.ce.LeafNode.js"></script> <script src="../../modules/ve/ce/ve.ce.FocusableNode.js"></script> + <script src="../../modules/ve/ce/ve.ce.ProtectedNode.js"></script> <script src="../../modules/ve/ce/ve.ce.RelocatableNode.js"></script> <script src="../../modules/ve/ce/ve.ce.ResizableNode.js"></script> <script src="../../modules/ve/ce/ve.ce.Surface.js"></script> diff --git a/demos/ve/pages/aliens.html b/demos/ve/pages/aliens.html index bd0a91c..911fea3 100644 --- a/demos/ve/pages/aliens.html +++ b/demos/ve/pages/aliens.html @@ -1,21 +1,21 @@ -<div style="float: right; height: 75px; width: 200px;">Single template, floated right</div> +<div rel="mw:Alien" style="float: right; height: 75px; width: 200px;">Single template, floated right</div> <p>Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled <alieninline>ALIEN1</alieninline> it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was <a href="http://www.wikia.com">popularised</a> in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled <alieninline>ALIEN1</alieninline> it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was <a href="http://www.wikia.com">popularised</a> in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.</p> -<div> +<div rel="mw:Alien"> <div style="float: right; height: 75px; width: 200px;">Template with two floats!</div> <div style="float: left; height: 75px; width: 200px;">Template with <span><span><span>two</span></span></span> Lorem Ipsum floats!</div> </div> <p>Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled <alieninline>ALIEN1</alieninline> it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was <a href="http://www.wikia.com">popularised</a> in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled <alieninline>ALIEN1</alieninline> it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was <a href="http://www.wikia.com">popularised</a> in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.</p> -<div style="height: 75px; width: 200px;" id="christian"> +<div rel="mw:Alien" style="height: 75px; width: 200px;" id="christian"> <div style="float: left">float</div> No floating or positioning at all </div> <p><alieninline><span><span style="color: red; position: absolute; top: 0; right: 0; float: right;">Coordinates</span></span></alieninline>Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled <alieninline>ALIEN1</alieninline> it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was <a href="http://www.wikia.com">popularised</a> in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled <alieninline>ALIEN1</alieninline> it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was <a href="http://www.wikia.com">popularised</a> in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.</p> -<div style="position: absolute; top: 200px; left: 200px;"> +<div rel="mw:Alien" style="position: absolute; top: 200px; left: 200px;"> Oh no, I'm absolutely positioned! </div> diff --git a/modules/ve/ce/nodes/ve.ce.AlienNode.js b/modules/ve/ce/nodes/ve.ce.AlienNode.js index cd59a85..bc24ae7 100644 --- a/modules/ve/ce/nodes/ve.ce.AlienNode.js +++ b/modules/ve/ce/nodes/ve.ce.AlienNode.js @@ -11,6 +11,8 @@ * @class * @abstract * @extends ve.ce.GeneratedContentNode + * @mixins ve.ce.ProtectedNode + * * @constructor * @param {ve.dm.AlienNode} model Model to observe */ @@ -18,57 +20,28 @@ // Parent constructor ve.ce.GeneratedContentNode.call( this, model ); - // DOM Changes + // Mixin constructors + ve.ce.ProtectedNode.call( this ); + + // Intitialization this.$.addClass( 've-ce-alienNode' ); - - // Properties - this.$phantoms = $( [] ); - - // Events - this.connect( this, { 'live': 'onLive' } ); - this.$.on( 'mouseenter', ve.bind( this.onMouseEnter, this ) ); }; /* Inheritance */ ve.inheritClass( ve.ce.AlienNode, ve.ce.GeneratedContentNode ); +ve.mixinClass( ve.ce.AlienNode, ve.ce.ProtectedNode ); + /* Static Properties */ ve.ce.AlienNode.static.name = 'alien'; +ve.ce.AlienNode.static.$phantomTemplate = ve.ce.AlienNode.static.$phantomTemplate.clone() + .addClass( 've-ce-alienNode-phantom' ) + .attr( 'title', ve.msg( 'visualeditor-aliennode-tooltip' ) ); + /* Methods */ - -/** - * Handle live events. - * - * @method - */ -ve.ce.AlienNode.prototype.onLive = function () { - var $shieldTemplate = this.constructor.static.$shieldTemplate, - surfaceModel = this.getRoot().getSurface().getModel(); - - if ( this.live === true ) { - // 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-alienNode' ) - ) { - return; - } - $this.append( $shieldTemplate.clone() ); - } - } ); - } else { - surfaceModel.disconnect( this, { 'change': 'onSurfaceModelChange' } ); - } -}; /** * Handle update events. @@ -77,112 +50,6 @@ */ ve.ce.AlienNode.prototype.onUpdate = function () { this.$.html( ve.copyArray( this.model.getAttribute( 'domElements' ) || [] ) ); -}; - -/** - * Handle mouse enter events. - * - * @method - */ -ve.ce.AlienNode.prototype.onMouseEnter = function () { - if ( !this.root.getSurface().dragging ) { - this.createPhantoms(); - } -}; - -/** - * Handle surface mouse move events. - * - * @method - * @param {jQuery.Event} e Mouse move event - */ -ve.ce.AlienNode.prototype.onSurfaceMouseMove = function ( e ) { - var $target = $( e.target ); - if ( - !$target.hasClass( 've-ce-surface-phantom' ) && - $target.closest( '.ve-ce-alienNode' ).length === 0 - ) { - this.clearPhantoms(); - } -}; - -/** - * Handle surface mouse out events. - * - * @method - * @param {jQuery.Event} e - */ -ve.ce.AlienNode.prototype.onSurfaceMouseOut = function ( e ) { - if ( e.toElement === null ) { - this.clearPhantoms(); - } -}; - -/** - * Handle surface model change events - * - * @method - */ -ve.ce.AlienNode.prototype.onSurfaceModelChange = function () { - if ( this.$phantoms.length ) { - this.positionPhantoms(); - } -}; - -/** - * Creates phantoms - * - * @method - */ -ve.ce.AlienNode.prototype.createPhantoms = function () { - var $phantomTemplate = ve.ce.Surface.static.$phantomTemplate, - surface = this.root.getSurface(); - - this.$.find( '.ve-ce-node-shield' ).each( - ve.bind( function () { - this.$phantoms = this.$phantoms.add( $phantomTemplate.clone() ); - }, this ) - ); - this.positionPhantoms(); - surface.replacePhantoms( this.$phantoms ); - - surface.$.on({ - 'mousemove.phantoms': ve.bind( this.onSurfaceMouseMove, this ), - 'mouseout.phantoms': ve.bind( this.onSurfaceMouseOut, this ) - }); -}; - -/** - * Positions phantoms - * - * @method - */ -ve.ce.AlienNode.prototype.positionPhantoms = function () { - this.$.find( '.ve-ce-node-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 .phantoms namespace event handlers - * - * @method - */ -ve.ce.AlienNode.prototype.clearPhantoms = function () { - var surface = this.root.getSurface(); - surface.replacePhantoms( null ); - surface.$.unbind( '.phantoms' ); - this.$phantoms = $( [] ); }; /* Concrete subclasses */ diff --git a/modules/ve/ce/nodes/ve.ce.MWReferenceListNode.js b/modules/ve/ce/nodes/ve.ce.MWReferenceListNode.js index 44eff8a..1001804 100644 --- a/modules/ve/ce/nodes/ve.ce.MWReferenceListNode.js +++ b/modules/ve/ce/nodes/ve.ce.MWReferenceListNode.js @@ -10,12 +10,17 @@ * * @class * @extends ve.ce.LeafNode + * @mixins ve.ce.ProtectedNode + * * @constructor * @param {ve.dm.MWReferenceListNode} model Model to observe */ ve.ce.MWReferenceListNode = function VeCeMWReferenceListNode( model ) { // Parent constructor ve.ce.LeafNode.call( this, model, $( '<div>' ) ); + + // Mixin constructors + ve.ce.ProtectedNode.call( this ); // DOM Changes this.$.addClass( 've-ce-MWreferenceListNode', 'reference' ) @@ -32,6 +37,8 @@ ve.inheritClass( ve.ce.MWReferenceListNode, ve.ce.LeafNode ); +ve.mixinClass( ve.ce.MWReferenceListNode, ve.ce.ProtectedNode ); + /* Static Properties */ ve.ce.MWReferenceListNode.static.name = 'MWreferenceList'; diff --git a/modules/ve/ce/nodes/ve.ce.MWReferenceNode.js b/modules/ve/ce/nodes/ve.ce.MWReferenceNode.js index b4e68e3..69d8a5a 100644 --- a/modules/ve/ce/nodes/ve.ce.MWReferenceNode.js +++ b/modules/ve/ce/nodes/ve.ce.MWReferenceNode.js @@ -10,12 +10,17 @@ * * @class * @extends ve.ce.LeafNode + * @mixins ve.ce.ProtectedNode + * * @constructor * @param {ve.dm.MWReferenceNode} model Model to observe */ ve.ce.MWReferenceNode = function VeCeMWReferenceNode( model ) { // Parent constructor ve.ce.LeafNode.call( this, model, $( '<sup>' ) ); + + // Mixin constructors + ve.ce.ProtectedNode.call( this ); // DOM Changes this.$link = $( '<a>' ).attr( 'href', '#' ); @@ -35,6 +40,8 @@ ve.inheritClass( ve.ce.MWReferenceNode, ve.ce.LeafNode ); +ve.mixinClass( ve.ce.MWReferenceNode, ve.ce.ProtectedNode ); + /* Static Properties */ ve.ce.MWReferenceNode.static.name = 'MWreference'; diff --git a/modules/ve/ce/nodes/ve.ce.MWTemplateNode.js b/modules/ve/ce/nodes/ve.ce.MWTemplateNode.js index 48e66d4..b2fedc0 100644 --- a/modules/ve/ce/nodes/ve.ce.MWTemplateNode.js +++ b/modules/ve/ce/nodes/ve.ce.MWTemplateNode.js @@ -11,12 +11,17 @@ * @class * @abstract * @extends ve.ce.GeneratedContentNode + * @mixins ve.ce.ProtectedNode + * * @constructor * @param {ve.dm.MWTemplateNode} model Model to observe */ ve.ce.MWTemplateNode = function VeCeMWTemplateNode( model ) { // Parent constructor ve.ce.GeneratedContentNode.call( this, model ); + + // Mixin constructors + ve.ce.ProtectedNode.call( this ); // DOM Changes this.$.addClass( 've-ce-MWtemplateNode' ); @@ -26,6 +31,8 @@ ve.inheritClass( ve.ce.MWTemplateNode, ve.ce.GeneratedContentNode ); +ve.mixinClass( ve.ce.MWTemplateNode, ve.ce.ProtectedNode ); + /* Static Properties */ ve.ce.MWTemplateNode.static.name = 'MWtemplate'; diff --git a/modules/ve/ce/styles/ve.ce.Node.css b/modules/ve/ce/styles/ve.ce.Node.css index ed744d9..e0143ef 100644 --- a/modules/ve/ce/styles/ve.ce.Node.css +++ b/modules/ve/ce/styles/ve.ce.Node.css @@ -5,10 +5,10 @@ * @license The MIT License (MIT); see LICENSE.txt */ -/* Alien styling */ +/* ve.ce.ProtectedNode */ -.ve-ce-alienNode, -.ve-ce-alienNode * { +.ve-ce-protectedNode, +.ve-ce-protectedNode * { position: relative !important; top: 0 !important; left: 0 !important; @@ -16,41 +16,11 @@ right: 0 !important; } -.ve-ce-imageNode { - cursor: default; -} - -.ve-ce-alienNode { +.ve-ce-protectedNode { z-index: 0; } -.ve-ce-resizableNode-transitioning { - -webkit-transition: width 100ms ease-in-out, height 100ms ease-in-out; - -moz-transition: width 100ms ease-in-out, height 100ms ease-in-out; - -ms-transition: width 100ms ease-in-out, height 100ms ease-in-out; - -o-transition: width 100ms ease-in-out, height 100ms ease-in-out; - transition: width 100ms ease-in-out, height 100ms ease-in-out; -} - -.ve-ce-resizableNode-handles-resizing { - z-index: 10000; -} - -/* Alien blocks and their contents are unselectable / unclickable */ -.ve-ce-alienBlockNode, -.ve-ce-alienBlockNode * { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} - -.ve-ce-alienBlockNode::-moz-selection, -.ve-ce-alienBlockNode *::-moz-selection { - background: transparent; -} - -.ve-ce-node-shield { +.ve-ce-protectedNode-shield { position: absolute !important; top: 0; left: 0; @@ -63,39 +33,25 @@ user-select: element; } -/* Fix weird Chrome native selection involving floated elements */ -.ve-ce-alienBlockNode:before, -.ve-ce-alienBlockNode:after { - content: ''; +.ve-ce-protectedNode-phantom { + position: absolute; + background-color: rgba(104,171,255,0.1); + box-shadow: inset 0 0 0 1px #68abff; + cursor: default; } -.ve-ce-alienBlockNode, -.ve-ce-MWtemplateBlockNode, { - display: block; +/* ve.ce.ResizableNode */ + +.ve-ce-resizableNode-transitioning { + -webkit-transition: width 100ms ease-in-out, height 100ms ease-in-out; + -moz-transition: width 100ms ease-in-out, height 100ms ease-in-out; + -ms-transition: width 100ms ease-in-out, height 100ms ease-in-out; + -o-transition: width 100ms ease-in-out, height 100ms ease-in-out; + transition: width 100ms ease-in-out, height 100ms ease-in-out; } -.ve-ce-alienInlineNode, -.ve-ce-MWtemplateInlineNode { - display: inline; -} - -.ve-ce-slugBlock { - display: block; -} - -.ve-ce-branchNode h1:empty:before, -.ve-ce-branchNode h2:empty:before, -.ve-ce-branchNode h3:empty:before, -.ve-ce-branchNode h4:empty:before, -.ve-ce-branchNode h5:empty:before, -.ve-ce-branchNode h6:empty:before, -.ve-ce-branchNode p:empty:before { - content: url('data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='); -} - -li.ve-ce-branchNode p.ve-ce-branchNode:first-child { - margin: 0; - padding: 0; +.ve-ce-resizableNode-handles-resizing { + z-index: 10000; } .ve-ce-resizableNode-handles { @@ -140,3 +96,81 @@ bottom: -0.33em; right: -0.33em; } + +/* ve.ce.ImageNode */ + +.ve-ce-imageNode { + cursor: default; +} + +/* ve.ce.AlienNode */ + +/* Block aliens are unselectable */ +.ve-ce-alienBlockNode, +.ve-ce-alienBlockNode * { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.ve-ce-alienBlockNode::-moz-selection, +.ve-ce-alienBlockNode *::-moz-selection { + background: transparent; +} + +/* Fix weird Chrome native selection involving floated elements */ +.ve-ce-alienBlockNode:before, +.ve-ce-alienBlockNode:after { + content: ''; +} + +.ve-ce-alienBlockNode { + display: block; +} + +.ve-ce-alienInlineNode { + display: inline; +} + +.ve-ce-alienNode-phantom { + background-color: #C3E59A; + background-image: -ms-repeating-linear-gradient(-45deg, white 0px, white 5px, #C3E59A 5px, #C3E59A 10px ); + background-image: -webkit-repeating-linear-gradient(-45deg, white 0px, white 5px, #C3E59A 5px, #C3E59A 10px ); + background-image: -moz-repeating-linear-gradient(-45deg, white 0px, white 5px, #C3E59A 5px, #C3E59A 10px); + background-image: repeating-linear-gradient(-45deg, white 0px, white 5px, #C3E59A 5px, #C3E59A 10px ); + background-size: 14px 14px; + box-shadow: none; + cursor: not-allowed; +} + +/* ve.ce.MWTemplateNode */ + +.ve-ce-MWtemplateBlockNode, { + display: block; +} + +.ve-ce-MWtemplateInlineNode { + display: inline; +} + +/* ve.ce.BranchNode */ + +.ve-ce-branchNode-blockSlug { + display: block; +} + +.ve-ce-branchNode h1:empty:before, +.ve-ce-branchNode h2:empty:before, +.ve-ce-branchNode h3:empty:before, +.ve-ce-branchNode h4:empty:before, +.ve-ce-branchNode h5:empty:before, +.ve-ce-branchNode h6:empty:before, +.ve-ce-branchNode p:empty:before { + content: url('data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='); +} + +li.ve-ce-branchNode p.ve-ce-branchNode:first-child { + margin: 0; + padding: 0; +} diff --git a/modules/ve/ce/styles/ve.ce.Surface.css b/modules/ve/ce/styles/ve.ce.Surface.css index cde0bee..feabdf2 100644 --- a/modules/ve/ce/styles/ve.ce.Surface.css +++ b/modules/ve/ce/styles/ve.ce.Surface.css @@ -36,21 +36,10 @@ /* @noflip */ .ve-ce-surface-phantoms { - cursor: not-allowed; - left: 0; - opacity: .85; position: absolute; top: 0; -} - -.ve-ce-surface-phantom { - background-color: #C3E59A; - background-image: -ms-repeating-linear-gradient(-45deg, white 0px, white 5px, #C3E59A 5px, #C3E59A 10px ); - background-image: -webkit-repeating-linear-gradient(-45deg, white 0px, white 5px, #C3E59A 5px, #C3E59A 10px ); - background-image: -moz-repeating-linear-gradient(-45deg, white 0px, white 5px, #C3E59A 5px, #C3E59A 10px); - background-image: repeating-linear-gradient(-45deg, white 0px, white 5px, #C3E59A 5px, #C3E59A 10px ); - background-size: 14px 14px; - position: absolute; + left: 0; + opacity: 0.75; } .ve-ce-surface-paste { diff --git a/modules/ve/ce/ve.ce.BranchNode.js b/modules/ve/ce/ve.ce.BranchNode.js index 4ed49c2..2c799f5 100644 --- a/modules/ve/ce/ve.ce.BranchNode.js +++ b/modules/ve/ce/ve.ce.BranchNode.js @@ -55,7 +55,6 @@ /* Static Properties */ - /** * Inline slug template. * @@ -63,7 +62,7 @@ * @property {jQuery} */ ve.ce.BranchNode.$inlineSlugTemplate = $( '<span>' ) - .addClass( 've-ce-slug' ) + .addClass( 've-ce-branchNode-slug ve-ce-branchNode-inlineSlug' ) .html( $.browser.msie ? ' ' : '' ); /** @@ -72,8 +71,9 @@ * @static * @property {jQuery} */ -ve.ce.BranchNode.$blockSlugTemplate = - ve.ce.BranchNode.$inlineSlugTemplate.clone().addClass( 've-ce-slugBlock' ); +ve.ce.BranchNode.$blockSlugTemplate = $( '<span>' ) + .addClass( 've-ce-branchNode-slug ve-ce-branchNode-blockSlug' ) + .html( $.browser.msie ? ' ' : '' ); /* Static Methods */ @@ -194,10 +194,9 @@ removals = this.children.splice.apply( this.children, args ); for ( i = 0, length = removals.length; i < length; i++ ) { removals[i].model.disconnect( this, { 'update': 'onModelUpdate' } ); - removals[i].detach(); - // Update DOM - removals[i].$.detach(); removals[i].setLive( false ); + removals[i].detach(); + removals[i].$.detach(); } if ( args.length >= 3 ) { if ( index ) { diff --git a/modules/ve/ce/ve.ce.Document.js b/modules/ve/ce/ve.ce.Document.js index 0c2bcd2..994ccb0 100644 --- a/modules/ve/ce/ve.ce.Document.js +++ b/modules/ve/ce/ve.ce.Document.js @@ -155,7 +155,7 @@ } } else if ( item.nodeType === Node.ELEMENT_NODE ) { $item = current[0].eq( current[1] ); - if ( $item.hasClass('ve-ce-slug') ) { + if ( $item.hasClass( 've-ce-branchNode-slug' ) ) { if ( offset === startOffset ) { return { node: $item[0], diff --git a/modules/ve/ce/ve.ce.Node.js b/modules/ve/ce/ve.ce.Node.js index 45ba2b1..16cb9e7 100644 --- a/modules/ve/ce/ve.ce.Node.js +++ b/modules/ve/ce/ve.ce.Node.js @@ -49,22 +49,6 @@ */ ve.ce.Node.static.canBeSplit = false; -/** - * 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. - * - * @static - * @property static.$shieldTemplate - * @inheritable - */ -ve.ce.Node.static.$shieldTemplate = $( '<img>' ) - .addClass( 've-ce-node-shield' ) - .attr( 'src', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAFElEQVR4' + - 'XgXAAQ0AAABAMP1L30IDCPwC/o5WcS4AAAAASUVORK5CYII=' ); - /* Methods */ /** diff --git a/modules/ve/ce/ve.ce.ProtectedNode.js b/modules/ve/ce/ve.ce.ProtectedNode.js new file mode 100644 index 0000000..2d70a7c --- /dev/null +++ b/modules/ve/ce/ve.ce.ProtectedNode.js @@ -0,0 +1,225 @@ +/*! + * 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 = $( '<img>' ) + .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 = $( '<div>' ) + .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 = $( [] ); +}; diff --git a/modules/ve/ce/ve.ce.RelocatableNode.js b/modules/ve/ce/ve.ce.RelocatableNode.js index e51abba..b87950a 100644 --- a/modules/ve/ce/ve.ce.RelocatableNode.js +++ b/modules/ve/ce/ve.ce.RelocatableNode.js @@ -17,7 +17,7 @@ ve.ce.RelocatableNode = function VeCeRelocatableNode( $draggable ) { // Properties this.$draggable = $draggable || this.$; - this.surface = null; + this.relocatingSurface = null; // Events this.$draggable.on( { @@ -36,11 +36,11 @@ */ ve.ce.RelocatableNode.prototype.onRelocatableDragStart = function () { // Store a copy of the surface, when dragend occurs the node will be detached - this.surface = this.getRoot().getSurface(); + this.relocatingSurface = this.getRoot().getSurface(); - if ( this.surface ) { + if ( this.relocatingSurface ) { // Allow dragging this node in the surface - this.surface.startRelocation( this ); + this.relocatingSurface.startRelocation( this ); } }; @@ -51,8 +51,8 @@ * @param {jQuery.Event} e Drag end event */ ve.ce.RelocatableNode.prototype.onRelocatableDragEnd = function () { - if ( this.surface ) { - this.surface.endRelocation(); - this.surface = null; + if ( this.relocatingSurface ) { + this.relocatingSurface.endRelocation(); + this.relocatingSurface = null; } }; diff --git a/modules/ve/ce/ve.ce.Surface.js b/modules/ve/ce/ve.ce.Surface.js index 1940955..75ef51e 100644 --- a/modules/ve/ce/ve.ce.Surface.js +++ b/modules/ve/ce/ve.ce.Surface.js @@ -102,19 +102,6 @@ ve.ce.Surface.static = {}; /** - * Phantom element template. - * - * @static - * @property {jQuery} - */ -ve.ce.Surface.static.$phantomTemplate = $( '<div>' ) - .addClass( 've-ce-surface-phantom' ) - .attr( { - 'title': ve.msg( 'visualeditor-aliennode-tooltip' ), - 'draggable': false - } ); - -/** * Pattern matching "normal" characters which we can let the browser handle natively. * * @static @@ -965,7 +952,7 @@ rangyRange.selectNode( $element[0] ); rangySelection.setSingleRange( rangyRange ); setTimeout( ve.bind( function () { - if ( !$element.hasClass( 've-ce-slug' ) ) { + if ( !$element.hasClass( 've-ce-branchNode-slug' ) ) { $element.remove(); } this.surfaceObserver.start(); @@ -1328,8 +1315,8 @@ // Ensure the range we are asking to select is from and to correct offsets - failure to do so // may cause getNodeAndOffset to throw an exception range = new ve.Range( - this.getNearestCorrectOffset( range.from ), - this.getNearestCorrectOffset( range.to ) + this.getNearestCorrectOffset( range.from, -1 ), + this.getNearestCorrectOffset( range.to, 1 ) ); if ( !range.isCollapsed() ) { diff --git a/modules/ve/ce/ve.ce.SurfaceObserver.js b/modules/ve/ce/ve.ce.SurfaceObserver.js index 39843c6..a92b4a8 100644 --- a/modules/ve/ce/ve.ce.SurfaceObserver.js +++ b/modules/ve/ce/ve.ce.SurfaceObserver.js @@ -157,10 +157,10 @@ if ( !rangyRange.equals( this.rangyRange ) ){ this.rangyRange = rangyRange; node = null; - $nodeOrSlug = $( rangyRange.anchorNode ).closest( '.ve-ce-branchNode, .ve-ce-slug' ); + $nodeOrSlug = $( rangyRange.anchorNode ).closest( '.ve-ce-branchNode, .ve-ce-branchNode-slug' ); if ( $nodeOrSlug.length ) { range = rangyRange.getRange(); - if ( !$nodeOrSlug.hasClass( 've-ce-slug' ) ) { + if ( !$nodeOrSlug.hasClass( 've-ce-branchNode-slug' ) ) { node = $nodeOrSlug.data( 'view' ); } } diff --git a/modules/ve/ce/ve.ce.js b/modules/ve/ce/ve.ce.js index 7273bc1..4e0b8f6 100644 --- a/modules/ve/ce/ve.ce.js +++ b/modules/ve/ce/ve.ce.js @@ -47,7 +47,7 @@ $element = $( element ); if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { - if ( $element.hasClass( 've-ce-slug' ) ) { + if ( $element.hasClass( 've-ce-branchNode-slug' ) ) { // Slugs are not represented in the model at all, but they do // contain a single nbsp/FEFF character in the DOM, so make sure // that character isn't counted @@ -164,7 +164,7 @@ } } else if ( item.nodeType === Node.ELEMENT_NODE ) { $item = current[0].eq( current[1] ); - if ( $item.hasClass( 've-ce-slug' ) ) { + if ( $item.hasClass( 've-ce-branchNode-slug' ) ) { if ( $item.contents()[0] === domNode ) { break; } @@ -198,7 +198,7 @@ nodeModel, node; - if ( $domNode.hasClass( 've-ce-slug' ) ) { + if ( $domNode.hasClass( 've-ce-branchNode-slug' ) ) { if ( $domNode.prev().length ) { nodeModel = $domNode.prev().data( 'view' ).getModel(); return nodeModel.getOffset() + nodeModel.getOuterLength(); diff --git a/modules/ve/dm/ve.dm.Transaction.js b/modules/ve/dm/ve.dm.Transaction.js index e1aac8b..787cfd7 100644 --- a/modules/ve/dm/ve.dm.Transaction.js +++ b/modules/ve/dm/ve.dm.Transaction.js @@ -821,7 +821,7 @@ var i, retainStart, internalStackDepth = 0; // Iterate over removal range and use a stack counter to determine if // we are inside an internal node - for ( i = removeStart; i <= removeEnd; i++ ) { + for ( i = removeStart; i < removeEnd; i++ ) { if ( doc.data.isElementData( i ) && ve.dm.nodeFactory.isNodeInternal( doc.data.getType( i ) ) ) { if ( !doc.data.isCloseElementData( i ) ) { if ( internalStackDepth === 0 ) { diff --git a/modules/ve/test/index.php b/modules/ve/test/index.php index 6b47593..4a08966 100644 --- a/modules/ve/test/index.php +++ b/modules/ve/test/index.php @@ -141,6 +141,7 @@ <script src="../../ve/ce/ve.ce.BranchNode.js"></script> <script src="../../ve/ce/ve.ce.ContentBranchNode.js"></script> <script src="../../ve/ce/ve.ce.LeafNode.js"></script> + <script src="../../ve/ce/ve.ce.ProtectedNode.js"></script> <script src="../../ve/ce/ve.ce.FocusableNode.js"></script> <script src="../../ve/ce/ve.ce.RelocatableNode.js"></script> <script src="../../ve/ce/ve.ce.ResizableNode.js"></script> -- To view, visit https://gerrit.wikimedia.org/r/62548 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I2468b16e1ba6785ad298e38190e33493135719c3 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/VisualEditor Gerrit-Branch: master Gerrit-Owner: Trevor Parscal <tpars...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits