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 ? '&nbsp;' : '&#xFEFF;' );
 
 /**
@@ -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 ? '&nbsp;' : '&#xFEFF;' );
 
 /* 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

Reply via email to