Divec has uploaded a new change for review. https://gerrit.wikimedia.org/r/217222
Change subject: WIP: Cursoring: find adjacent position in DOM order ...................................................................... WIP: Cursoring: find adjacent position in DOM order WIP only because the dependent code is not yet pushed Change-Id: I12a9c9a5f1fcf7c9415c6d1f9d1d319ce1563b46 --- M src/ve.utils.js M tests/ve.test.js 2 files changed, 186 insertions(+), 0 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/VisualEditor/VisualEditor refs/changes/22/217222/1 diff --git a/src/ve.utils.js b/src/ve.utils.js index 81a0ef6..783acdc 100644 --- a/src/ve.utils.js +++ b/src/ve.utils.js @@ -1458,3 +1458,75 @@ ); return $result.contents(); }; + +/** + * Get the closest DOM position in document order (forward or reverse) + * + * A DOM position is represented as an object with "node" and "offset" properties. The noDescend + * option can be used to exclude the positions inside certain element nodes; it is a jQuery + * selector/function ( used as a test by $node.is() - see http://api.jquery.com/is/ ). + * + * Caveat: Distinct DOM positions may be treated equivalently for cursoring purposes, e.g. the + * positions just before/just after the boundary of a text element or an annotation element, or + * the start/the interior of certain grapheme clusters such as 'x\u0301'. Chromium normalizes + * cursor focus/offset, when they are set, to the start-most equivalent position in document order. + * Firefox does not normalize, but jumps when cursoring over positions that are equivalent to the + * start position. + * + * Even aside from equivalence, some DOM positions cannot actually hold the cursor; e.g. the start + * of the interior of a table node. + * + * @param {Object} position Start position + * @param {Node} position.node Start node + * @param {Node} position.offset Start offset + * @param {number} direction +1 for forward, -1 for reverse + * @param {Object} [options] + * @param {Function|string} [options.noDescend] Selector or function: nodes to skip over + * @returns {Object} The adjacent DOM position encountered + * @returns.node {Node|null} The node, or null if we stepped past the root node + * @returns.offset {number|null} The offset, or null if we stepped past the root node + */ +ve.adjacentDomPosition = function ( position, direction, options ) { + var forward, childNode, + node = position.node, + offset = position.offset, + noDescend = ( options || {} ).noDescend; + + direction = direction < 0 ? -1 : 1; + forward = ( direction === 1 ); + + // If we're at the node's leading edge, return the adjacent position in the parent node + if ( offset === ( forward ? node.length || node.childNodes.length : 0 ) ) { + if ( node.parentNode === null ) { + return { node: null, offset: null }; + } + return { + node: node.parentNode, + offset: Array.prototype.indexOf.call( node.parentNode.childNodes, node ) + + ( forward ? 1 : 0 ) + }; + } + + // If we're in a text node, return the position in this node at the next offset + if ( node.nodeType === Node.TEXT_NODE ) { + return { node: node, offset: offset + ( forward ? 1 : -1 ) }; + } + + childNode = node.childNodes[ forward ? offset : offset - 1 ]; + + // If the child is an element matching noDescend, do not descend into it: instead, + // return the position at the next offset in the current node + if ( + noDescend && + childNode.nodeType === Node.ELEMENT_NODE && + $( childNode ).is( noDescend ) + ) { + return { node: node, offset: offset + ( forward ? 1 : -1 ) }; + } + + // Return the closest offset inside the child node + return { + node: childNode, + offset: forward ? 0 : childNode.length || childNode.childNodes.length + }; +}; diff --git a/tests/ve.test.js b/tests/ve.test.js index 482dd36..fa5c5ac 100644 --- a/tests/ve.test.js +++ b/tests/ve.test.js @@ -883,3 +883,117 @@ ); } } ); + +QUnit.test( 'adjacentDomPosition', function ( assert ) { + var tests, direction, i, len, test, offsetPaths, position, + div = document.createElement( 'div' ); + + // In the following tests, the html is put inside the top-level div as innerHTML. Then + // ve.adjacentDomNode is called with the position just inside the div (i.e. + // { node: div, offset: 0 } for forward direction tests, and + // { node: div, offset: div.childNodes.length } for reverse direction tests). The result + // of the first call is passed into the function again, and so on iteratively until the + // function returns null. The 'path' properties are a list of descent offsets to find a + // particular position node from the top-level div. E.g. a path of [ 5, 7 ] refers to the + // node div.childNodes[ 5 ].childNodes[ 7 ] . + tests = [ + { + title: 'Simple p node', + html: '<p>x</p>', + options: {}, + expectedOffsetPaths: [ + [ 0 ], + [ 0, 0 ], + [ 0, 0, 0 ], + [ 0, 0, 1 ], + [ 0, 1 ], + [ 1 ] + ] + }, + { + title: 'Filtered descent', + html: '<div class="x">foo</div><div class="y">bar</div>', + options: { noDescend: '.x' }, + expectedOffsetPaths: [ + [ 0 ], + [ 1 ], + [ 1, 0 ], + [ 1, 0, 0 ], + [ 1, 0, 1 ], + [ 1, 0, 2 ], + [ 1, 0, 3 ], + [ 1, 1 ], + [ 2 ] + ] + }, + { + title: 'Empty tags and heavy nesting', + html: '<div><br/><p>foo <b>bar <i>baz</i></b></p></div>', + options: {}, + expectedOffsetPaths: [ + [ 0 ], + [ 0, 0 ], + // Inside the <br/> tag *is* a position (a meaningless one) + [ 0, 0, 0 ], + [ 0, 1 ], + [ 0, 1, 0 ], + [ 0, 1, 0, 0 ], + [ 0, 1, 0, 1 ], + [ 0, 1, 0, 2 ], + [ 0, 1, 0, 3 ], + [ 0, 1, 0, 4 ], + [ 0, 1, 1 ], + [ 0, 1, 1, 0 ], + [ 0, 1, 1, 0, 0 ], + [ 0, 1, 1, 0, 1 ], + [ 0, 1, 1, 0, 2 ], + [ 0, 1, 1, 0, 3 ], + [ 0, 1, 1, 0, 4 ], + [ 0, 1, 1, 1 ], + [ 0, 1, 1, 1, 0 ], + [ 0, 1, 1, 1, 0, 0 ], + [ 0, 1, 1, 1, 0, 1 ], + [ 0, 1, 1, 1, 0, 2 ], + [ 0, 1, 1, 1, 0, 3 ], + [ 0, 1, 1, 1, 1 ], + [ 0, 1, 1, 2 ], + [ 0, 1, 2 ], + [ 0, 2 ], + [ 1 ] + ] + } + ]; + + QUnit.expect( 2 * tests.length ); + + for ( direction in { forward: undefined, backward: undefined } ) { + for ( i = 0, len = tests.length; i < len; i++ ) { + test = tests[i]; + div.innerHTML = test.html; + offsetPaths = []; + position = { + node: div, + offset: direction === 'backward' ? div.childNodes.length : 0 + }; + while ( position.node !== null ) { + offsetPaths.push( + ve.getOffsetPath( div, position.node, position.offset ) + ); + position = ve.adjacentDomPosition( + position, + direction === 'backward' ? -1 : 1, + test.options + ); + } + assert.deepEqual( + offsetPaths, + ( + direction === 'backward' ? + test.expectedOffsetPaths.slice().reverse() : + test.expectedOffsetPaths + ), + test.title + ' (' + direction + ')' + ); + } + } +} ); -- To view, visit https://gerrit.wikimedia.org/r/217222 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I12a9c9a5f1fcf7c9415c6d1f9d1d319ce1563b46 Gerrit-PatchSet: 1 Gerrit-Project: VisualEditor/VisualEditor Gerrit-Branch: master Gerrit-Owner: Divec <da...@sheetmusic.org.uk> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits