jenkins-bot has submitted this change and it was merged.

Change subject: ve.SelectionState: DOM selection snapshot
......................................................................


ve.SelectionState: DOM selection snapshot

ve.SelectionState
* Snapshot similar to native DOM Selection, with anchor/focus

ve.ce.Surface
* Refactor to use ve.SelectionState
* Fix bug restoring backwards selections in onCopy

ve.ce.RangeState
* Refactor to use ve.SelectionState

Bug: T104517
Change-Id: I33c8073592341d5b775e2839be0221662705a6df
---
M .jsduck/categories.json
M build/modules.json
M demos/ve/desktop.html
M demos/ve/mobile.html
M src/ce/ve.ce.RangeState.js
M src/ce/ve.ce.Surface.js
A src/ve.SelectionState.js
M tests/ce/ve.ce.Surface.test.js
M tests/index.html
9 files changed, 345 insertions(+), 249 deletions(-)

Approvals:
  Esanders: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/.jsduck/categories.json b/.jsduck/categories.json
index 0dde668..dccbece 100644
--- a/.jsduck/categories.json
+++ b/.jsduck/categories.json
@@ -4,7 +4,14 @@
                "groups": [
                        {
                                "name": "Utilities",
-                               "classes": ["ve", "ve.Range", 
"ve.EventSequencer", "ve.Filibuster", "ve.TriggerListener"]
+                               "classes": [
+                                       "ve",
+                                       "ve.Range",
+                                       "ve.SelectionState",
+                                       "ve.EventSequencer",
+                                       "ve.Filibuster",
+                                       "ve.TriggerListener"
+                               ]
                        },
                        {
                                "name": "Nodes",
diff --git a/build/modules.json b/build/modules.json
index ff6437f..07c662f 100644
--- a/build/modules.json
+++ b/build/modules.json
@@ -196,6 +196,7 @@
        "visualEditor.core.build": {
                "scripts": [
                        "src/ve.Range.js",
+                       "src/ve.SelectionState.js",
                        "src/ve.Node.js",
                        "src/ve.BranchNode.js",
                        "src/ve.LeafNode.js",
diff --git a/demos/ve/desktop.html b/demos/ve/desktop.html
index 3d72adf..1cc9210 100644
--- a/demos/ve/desktop.html
+++ b/demos/ve/desktop.html
@@ -157,6 +157,7 @@
 
                <!-- visualEditor.core.build -->
                <script src="../../src/ve.Range.js"></script>
+               <script src="../../src/ve.SelectionState.js"></script>
                <script src="../../src/ve.Node.js"></script>
                <script src="../../src/ve.BranchNode.js"></script>
                <script src="../../src/ve.LeafNode.js"></script>
diff --git a/demos/ve/mobile.html b/demos/ve/mobile.html
index 1b22432..69e5243 100644
--- a/demos/ve/mobile.html
+++ b/demos/ve/mobile.html
@@ -158,6 +158,7 @@
 
                <!-- visualEditor.core.build -->
                <script src="../../src/ve.Range.js"></script>
+               <script src="../../src/ve.SelectionState.js"></script>
                <script src="../../src/ve.Node.js"></script>
                <script src="../../src/ve.BranchNode.js"></script>
                <script src="../../src/ve.LeafNode.js"></script>
diff --git a/src/ce/ve.ce.RangeState.js b/src/ce/ve.ce.RangeState.js
index 2bda46d..b98fd9b 100644
--- a/src/ce/ve.ce.RangeState.js
+++ b/src/ce/ve.ce.RangeState.js
@@ -57,37 +57,6 @@
 
 OO.initClass( ve.ce.RangeState );
 
-/* Static methods */
-
-/**
- * Create a plain selection object equivalent to no selection
- *
- * @return {Object} Plain selection object
- */
-ve.ce.RangeState.static.createNullSelection = function () {
-       return {
-               focusNode: null,
-               focusOffset: 0,
-               anchorNode: null,
-               anchorOffset: 0
-       };
-};
-
-/**
- * Compare two plain selection objects, checking that all values are equal
- * and all nodes are reference-equal.
- *
- * @param {Object} a First plain selection object
- * @param {Object} b First plain selection object
- * @return {boolean} Selections are identical
- */
-ve.ce.RangeState.static.compareSelections = function ( a, b ) {
-       return a.focusNode === b.focusNode &&
-               a.focusOffset === b.focusOffset &&
-               a.anchorNode === b.anchorNode &&
-               a.anchorOffset === b.anchorOffset;
-};
-
 /* Methods */
 
 /**
@@ -99,7 +68,7 @@
  */
 ve.ce.RangeState.prototype.saveState = function ( old, documentNode, 
selectionOnly ) {
        var $node, selection, anchorNodeChanged,
-               oldSelection = old ? old.misleadingSelection : 
this.constructor.static.createNullSelection(),
+               oldSelection = old ? old.misleadingSelection : 
ve.SelectionState.static.newNullSelection(),
                nativeSelection = 
documentNode.getElementDocument().getSelection();
 
        if (
@@ -107,19 +76,14 @@
                OO.ui.contains( documentNode.$element[0], 
nativeSelection.anchorNode, true )
        ) {
                // Freeze selection out of live object.
-               selection = {
-                       focusNode: nativeSelection.focusNode,
-                       focusOffset: nativeSelection.focusOffset,
-                       anchorNode: nativeSelection.anchorNode,
-                       anchorOffset: nativeSelection.anchorOffset
-               };
+               selection = new ve.SelectionState( nativeSelection );
        } else {
                // Use a blank selection if the selection is outside the 
document
-               selection = this.constructor.static.createNullSelection();
+               selection = ve.SelectionState.static.newNullSelection();
        }
 
        // Get new range information
-       if ( this.constructor.static.compareSelections( oldSelection, selection 
) ) {
+       if ( selection.equalsSelection( oldSelection ) ) {
                // No change; use old values for speed
                this.selectionChanged = false;
                this.veRange = old && old.veRange;
diff --git a/src/ce/ve.ce.Surface.js b/src/ce/ve.ce.Surface.js
index c710a02..cb76fbd 100644
--- a/src/ce/ve.ce.Surface.js
+++ b/src/ce/ve.ce.Surface.js
@@ -717,7 +717,7 @@
 /**
  * Update the fake selection while the surface is deactivated.
  *
- * While the surface is deactivated, all calls to showSelection will get 
redirected here.
+ * While the surface is deactivated, all calls to showModelSelection will get 
redirected here.
  */
 ve.ce.Surface.prototype.updateDeactivatedSelection = function () {
        var i, l, rects,
@@ -1531,7 +1531,7 @@
                }
        }
        this.renderSelectedContentBranchNode();
-       this.showSelection( this.getModel().getSelection() );
+       this.showModelSelection( this.getModel().getSelection() );
 };
 
 /**
@@ -1588,7 +1588,7 @@
  * @param {jQuery.Event} e Copy event
  */
 ve.ce.Surface.prototype.onCopy = function ( e ) {
-       var originalRange,
+       var originalSelection,
                clipboardIndex, clipboardItem,
                scrollTop, unsafeSelector, range, slice,
                selection = this.getModel().getSelection(),
@@ -1668,10 +1668,10 @@
 
                // If direct clipboard editing is not allowed, we must use the 
pasteTarget to
                // select the data we want to go in the clipboard
+               if ( this.getModel().getSelection() instanceof 
ve.dm.LinearSelection ) {
+                       // We have a selection in the document; preserve it so 
it can restored
+                       originalSelection = new ve.SelectionState( 
this.nativeSelection );
 
-               // If we have a range in the document, preserve it so it can 
restored
-               originalRange = this.getNativeRange();
-               if ( originalRange ) {
                        // Save scroll position before changing focus to 
"offscreen" paste target
                        scrollTop = this.$window.scrollTop();
 
@@ -1684,11 +1684,10 @@
 
                        setTimeout( function () {
                                // If the range was in $highlights (right-click 
copy), don't restore it
-                               if ( !OO.ui.contains( view.$highlights[0], 
originalRange.startContainer, true ) ) {
+                               if ( !OO.ui.contains( view.$highlights[0], 
originalSelection.focusNode, true ) ) {
                                        // Change focus back
                                        view.$documentNode[0].focus();
-                                       view.nativeSelection.removeAllRanges();
-                                       view.nativeSelection.addRange( 
originalRange.cloneRange() );
+                                       view.showSelectionState( 
originalSelection );
                                        // Restore scroll position
                                        view.$window.scrollTop( scrollTop );
                                }
@@ -2318,7 +2317,7 @@
        // called with the same (object-identical) selection object
        // (i.e. if the model is calling us back)
        if ( !this.isRenderingLocked() && selection !== this.newModelSelection 
) {
-               this.showSelection( selection );
+               this.showModelSelection( selection );
                this.checkUnicorns( false );
        }
        // Update the selection state in the SurfaceObserver
@@ -2437,7 +2436,7 @@
                return;
        }
        // Must re-apply the selection after re-rendering
-       this.showSelection( this.getModel().getSelection() );
+       this.showModelSelection( this.getModel().getSelection() );
        this.surfaceObserver.pollOnceNoEmit();
 };
 
@@ -2487,7 +2486,7 @@
                        // Re-apply selection in case the branch node change 
left us at an invalid offset
                        // e.g. in the document node.
                        surface.updateCursorHolders();
-                       surface.showSelection( 
surface.getModel().getSelection() );
+                       surface.showModelSelection( 
surface.getModel().getSelection() );
                } );
        }
 };
@@ -2798,7 +2797,7 @@
                executed = sequences[i].execute( this.surface ) || executed;
        }
        if ( executed ) {
-               this.showSelection( model.getSelection() );
+               this.showModelSelection( model.getSelection() );
        }
 };
 
@@ -2865,8 +2864,7 @@
 /**
  * Store a state snapshot at a keydown event, to be used in an after-keydown 
handler
  *
- * A selection object is stored, but only when the key event is a cursor key. 
It contains
- * anchorNode/anchorOffset/focusNode/focusOffset/isCollapsed like a 
nativeSelection.
+ * A ve.SelectionState object is stored, but only when the key event is a 
cursor key.
  * (It would be misleading to save selection properties for key events where 
the DOM might get
  * modified, because anchorNode/focusNode are live and mutable, and so the 
offsets may come to
  * point confusingly to different places than they did when the selection was 
saved).
@@ -2891,13 +2889,7 @@
                        e.keyCode === OO.ui.Keys.LEFT ||
                        e.keyCode === OO.ui.Keys.RIGHT
                ) {
-                       this.keyDownState.selection = {
-                               isCollapsed: this.nativeSelection.isCollapsed,
-                               anchorNode: this.nativeSelection.anchorNode,
-                               anchorOffset: this.nativeSelection.anchorOffset,
-                               focusNode: this.nativeSelection.focusNode,
-                               focusOffset: this.nativeSelection.focusOffset
-                       };
+                       this.keyDownState.selection = new ve.SelectionState( 
this.nativeSelection );
                }
        }
 };
@@ -2964,7 +2956,7 @@
                ( editingRange = activeTableNode.getEditingRange() ) &&
                !editingRange.containsRange( ve.ce.veRangeFromSelection( 
this.nativeSelection ) )
        ) {
-               this.showSelection( this.getModel().getSelection() );
+               this.showModelSelection( this.getModel().getSelection() );
                return true;
        } else {
                return false;
@@ -3768,16 +3760,17 @@
 };
 
 /**
- * Show selection
+ * Apply a DM selection to the DOM
  *
  * @method
  * @param {ve.dm.Selection} selection Selection to show
+ * @returns {boolean} Whether the selection actually changed
  */
-ve.ce.Surface.prototype.showSelection = function ( selection ) {
+ve.ce.Surface.prototype.showModelSelection = function ( selection ) {
        if ( this.deactivated ) {
                // Defer until view has updated
                setTimeout( this.updateDeactivatedSelection.bind( this ) );
-               return;
+               return false;
        }
 
        if (
@@ -3785,127 +3778,129 @@
                this.focusedNode ||
                this.focusedBlockSlug
        ) {
-               return;
+               return false;
        }
 
-       var endRange, oldRange, $node,
-               range = selection.getRange(),
-               rangeSelection = this.getRangeSelection( range ),
-               nativeRange = this.getElementDocument().createRange();
+       return this.showSelectionState( this.getSelectionState( 
selection.getRange() ) );
+};
 
-       nativeRange.setStart( rangeSelection.start.node, 
rangeSelection.start.offset );
-       if ( rangeSelection.end ) {
-               nativeRange.setEnd( rangeSelection.end.node, 
rangeSelection.end.offset );
+/**
+ * Apply a selection state to the DOM
+ *
+ * If the browser cannot show a backward selection, fall back to the forward 
equivalent
+ *
+ * @param {ve.SelectionState} selection The selection state to show
+ * @returns {boolean} Whether the selection actually changed
+ */
+ve.ce.Surface.prototype.showSelectionState = function ( selection ) {
+       var range,
+               extendedBackwards = false,
+               sel = this.nativeSelection,
+               newSel = selection;
+
+       if ( newSel.equalsSelection( sel ) ) {
+               return false;
        }
-       if ( rangeSelection.end && rangeSelection.isBackwards && 
this.nativeSelection.extend ) {
-               endRange = nativeRange.cloneRange();
-               endRange.collapse( false );
-               this.nativeSelection.removeAllRanges();
-               this.nativeSelection.addRange( endRange );
-               try {
-                       this.nativeSelection.extend( 
nativeRange.startContainer, nativeRange.startOffset );
-               } catch ( e ) {
-                       // Firefox sometimes fails when nodes are different,
-                       // see 
https://bugzilla.mozilla.org/show_bug.cgi?id=921444
-                       this.nativeSelection.addRange( nativeRange );
+
+       if ( newSel.isBackwards ) {
+               if ( sel.extend ) {
+                       // Set the range at the anchor, and extend backwards to 
the focus
+                       range = this.getElementDocument().createRange();
+                       range.setStart( newSel.anchorNode, newSel.anchorOffset 
);
+                       sel.removeAllRanges();
+                       sel.addRange( range );
+                       try {
+                               sel.extend( newSel.focusNode, 
newSel.focusOffset );
+                               extendedBackwards = true;
+                       } catch ( e ) {
+                               // Firefox sometimes fails when nodes are 
different
+                               // see 
https://bugzilla.mozilla.org/show_bug.cgi?id=921444
+                       }
                }
-       } else if ( !(
-               this.nativeSelection.rangeCount > 0 &&
-               ( oldRange = this.nativeSelection.getRangeAt( 0 ) ) &&
-               oldRange.startContainer === nativeRange.startContainer &&
-               oldRange.startOffset === nativeRange.startOffset &&
-               oldRange.endContainer === nativeRange.endContainer &&
-               oldRange.endOffset === nativeRange.endOffset
-       ) ) {
-               // Genuine selection change: apply it.
-               // TODO: this is slightly too zealous, because a cursor 
position at a node edge
-               // can have more than one (container,offset) representation
-               this.nativeSelection.removeAllRanges();
-               this.nativeSelection.addRange( nativeRange );
-       } else {
-               // Not a selection change: don't needlessly reapply the same 
selection.
-               return;
+               if ( !extendedBackwards ) {
+                       // Fallback: Apply the corresponding forward selection
+                       newSel = newSel.flip();
+                       if ( newSel.equalsSelection( sel ) ) {
+                               return false;
+                       }
+               }
+       }
+
+       if ( !extendedBackwards ) {
+               // Forward selection
+               sel.removeAllRanges();
+               sel.addRange( newSel.getNativeRange( this.getElementDocument() 
) );
        }
 
        // Setting a range doesn't give focus in all browsers so make sure this 
happens
        // Also set focus after range to prevent scrolling to top
-       if ( !OO.ui.contains( this.getElementDocument().activeElement, 
rangeSelection.start.node, true ) ) {
-               $( rangeSelection.start.node ).closest( 
'[contenteditable=true]' ).focus();
+       if ( !OO.ui.contains( this.getElementDocument().activeElement, 
newSel.focusNode, true ) ) {
+               $( newSel.focusNode ).closest( '[contenteditable=true]' 
).focus();
        } else {
-               $node = $( rangeSelection.start.node ).closest( '*' );
                // Scroll the node into view
-               OO.ui.Element.static.scrollIntoView( $node.get( 0 ) );
+               OO.ui.Element.static.scrollIntoView(
+                       $( newSel.focusNode ).closest( '*' ).get( 0 )
+               );
        }
+       return true;
 };
 
 /**
- * Get selection for a range.
+ * Get a SelectionState corresponding to a ve.Range.
  *
  * @method
  * @param {ve.Range} range Range to get selection for
- * @returns {Object} Object containing start and end node/offset selections, 
and an isBackwards flag.
+ * @returns {Object} The selection
+ * @returns.anchorNode {Node} The anchor node
+ * @returns.anchorOffset {number} The anchor offset
+ * @returns.focusNode {Node} The focus node
+ * @returns.focusOffset {number} The focus offset
+ * @returns.isCollapsed {boolean} True if the focus and anchor are in the same 
place
+ * @returns.isBackwards {boolean} True if the focus is before the anchor
  */
-ve.ce.Surface.prototype.getRangeSelection = function ( range ) {
-       range = new ve.Range(
-               this.getNearestCorrectOffset( range.from, -1 ),
-               this.getNearestCorrectOffset( range.to, 1 )
-       );
+ve.ce.Surface.prototype.getSelectionState = function ( range ) {
+       var anchor, focus;
 
-       if ( !range.isCollapsed() ) {
-               return {
-                       start: this.documentView.getNodeAndOffset( range.start 
),
-                       end: this.documentView.getNodeAndOffset( range.end ),
-                       isBackwards: range.isBackwards()
-               };
-       } else {
-               return {
-                       start: this.documentView.getNodeAndOffset( range.start )
-               };
-       }
+       // Anchor/focus at the nearest correct position in the direction that 
grows the selection
+       anchor = this.documentView.getNodeAndOffset(
+               this.getNearestCorrectOffset( range.from, range.isBackwards() ? 
1 : -1 )
+       );
+       focus = this.documentView.getNodeAndOffset(
+               this.getNearestCorrectOffset( range.to, range.isBackwards() ? 
-1 : 1 )
+       );
+       return new ve.SelectionState( {
+               anchorNode: anchor.node,
+               anchorOffset: anchor.offset,
+               focusNode: focus.node,
+               focusOffset: focus.offset,
+               isBackwards: range.isBackwards()
+       } );
 };
 
 /**
- * Get a native range object for a specified range
+ * Get a native range object for a specified ve.Range
  *
- * Native ranges are only used by linear selections.
- *
- * Doesn't correct backwards selection so should be used for measurement only.
+ * Native ranges are only used by linear selections. They don't show whether 
the selection
+ * is backwards, so they should be used for measurement only.
  *
  * @param {ve.Range} [range] Optional range to get the native range for, 
defaults to current selection's range
  * @return {Range|null} Native range object, or null if there is no suitable 
selection
  */
 ve.ce.Surface.prototype.getNativeRange = function ( range ) {
-       var nativeRange, rangeSelection,
-               selection = this.getModel().getSelection();
+       var selectionState, modelSelection;
 
-       if (
-               range && !this.deactivated &&
-               selection instanceof ve.dm.LinearSelection && 
selection.getRange().equalsSelection( range )
-       ) {
-               // Range requested is equivalent to native selection so reset
-               range = null;
+       if ( !range || (
+               !this.deactivated &&
+               ( modelSelection = this.getModel().getSelection() ) instanceof 
ve.dm.LinearSelection &&
+               modelSelection.getRange().equalsSelection( range )
+       ) ) {
+               // If no range specified, or range is equivalent to current 
native selection,
+               // then use the current native selection
+               selectionState = new ve.SelectionState( this.nativeSelection );
+       } else {
+               selectionState = this.getSelectionState( range );
        }
-       if ( !range ) {
-               // Use native range, unless selection is null
-               if ( !( selection instanceof ve.dm.LinearSelection ) ) {
-                       return null;
-               }
-               if ( this.nativeSelection.rangeCount > 0 ) {
-                       try {
-                               return this.nativeSelection.getRangeAt( 0 );
-                       } catch ( e ) {}
-               }
-               return null;
-       }
-
-       nativeRange = document.createRange();
-       rangeSelection = this.getRangeSelection( range );
-
-       nativeRange.setStart( rangeSelection.start.node, 
rangeSelection.start.offset );
-       if ( rangeSelection.end ) {
-               nativeRange.setEnd( rangeSelection.end.node, 
rangeSelection.end.offset );
-       }
-       return nativeRange;
+       return selectionState.getNativeRange( this.getElementDocument() );
 };
 
 /**
diff --git a/src/ve.SelectionState.js b/src/ve.SelectionState.js
new file mode 100644
index 0000000..128d6bc
--- /dev/null
+++ b/src/ve.SelectionState.js
@@ -0,0 +1,126 @@
+/*!
+ * VisualEditor DOM selection-like class
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
+/**
+ * Like the DOM Selection object, but not updated live from the actual 
selection
+ *
+ * WARNING: the Nodes are still live and mutable, which can change the meaning
+ * of the offsets or invalidate the value of isBackwards.
+ *
+ * @class
+ *
+ * @constructor
+ * @param {ve.SelectionState|Selection|Object} selection DOM Selection-like 
object
+ * @param {Node|null} selection.anchorNode the Anchor node (null if no 
selection)
+ * @param {number} selection.anchorOffset the Anchor offset (0 if no selection)
+ * @param {Node|null} selection.focusNode the Focus node (null if no selection)
+ * @param {number} selection.focusOffset the Focusoffset (0 if no selection)
+ * @param {boolean} [selection.isCollapsed] Whether the anchor and focus are 
the same
+ * @param {boolean} [selection.isBackwards] Whether the focus is before the 
anchor in document order
+ */
+ve.SelectionState = function VeSelectionState( selection ) {
+       this.anchorNode = selection.anchorNode;
+       this.anchorOffset = selection.anchorOffset;
+       this.focusNode = selection.focusNode;
+       this.focusOffset = selection.focusOffset;
+
+       this.isCollapsed = selection.isCollapsed;
+       if ( this.isCollapsed === undefined ) {
+               // Set to true if nodes are null (matches DOM Selection 
object's behaviour)
+               this.isCollapsed = this.anchorNode === this.focusNode &&
+                       this.anchorOffset === this.focusOffset;
+       }
+       this.isBackwards = selection.isBackwards;
+       if ( this.isBackwards === undefined ) {
+               // Set to false if nodes are null
+               this.isBackwards = this.focusNode !== null && 
ve.compareDocumentOrder(
+                       this.focusNode,
+                       this.focusOffset,
+                       this.anchorNode,
+                       this.anchorOffset
+               ) < 0;
+       }
+};
+
+/* Inheritance */
+
+OO.initClass( ve.SelectionState );
+
+/* Static methods */
+
+/**
+ * Create a selection state object representing no selection
+ *
+ * @return {ve.SelectionState} Object representing no selection
+ */
+ve.SelectionState.static.newNullSelection = function () {
+       return new ve.SelectionState( {
+               focusNode: null,
+               focusOffset: 0,
+               anchorNode: null,
+               anchorOffset: 0
+       } );
+};
+
+/* Methods */
+
+/**
+ * Returns the selection with the anchor and focus swapped
+ *
+ * @returns {Object} selection with anchor/focus swapped. Object-identical to 
this if isCollapsed
+ */
+ve.SelectionState.prototype.flip = function () {
+       if ( this.isCollapsed ) {
+               return this;
+       }
+       return {
+               anchorNode: this.focusNode,
+               anchorOffset: this.focusOffset,
+               focusNode: this.anchorNode,
+               focusOffset: this.anchorOffset,
+               isCollapsed: false,
+               isBackwards: !this.isBackwards
+       };
+};
+
+/**
+ * Whether the selection represents is the same range as another DOM 
Selection-like object
+ *
+ * @param {Object} other DOM Selection-like object
+ * @returns {boolean} True if the anchors/focuses are equal (including null)
+ */
+ve.SelectionState.prototype.equalsSelection = function ( other ) {
+       return this.anchorNode === other.anchorNode &&
+               this.anchorOffset === other.anchorOffset &&
+               this.focusNode === other.focusNode &&
+               this.focusOffset === other.focusOffset;
+};
+
+/**
+ * Get a range representation of the selection
+ *
+ * N.B. Range objects do not show whether the selection is backwards
+ *
+ * @param {HTMLDocument} doc The owner document of the selection nodes
+ * @returns {Range|null} Range
+ */
+ve.SelectionState.prototype.getNativeRange = function ( doc ) {
+       var range;
+       if ( this.anchorNode === null ) {
+               return null;
+       }
+       range = doc.createRange();
+       if ( this.isBackwards ) {
+               range.setStart( this.focusNode, this.focusOffset );
+               range.setEnd( this.anchorNode, this.anchorOffset );
+       } else {
+               range.setStart( this.anchorNode, this.anchorOffset );
+               if ( !this.isCollapsed ) {
+                       range.setEnd( this.focusNode, this.focusOffset );
+               }
+       }
+       return range;
+};
diff --git a/tests/ce/ve.ce.Surface.test.js b/tests/ce/ve.ce.Surface.test.js
index 366912f..4458a25 100644
--- a/tests/ce/ve.ce.Surface.test.js
+++ b/tests/ce/ve.ce.Surface.test.js
@@ -1408,8 +1408,8 @@
        view.destroy();
 } );
 
-QUnit.test( 'getRangeSelection', function ( assert ) {
-       var i, j, l, view, selection, expectedNode, internlListNode, node, msg,
+QUnit.test( 'getSelectionState', function ( assert ) {
+       var i, j, l, view, selection, expectedNode, internalListNode, node, msg,
                expect = 0,
                cases = [
                        {
@@ -1425,102 +1425,102 @@
                                        '2<b>n</b>d' +
                                '</p>',
                                expected: [
-                                       { startNode: 'Foo', startOffset: 0 },
-                                       { startNode: 'Foo', startOffset: 0 },
-                                       { startNode: 'Foo', startOffset: 1 },
-                                       { startNode: 'Foo', startOffset: 2 },
-                                       { startNode: 'Foo', startOffset: 3 },
+                                       { anchorNode: 'Foo', anchorOffset: 0 },
+                                       { anchorNode: 'Foo', anchorOffset: 0 },
+                                       { anchorNode: 'Foo', anchorOffset: 1 },
+                                       { anchorNode: 'Foo', anchorOffset: 2 },
+                                       { anchorNode: 'Foo', anchorOffset: 3 },
                                        null, // Focusable
-                                       { startNode: 'Whee', startOffset: 0 },
-                                       { startNode: 'Whee', startOffset: 1 },
-                                       { startNode: 'Whee', startOffset: 2 },
-                                       { startNode: 'Whee', startOffset: 3 },
-                                       { startNode: 'Whee', startOffset: 4 },
-                                       { startNode: 'Whee', startOffset: 4, 
endNode: '2', endOffset: 0 },
-                                       { startNode: '2', startOffset: 0 },
-                                       { startNode: '2', startOffset: 1 },
-                                       { startNode: 'n', startOffset: 1 },
-                                       { startNode: 'd', startOffset: 1 }
+                                       { anchorNode: 'Whee', anchorOffset: 0 },
+                                       { anchorNode: 'Whee', anchorOffset: 1 },
+                                       { anchorNode: 'Whee', anchorOffset: 2 },
+                                       { anchorNode: 'Whee', anchorOffset: 3 },
+                                       { anchorNode: 'Whee', anchorOffset: 4 },
+                                       { anchorNode: 'Whee', anchorOffset: 4, 
focusNode: '2', focusOffset: 0 },
+                                       { anchorNode: '2', anchorOffset: 0 },
+                                       { anchorNode: '2', anchorOffset: 1 },
+                                       { anchorNode: 'n', anchorOffset: 1 },
+                                       { anchorNode: 'd', anchorOffset: 1 }
                                ]
                        },
                        {
                                msg: 'Simple example doc',
                                html: ve.dm.example.html,
                                expected: [
-                                       { startNode: 'a', startOffset: 0 },
-                                       { startNode: 'a', startOffset: 0 },
-                                       { startNode: 'a', startOffset: 1 },
-                                       { startNode: 'b', startOffset: 1 },
-                                       { startNode: 'c', startOffset: 1 },
-                                       { startNode: 'c', startOffset: 1, 
endNode: 'd', endOffset: 0 },
-                                       { startNode: 'c', startOffset: 1, 
endNode: 'd', endOffset: 0 },
-                                       { startNode: 'c', startOffset: 1, 
endNode: 'd', endOffset: 0 },
-                                       { startNode: 'c', startOffset: 1, 
endNode: 'd', endOffset: 0 },
-                                       { startNode: 'c', startOffset: 1, 
endNode: 'd', endOffset: 0 },
+                                       { anchorNode: 'a', anchorOffset: 0 },
+                                       { anchorNode: 'a', anchorOffset: 0 },
+                                       { anchorNode: 'a', anchorOffset: 1 },
+                                       { anchorNode: 'b', anchorOffset: 1 },
+                                       { anchorNode: 'c', anchorOffset: 1 },
+                                       { anchorNode: 'c', anchorOffset: 1, 
focusNode: 'd', focusOffset: 0 },
+                                       { anchorNode: 'c', anchorOffset: 1, 
focusNode: 'd', focusOffset: 0 },
+                                       { anchorNode: 'c', anchorOffset: 1, 
focusNode: 'd', focusOffset: 0 },
+                                       { anchorNode: 'c', anchorOffset: 1, 
focusNode: 'd', focusOffset: 0 },
+                                       { anchorNode: 'c', anchorOffset: 1, 
focusNode: 'd', focusOffset: 0 },
                                        // 10
-                                       { startNode: 'd', startOffset: 0 },
-                                       { startNode: 'd', startOffset: 1 },
-                                       { startNode: 'd', startOffset: 1, 
endNode: 'e', endOffset: 0 },
-                                       { startNode: 'd', startOffset: 1, 
endNode: 'e', endOffset: 0 },
-                                       { startNode: 'd', startOffset: 1, 
endNode: 'e', endOffset: 0 },
-                                       { startNode: 'e', startOffset: 0 },
-                                       { startNode: 'e', startOffset: 1 },
-                                       { startNode: 'e', startOffset: 1, 
endNode: 'f', endOffset: 0 },
-                                       { startNode: 'e', startOffset: 1, 
endNode: 'f', endOffset: 0 },
-                                       { startNode: 'e', startOffset: 1, 
endNode: 'f', endOffset: 0 },
+                                       { anchorNode: 'd', anchorOffset: 0 },
+                                       { anchorNode: 'd', anchorOffset: 1 },
+                                       { anchorNode: 'd', anchorOffset: 1, 
focusNode: 'e', focusOffset: 0 },
+                                       { anchorNode: 'd', anchorOffset: 1, 
focusNode: 'e', focusOffset: 0 },
+                                       { anchorNode: 'd', anchorOffset: 1, 
focusNode: 'e', focusOffset: 0 },
+                                       { anchorNode: 'e', anchorOffset: 0 },
+                                       { anchorNode: 'e', anchorOffset: 1 },
+                                       { anchorNode: 'e', anchorOffset: 1, 
focusNode: 'f', focusOffset: 0 },
+                                       { anchorNode: 'e', anchorOffset: 1, 
focusNode: 'f', focusOffset: 0 },
+                                       { anchorNode: 'e', anchorOffset: 1, 
focusNode: 'f', focusOffset: 0 },
                                        // 20
-                                       { startNode: 'f', startOffset: 0 },
-                                       { startNode: 'f', startOffset: 1 },
-                                       { startNode: 'f', startOffset: 1, 
endNode: 'g', endOffset: 0 },
-                                       { startNode: 'f', startOffset: 1, 
endNode: 'g', endOffset: 0 },
-                                       { startNode: 'f', startOffset: 1, 
endNode: 'g', endOffset: 0 },
-                                       { startNode: 'f', startOffset: 1, 
endNode: 'g', endOffset: 0 },
-                                       { startNode: 'f', startOffset: 1, 
endNode: 'g', endOffset: 0 },
-                                       { startNode: 'f', startOffset: 1, 
endNode: 'g', endOffset: 0 },
-                                       { startNode: 'f', startOffset: 1, 
endNode: 'g', endOffset: 0 },
-                                       { startNode: 'g', startOffset: 0 },
+                                       { anchorNode: 'f', anchorOffset: 0 },
+                                       { anchorNode: 'f', anchorOffset: 1 },
+                                       { anchorNode: 'f', anchorOffset: 1, 
focusNode: 'g', focusOffset: 0 },
+                                       { anchorNode: 'f', anchorOffset: 1, 
focusNode: 'g', focusOffset: 0 },
+                                       { anchorNode: 'f', anchorOffset: 1, 
focusNode: 'g', focusOffset: 0 },
+                                       { anchorNode: 'f', anchorOffset: 1, 
focusNode: 'g', focusOffset: 0 },
+                                       { anchorNode: 'f', anchorOffset: 1, 
focusNode: 'g', focusOffset: 0 },
+                                       { anchorNode: 'f', anchorOffset: 1, 
focusNode: 'g', focusOffset: 0 },
+                                       { anchorNode: 'f', anchorOffset: 1, 
focusNode: 'g', focusOffset: 0 },
+                                       { anchorNode: 'g', anchorOffset: 0 },
                                        // 30
-                                       { startNode: 'g', startOffset: 1 },
-                                       { startNode: 'g', startOffset: 1, 
endNode: 'h', endOffset: 0 },
-                                       { startNode: 'g', startOffset: 1, 
endNode: 'h', endOffset: 0 },
-                                       { startNode: 'g', startOffset: 1, 
endNode: 'h', endOffset: 0 },
-                                       { startNode: 'g', startOffset: 1, 
endNode: 'h', endOffset: 0 },
-                                       { startNode: 'g', startOffset: 1, 
endNode: 'h', endOffset: 0 },
-                                       { startNode: 'g', startOffset: 1, 
endNode: 'h', endOffset: 0 },
-                                       { startNode: 'g', startOffset: 1, 
endNode: 'h', endOffset: 0 },
-                                       { startNode: 'h', startOffset: 0 },
-                                       { startNode: 'h', startOffset: 1 },
+                                       { anchorNode: 'g', anchorOffset: 1 },
+                                       { anchorNode: 'g', anchorOffset: 1, 
focusNode: 'h', focusOffset: 0 },
+                                       { anchorNode: 'g', anchorOffset: 1, 
focusNode: 'h', focusOffset: 0 },
+                                       { anchorNode: 'g', anchorOffset: 1, 
focusNode: 'h', focusOffset: 0 },
+                                       { anchorNode: 'g', anchorOffset: 1, 
focusNode: 'h', focusOffset: 0 },
+                                       { anchorNode: 'g', anchorOffset: 1, 
focusNode: 'h', focusOffset: 0 },
+                                       { anchorNode: 'g', anchorOffset: 1, 
focusNode: 'h', focusOffset: 0 },
+                                       { anchorNode: 'g', anchorOffset: 1, 
focusNode: 'h', focusOffset: 0 },
+                                       { anchorNode: 'h', anchorOffset: 0 },
+                                       { anchorNode: 'h', anchorOffset: 1 },
                                        // 40
                                        null, // Focusable
-                                       { startNode: 'i', startOffset: 0 },
-                                       { startNode: 'i', startOffset: 1 },
-                                       { startNode: 'i', startOffset: 1, 
endNode: 'j', endOffset: 0 },
-                                       { startNode: 'i', startOffset: 1, 
endNode: 'j', endOffset: 0 },
-                                       { startNode: 'i', startOffset: 1, 
endNode: 'j', endOffset: 0 },
-                                       { startNode: 'j', startOffset: 0 },
-                                       { startNode: 'j', startOffset: 1 },
-                                       { startNode: 'j', startOffset: 1, 
endNode: 'k', endOffset: 0 },
-                                       { startNode: 'j', startOffset: 1, 
endNode: 'k', endOffset: 0 },
+                                       { anchorNode: 'i', anchorOffset: 0 },
+                                       { anchorNode: 'i', anchorOffset: 1 },
+                                       { anchorNode: 'i', anchorOffset: 1, 
focusNode: 'j', focusOffset: 0 },
+                                       { anchorNode: 'i', anchorOffset: 1, 
focusNode: 'j', focusOffset: 0 },
+                                       { anchorNode: 'i', anchorOffset: 1, 
focusNode: 'j', focusOffset: 0 },
+                                       { anchorNode: 'j', anchorOffset: 0 },
+                                       { anchorNode: 'j', anchorOffset: 1 },
+                                       { anchorNode: 'j', anchorOffset: 1, 
focusNode: 'k', focusOffset: 0 },
+                                       { anchorNode: 'j', anchorOffset: 1, 
focusNode: 'k', focusOffset: 0 },
                                        // 50
-                                       { startNode: 'j', startOffset: 1, 
endNode: 'k', endOffset: 0 },
-                                       { startNode: 'k', startOffset: 0 },
-                                       { startNode: 'k', startOffset: 1 },
-                                       { startNode: 'k', startOffset: 1, 
endNode: 'l', endOffset: 0 },
-                                       { startNode: 'k', startOffset: 1, 
endNode: 'l', endOffset: 0 },
-                                       { startNode: 'k', startOffset: 1, 
endNode: 'l', endOffset: 0 },
-                                       { startNode: 'l', startOffset: 0 },
-                                       { startNode: 'l', startOffset: 1 },
-                                       { startNode: 'l', startOffset: 1, 
endNode: 'm', endOffset: 0 },
-                                       { startNode: 'm', startOffset: 0 },
+                                       { anchorNode: 'j', anchorOffset: 1, 
focusNode: 'k', focusOffset: 0 },
+                                       { anchorNode: 'k', anchorOffset: 0 },
+                                       { anchorNode: 'k', anchorOffset: 1 },
+                                       { anchorNode: 'k', anchorOffset: 1, 
focusNode: 'l', focusOffset: 0 },
+                                       { anchorNode: 'k', anchorOffset: 1, 
focusNode: 'l', focusOffset: 0 },
+                                       { anchorNode: 'k', anchorOffset: 1, 
focusNode: 'l', focusOffset: 0 },
+                                       { anchorNode: 'l', anchorOffset: 0 },
+                                       { anchorNode: 'l', anchorOffset: 1 },
+                                       { anchorNode: 'l', anchorOffset: 1, 
focusNode: 'm', focusOffset: 0 },
+                                       { anchorNode: 'm', anchorOffset: 0 },
                                        // 60
-                                       { startNode: 'm', startOffset: 1 }
+                                       { anchorNode: 'm', anchorOffset: 1 }
                                ]
                        }
                ];
 
        for ( i = 0; i < cases.length; i++ ) {
                for ( j = 0; j < cases[i].expected.length; j++ ) {
-                       expect += cases[i].expected[j] ? ( 
cases[i].expected[j].endNode ? 4 : 2 ) : 1;
+                       expect += cases[i].expected[j] ? ( 
cases[i].expected[j].focusNode === undefined ? 2 : 4 ) : 1;
                }
        }
 
@@ -1528,25 +1528,25 @@
 
        for ( i = 0; i < cases.length; i++ ) {
                view = ve.test.utils.createSurfaceViewFromHtml( cases[i].html );
-               internlListNode = 
view.getModel().getDocument().getInternalList().getListNode();
-               for ( j = 0, l = internlListNode.getOuterRange().start; j < l; 
j++ ) {
+               internalListNode = 
view.getModel().getDocument().getInternalList().getListNode();
+               for ( j = 0, l = internalListNode.getOuterRange().start; j < l; 
j++ ) {
                        msg = ' at ' + j + ' in ' + cases[i].msg;
                        node = 
view.getDocument().getDocumentNode().getNodeFromOffset( j );
                        if ( node.isFocusable() ) {
                                assert.strictEqual( null, cases[i].expected[j], 
'Focusable node at ' + j );
                        } else {
-                               selection = view.getRangeSelection( new 
ve.Range( j ) );
-                               if ( selection.end ) {
-                                       expectedNode = $( '<div>' ).html( 
cases[i].expected[j].startNode )[0].childNodes[0];
-                                       assert.equalDomElement( 
selection.start.node, expectedNode, 'Start node ' + msg );
-                                       assert.strictEqual( 
selection.start.offset, cases[i].expected[j].startOffset, 'Start offfset ' + 
msg );
-                                       expectedNode = $( '<div>' ).html( 
cases[i].expected[j].endNode )[0].childNodes[0];
-                                       assert.equalDomElement( 
selection.end.node, expectedNode, 'End node ' + msg );
-                                       assert.strictEqual( 
selection.end.offset, cases[i].expected[j].endOffset, 'End offfset ' + msg );
+                               selection = view.getSelectionState( new 
ve.Range( j ) );
+                               if ( selection.isCollapsed ) {
+                                       expectedNode = $( '<div>' ).html( 
cases[i].expected[j].anchorNode )[0].childNodes[0];
+                                       assert.equalDomElement( 
selection.anchorNode, expectedNode, 'Node ' + msg );
+                                       assert.strictEqual( 
selection.anchorOffset, cases[i].expected[j].anchorOffset, 'Offset ' + msg );
                                } else {
-                                       expectedNode = $( '<div>' ).html( 
cases[i].expected[j].startNode )[0].childNodes[0];
-                                       assert.equalDomElement( 
selection.start.node, expectedNode, 'Node ' + msg );
-                                       assert.strictEqual( 
selection.start.offset, cases[i].expected[j].startOffset, 'Offset ' + msg );
+                                       expectedNode = $( '<div>' ).html( 
cases[i].expected[j].anchorNode )[0].childNodes[0];
+                                       assert.equalDomElement( 
selection.anchorNode, expectedNode, 'Anchor node ' + msg );
+                                       assert.strictEqual( 
selection.anchorOffset, cases[i].expected[j].anchorOffset, 'Anchor offset ' + 
msg );
+                                       expectedNode = $( '<div>' ).html( 
cases[i].expected[j].focusNode )[0].childNodes[0];
+                                       assert.equalDomElement( 
selection.focusNode, expectedNode, 'End node ' + msg );
+                                       assert.strictEqual( 
selection.focusOffset, cases[i].expected[j].focusOffset, 'Focus offset ' + msg 
);
                                }
                        }
                }
@@ -1599,7 +1599,7 @@
 // TODO: ve.ce.Surface#handleTableDelete
 // TODO: ve.ce.Surface#handleTableEditingEscape
 // TODO: ve.ce.Surface#handleTableEnter
-// TODO: ve.ce.Surface#showSelection
+// TODO: ve.ce.Surface#showModelSelection
 // TODO: ve.ce.Surface#appendHighlights
 // TODO: ve.ce.Surface#incRenderLock
 // TODO: ve.ce.Surface#decRenderLock
diff --git a/tests/index.html b/tests/index.html
index 9dbac1a..e80df67 100644
--- a/tests/index.html
+++ b/tests/index.html
@@ -88,6 +88,7 @@
 
                <!-- visualEditor.core.build -->
                <script src="../src/ve.Range.js"></script>
+               <script src="../src/ve.SelectionState.js"></script>
                <script src="../src/ve.Node.js"></script>
                <script src="../src/ve.BranchNode.js"></script>
                <script src="../src/ve.LeafNode.js"></script>

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

Gerrit-MessageType: merged
Gerrit-Change-Id: I33c8073592341d5b775e2839be0221662705a6df
Gerrit-PatchSet: 9
Gerrit-Project: VisualEditor/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Divec <da...@troi.org>
Gerrit-Reviewer: Esanders <esand...@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