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

Reply via email to