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