diff --git a/modules/ve/ce/ve.ce.js b/modules/ve/ce/ve.ce.js index 8ab47d47a4..98cb01d2d2 100644 --- a/modules/ve/ce/ve.ce.js +++ b/modules/ve/ce/ve.ce.js @@ -191,13 +191,12 @@ ve.ce.getOffsetFromTextNode = function ( domNode, domOffset ) { * @method * @param {HTMLElement} domNode DOM node * @param {number} domOffset DOM offset within the DOM Element - * @param {boolean} [addOuterLength] Use outer length, which includes wrappers if any exist + * @param {number} [firstRecursionDirection] Which direction the first recursive call went in (+/-1) * @returns {number} Linear model offset */ -ve.ce.getOffsetFromElementNode = function ( domNode, domOffset, addOuterLength ) { - var $domNode = $( domNode ), - nodeModel, - node; +ve.ce.getOffsetFromElementNode = function ( domNode, domOffset, firstRecursionDirection ) { + var direction, nodeModel, node, + $domNode = $( domNode ); if ( $domNode.hasClass( 've-ce-branchNode-slug' ) ) { if ( $domNode.prev().length ) { @@ -211,8 +210,8 @@ ve.ce.getOffsetFromElementNode = function ( domNode, domOffset, addOuterLength ) } // IE sometimes puts the cursor in a text node inside ce="false". BAD! - if ( domNode.contentEditable === 'false' ) { - nodeModel = $domNode.data( 'view' ).getModel(); + if ( !firstRecursionDirection && !domNode.isContentEditable ) { + nodeModel = $domNode.closest( '.ve-ce-branchNode, .ve-ce-leafNode' ).data( 'view' ).getModel(); return nodeModel.getOffset() + nodeModel.getOuterLength(); } @@ -220,22 +219,30 @@ ve.ce.getOffsetFromElementNode = function ( domNode, domOffset, addOuterLength ) node = $domNode.data( 'view' ); if ( node && node instanceof ve.ce.Node ) { nodeModel = $domNode.data( 'view' ).getModel(); - if ( addOuterLength === true ) { + if ( firstRecursionDirection === -1 ) { return nodeModel.getOffset() + nodeModel.getOuterLength(); + } else if ( firstRecursionDirection === 1 ) { + return nodeModel.getOffset(); } else { return nodeModel.getOffset() + ( nodeModel.isWrapped() ? 1 : 0 ); } } else { node = $domNode.contents().last()[0]; + if ( !firstRecursionDirection ) { + direction = 1; + } } } else { node = $domNode.contents()[ domOffset - 1 ]; + if ( !firstRecursionDirection ) { + direction = -1; + } } if ( node.nodeType === Node.TEXT_NODE ) { return ve.ce.getOffsetFromTextNode( node, node.length ); } else { - return ve.ce.getOffsetFromElementNode( node, 0, true ); + return ve.ce.getOffsetFromElementNode( node, 0, direction ); } }; diff --git a/modules/ve/test/ce/ve.ce.test.js b/modules/ve/test/ce/ve.ce.test.js index 8b4adf4a2c..fc07fd77ba 100644 --- a/modules/ve/test/ce/ve.ce.test.js +++ b/modules/ve/test/ce/ve.ce.test.js @@ -29,3 +29,75 @@ QUnit.test( 'getDomHash', 1, function ( assert ) { '####' ); } ); + +QUnit.test( 'getOffsetFrom(Element|Text)Node', function ( assert ) { + var i, dom, target, surface, documentModel, documentView, + expected = 0, + testCases = [ + { + 'msg': 'Annotated alien', + 'html': '
FooBarBaz
', + // CE html summary; + //FooBarBaz
+ 'expected': [ + 0, + 1, 1, + 2, + 3, + 4, 4, 4, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 7, + 8, + 9, 9, + 10 + ] + } + ]; + + for( i = 0; i < testCases.length; i++ ) { + expected += testCases[i].expected.length; + } + + QUnit.expect( expected ); + + function testOffsets( parent, testCase, expectedIndex ) { + var i; + switch ( parent.nodeType ) { + case Node.ELEMENT_NODE: + for ( i = 0; i <= parent.childNodes.length; i++ ) { + expectedIndex++; + assert.equal( + ve.ce.getOffsetFromElementNode( parent, i ), + testCase.expected[expectedIndex], + testCase.msg + ': offset ' + i + ' in <' + parent.nodeName.toLowerCase() + '>' + ); + if ( parent.childNodes[i] ) { + expectedIndex = testOffsets( parent.childNodes[i], testCase, expectedIndex ); + } + } + break; + case Node.TEXT_NODE: + for ( i = 0; i <= parent.data.length; i++ ) { + expectedIndex++; + assert.equal( + ve.ce.getOffsetFromTextNode( parent, i ), + testCase.expected[expectedIndex], + testCase.msg + ': offset ' + i + ' in "' + parent.data + '"' + ); + } + break; + } + return expectedIndex; + } + + for( i = 0; i < testCases.length; i++ ) { + dom = ve.createDocumentFromHtml( testCases[i].html ); + target = new ve.init.sa.Target( $( '#qunit-fixture' ), dom ); + surface = target.surface; + documentModel = surface.getModel().getDocument(); + documentView = surface.getView().getDocument(); + + testOffsets( documentView.documentNode.$[0], testCases[i], -1 ); + } +} ); +