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