jenkins-bot has submitted this change and it was merged. ( 
https://gerrit.wikimedia.org/r/383404 )

Change subject: Get a ve.ce.BranchNode position's corresponding DOM position
......................................................................


Get a ve.ce.BranchNode position's corresponding DOM position

Change-Id: I8c9a24dc877a530c9fa421e63caf73f78d79d04c
---
M src/ce/ve.ce.BranchNode.js
M tests/ce/ve.ce.BranchNode.test.js
2 files changed, 113 insertions(+), 39 deletions(-)

Approvals:
  Catrope: Looks good to me, approved
  jenkins-bot: Verified
  Jforrester: Looks good to me, but someone else must approve



diff --git a/src/ce/ve.ce.BranchNode.js b/src/ce/ve.ce.BranchNode.js
index b1a3131..8caf00a 100644
--- a/src/ce/ve.ce.BranchNode.js
+++ b/src/ce/ve.ce.BranchNode.js
@@ -174,16 +174,8 @@
  * @param {...ve.dm.BranchNode} [nodes] Variadic list of nodes to insert
  */
 ve.ce.BranchNode.prototype.onSplice = function ( index ) {
-       var i, j,
-               length,
-               args = [],
-               anchorCeNode,
-               prevCeNode,
-               anchorDomNode,
-               afterAnchor,
-               node,
-               parentNode,
-               removals;
+       var i, length, removals, position, j,
+               args = [];
 
        for ( i = 0, length = arguments.length; i < length; i++ ) {
                args.push( arguments[ i ] );
@@ -205,37 +197,14 @@
                removals[ i ].$element.detach();
        }
        if ( args.length >= 3 ) {
-               if ( index > 0 ) {
-                       // Get the element before the insertion
-                       anchorCeNode = this.children[ index - 1 ];
-                       // If the CE node is a text node, its $element will be 
empty
-                       // Look at its previous sibling, which cannot be a text 
node
-                       if ( anchorCeNode.getType() === 'text' ) {
-                               prevCeNode = this.children[ index - 2 ];
-                               if ( prevCeNode ) {
-                                       anchorDomNode = 
prevCeNode.$element.last()[ 0 ].nextSibling;
-                               } else {
-                                       anchorDomNode = this.$element[ 0 
].firstChild;
-                               }
-                       } else {
-                               anchorDomNode = anchorCeNode.$element.last()[ 0 
];
-                       }
-               }
+               position = this.getDomPosition( index );
                for ( i = args.length - 1; i >= 2; i-- ) {
                        args[ i ].attach( this );
-                       if ( anchorDomNode ) {
-                               // DOM equivalent of $( anchorDomNode ).after( 
args[i].$element );
-                               afterAnchor = anchorDomNode.nextSibling;
-                               parentNode = anchorDomNode.parentNode;
-                               for ( j = 0, length = args[ i 
].$element.length; j < length; j++ ) {
-                                       parentNode.insertBefore( args[ i 
].$element[ j ], afterAnchor );
-                               }
-                       } else {
-                               // DOM equivalent of this.$element.prepend( 
args[j].$element );
-                               node = this.$element[ 0 ];
-                               for ( j = args[ i ].$element.length - 1; j >= 
0; j-- ) {
-                                       node.insertBefore( args[ i ].$element[ 
j ], node.firstChild );
-                               }
+                       for ( j = 0, length = args[ i ].$element.length; j < 
length; j++ ) {
+                               position.node.insertBefore(
+                                       args[ i ].$element[ j ],
+                                       position.node.children[ position.offset 
]
+                               );
                        }
                        if ( this.live !== args[ i ].isLive() ) {
                                args[ i ].setLive( this.live );
@@ -386,3 +355,66 @@
        // Parent method
        ve.ce.BranchNode.super.prototype.destroy.call( this );
 };
+
+/**
+ * Get the DOM position (node and offset) corresponding to a position in this 
node
+ *
+ * The node/offset have the same semantics as a DOM Selection 
focusNode/focusOffset
+ *
+ * @param {number} offset The offset inside this node of the required position
+ * @return {Object|null} The DOM position
+ * @return {Node} return.node DOM node; guaranteed to be this node's final DOM 
node
+ * @return {number} return.offset DOM offset
+ */
+ve.ce.BranchNode.prototype.getDomPosition = function ( offset ) {
+       var i, ceNode,
+               domNode = this.$element.last()[ 0 ];
+
+       // Step backwards past empty nodes
+       i = offset - 1;
+       while ( true ) {
+               ceNode = this.children[ i-- ];
+               if ( !ceNode ) {
+                       // No preceding children with DOM nodes
+                       return { node: domNode, offset: 0 };
+               }
+               if ( ceNode.$element && ceNode.$element.length > 0 ) {
+                       // Preceding child with a DOM node
+                       return {
+                               node: domNode,
+                               offset: Array.prototype.indexOf.call(
+                                       domNode.childNodes,
+                                       ceNode.$element.last()[ 0 ]
+                               ) + 1
+                       };
+               }
+               if ( ceNode.getType() === 'text' ) {
+                       break;
+               }
+       }
+       // Darn, we hit a text node. CE text nodes can contain varying 
annotations and so it is
+       // difficult to calculate how many childNodes to skip. Let's try 
stepping forward instead.
+       i = offset;
+       while ( true ) {
+               ceNode = this.children[ i++ ];
+               if ( !ceNode ) {
+                       // No following children with DOM nodes
+                       return { node: domNode, offset: 
domNode.childNodes.length };
+               }
+               if ( ceNode.$element && ceNode.$element.length > 0 ) {
+                       // Following child with a DOM node
+                       return {
+                               node: domNode,
+                               offset: Array.prototype.indexOf.call(
+                                       domNode.childNodes,
+                                       ceNode.$element.first()[ 0 ]
+                               )
+                       };
+               }
+               if ( ceNode.getType() === 'text' ) {
+                       break;
+               }
+       }
+       // Oh no, there's a text node in both directions
+       throw new Error( 'Cannot calculate DOM position: adjacent text nodes' );
+};
diff --git a/tests/ce/ve.ce.BranchNode.test.js 
b/tests/ce/ve.ce.BranchNode.test.js
index cf2bd36..7257c5d 100644
--- a/tests/ce/ve.ce.BranchNode.test.js
+++ b/tests/ce/ve.ce.BranchNode.test.js
@@ -68,6 +68,48 @@
        assert.strictEqual( node.$element.text(), 'hello', 'contents are added 
to new wrapper' );
 } );
 
+QUnit.test( 'getDomPosition', function ( assert ) {
+       var expectedOffsets, i, len, position,
+               ceParent = new ve.ce.BranchNodeStub( new ve.dm.BranchNodeStub() 
);
+
+       // Create prior state by attaching manually, to avoid circular 
dependence on onSplice
+       ceParent.$element = $( '<p>' );
+       ceParent.children.push(
+               // Node with two DOM nodes
+               // TODO: The use of BranchNodeStub below is dissonant
+               new ve.ce.LeafNode( new ve.dm.BranchNodeStub() ),
+               // Node with no DOM nodes
+               new ve.ce.LeafNode( new ve.dm.BranchNodeStub() ),
+               new ve.ce.LeafNode( new ve.dm.BranchNodeStub() ),
+               // TextNode with no annotation
+               new ve.ce.TextNode( new ve.dm.BranchNodeStub() ),
+               // Node with one DOM node
+               new ve.ce.LeafNode( new ve.dm.BranchNodeStub() ),
+               // TextNode with some annotation
+               new ve.ce.TextNode( new ve.dm.BranchNodeStub() )
+       );
+       expectedOffsets = [ 0, 2, 2, 2, 3, 4, 7 ];
+       ceParent.children[ 0 ].$element = $( '<img><img>' );
+       ceParent.children[ 1 ].$element = $();
+       ceParent.children[ 2 ].$element = $();
+       ceParent.children[ 3 ].$element = undefined;
+       ceParent.children[ 4 ].$element = $( '<img>' );
+       ceParent.children[ 5 ].$element = undefined;
+       ceParent.$element.empty()
+               .append( ceParent.children[ 0 ].$element )
+               .append( 'foo' )
+               .append( ceParent.children[ 4 ].$element )
+               .append( 'bar<b>baz</b>qux' );
+
+       assert.expect( 2 * ceParent.children.length + 2 );
+
+       for ( i = 0, len = ceParent.children.length + 1; i < len; i++ ) {
+               position = ceParent.getDomPosition( i );
+               assert.strictEqual( position.node, ceParent.$element.last()[ 0 
], 'i=' + i + ' node' );
+               assert.strictEqual( position.offset, expectedOffsets[ i ], 'i=' 
+ i + ' position' );
+       }
+} );
+
 QUnit.test( 'onSplice', function ( assert ) {
        var modelA = new ve.dm.BranchNodeStub(),
                modelB = new ve.dm.BranchNodeStub(),

-- 
To view, visit https://gerrit.wikimedia.org/r/383404
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I8c9a24dc877a530c9fa421e63caf73f78d79d04c
Gerrit-PatchSet: 7
Gerrit-Project: VisualEditor/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Divec <da...@troi.org>
Gerrit-Reviewer: Catrope <r...@wikimedia.org>
Gerrit-Reviewer: Divec <da...@troi.org>
Gerrit-Reviewer: Esanders <esand...@wikimedia.org>
Gerrit-Reviewer: Jforrester <jforres...@wikimedia.org>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to