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

Change subject: Refactor SurfaceObserver pollOnceInternal
......................................................................


Refactor SurfaceObserver pollOnceInternal

Refactor pollOnceInternal to make its logic clearer.

* Remove ve.ce.DomRange class (had misleadingly live node properties)
* Move much of the SurfaceObserver state to new ve.ce.RangeState class

Change-Id: Iccb6d39022ee06c8bd1fb687343efa160dbac44f
---
M .docs/categories.json
M .docs/eg-iframe.html
M build/modules.json
M demos/ve/desktop.html
M demos/ve/mobile.html
D src/ce/ve.ce.DomRange.js
A src/ce/ve.ce.RangeState.js
M src/ce/ve.ce.SurfaceObserver.js
M tests/index.html
9 files changed, 266 insertions(+), 178 deletions(-)

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



diff --git a/.docs/categories.json b/.docs/categories.json
index 5a5e4a4..9b5d74b 100644
--- a/.docs/categories.json
+++ b/.docs/categories.json
@@ -96,7 +96,7 @@
                                        "ve.ce.NodeFactory",
                                        "ve.ce.Surface",
                                        "ve.ce.SurfaceObserver",
-                                       "ve.ce.DomRange"
+                                       "ve.ce.RangeState"
                                ]
                        },
                        {
diff --git a/.docs/eg-iframe.html b/.docs/eg-iframe.html
index 9562a05..a475983 100644
--- a/.docs/eg-iframe.html
+++ b/.docs/eg-iframe.html
@@ -210,7 +210,7 @@
                <script 
src="../src/dm/metaitems/ve.dm.CommentMetaItem.js"></script>
                <script src="../src/dm/nodes/ve.dm.CommentNode.js"></script>
                <script src="../src/ce/ve.ce.js"></script>
-               <script src="../src/ce/ve.ce.DomRange.js"></script>
+               <script src="../src/ce/ve.ce.RangeState.js"></script>
                <script src="../src/ce/ve.ce.AnnotationFactory.js"></script>
                <script src="../src/ce/ve.ce.NodeFactory.js"></script>
                <script src="../src/ce/ve.ce.Document.js"></script>
diff --git a/build/modules.json b/build/modules.json
index c96abff..0f8adb3 100644
--- a/build/modules.json
+++ b/build/modules.json
@@ -235,7 +235,7 @@
                        "src/dm/metaitems/ve.dm.CommentMetaItem.js",
                        "src/dm/nodes/ve.dm.CommentNode.js",
                        "src/ce/ve.ce.js",
-                       "src/ce/ve.ce.DomRange.js",
+                       "src/ce/ve.ce.RangeState.js",
                        "src/ce/ve.ce.AnnotationFactory.js",
                        "src/ce/ve.ce.NodeFactory.js",
                        "src/ce/ve.ce.Document.js",
diff --git a/demos/ve/desktop.html b/demos/ve/desktop.html
index 41ab04d..49ddc20 100644
--- a/demos/ve/desktop.html
+++ b/demos/ve/desktop.html
@@ -222,7 +222,7 @@
                <script 
src="../../src/dm/metaitems/ve.dm.CommentMetaItem.js"></script>
                <script src="../../src/dm/nodes/ve.dm.CommentNode.js"></script>
                <script src="../../src/ce/ve.ce.js"></script>
-               <script src="../../src/ce/ve.ce.DomRange.js"></script>
+               <script src="../../src/ce/ve.ce.RangeState.js"></script>
                <script src="../../src/ce/ve.ce.AnnotationFactory.js"></script>
                <script src="../../src/ce/ve.ce.NodeFactory.js"></script>
                <script src="../../src/ce/ve.ce.Document.js"></script>
diff --git a/demos/ve/mobile.html b/demos/ve/mobile.html
index ed878ed..6997b4f 100644
--- a/demos/ve/mobile.html
+++ b/demos/ve/mobile.html
@@ -223,7 +223,7 @@
                <script 
src="../../src/dm/metaitems/ve.dm.CommentMetaItem.js"></script>
                <script src="../../src/dm/nodes/ve.dm.CommentNode.js"></script>
                <script src="../../src/ce/ve.ce.js"></script>
-               <script src="../../src/ce/ve.ce.DomRange.js"></script>
+               <script src="../../src/ce/ve.ce.RangeState.js"></script>
                <script src="../../src/ce/ve.ce.AnnotationFactory.js"></script>
                <script src="../../src/ce/ve.ce.NodeFactory.js"></script>
                <script src="../../src/ce/ve.ce.Document.js"></script>
diff --git a/src/ce/ve.ce.DomRange.js b/src/ce/ve.ce.DomRange.js
deleted file mode 100644
index 7f818c8..0000000
--- a/src/ce/ve.ce.DomRange.js
+++ /dev/null
@@ -1,69 +0,0 @@
-/*!
- * VisualEditor DomRange class.
- *
- * @copyright 2011-2014 VisualEditor Team and others; see 
http://ve.mit-license.org
- */
-
-/**
- * DOM range
- *
- * @class
- * @constructor
- * @param {HTMLElement} focusNode Selection focus node
- * @param {number} focusOffset Selection focus offset
- * @param {HTMLElement} anchorNode Selection anchor node
- * @param {number} anchorOffset Selection anchor offset
- */
-ve.ce.DomRange = function VeCeDomRange( focusNode, focusOffset, anchorNode, 
anchorOffset ) {
-       this.focusNode = focusNode;
-       this.focusOffset = focusOffset;
-       this.anchorNode = anchorNode;
-       this.anchorOffset = anchorOffset;
-};
-
-/* Static Methods */
-
-/**
- * Create a new DOM range from a document's native selection
- *
- * @param {HTMLDocument} doc Document to get selection from
- * @return {ve.ce.DomRange} DOM range
- */
-ve.ce.DomRange.newFromDocument = function ( doc ) {
-       var selection = doc.getSelection();
-       return new ve.ce.DomRange(
-               selection.focusNode, selection.focusOffset, 
selection.anchorNode, selection.anchorOffset
-       );
-};
-
-/* Methods */
-
-/**
- * Check if a DOM range is equal to another DOM range
- *
- * @param {ve.ce.DomRange} other DOM range to compare to
- * @return {boolean} The other DOM range is equal to this one
- */
-ve.ce.DomRange.prototype.equals = function ( other ) {
-       return other &&
-               this.focusNode === other.focusNode &&
-               this.focusOffset === other.focusOffset &&
-               this.anchorNode === other.anchorNode &&
-               this.anchorOffset === other.anchorOffset;
-};
-
-/**
- * Get a linear model ve.Range for the DOM range
- *
- * @returns {ve.Range|null} Linear model range, or null if out of bounds
- */
-ve.ce.DomRange.prototype.getRange = function () {
-       try {
-               return new ve.Range(
-                       ve.ce.getOffset( this.anchorNode, this.anchorOffset ),
-                       ve.ce.getOffset( this.focusNode, this.focusOffset )
-               );
-       } catch ( e ) {
-               return null;
-       }
-};
diff --git a/src/ce/ve.ce.RangeState.js b/src/ce/ve.ce.RangeState.js
new file mode 100644
index 0000000..a719214
--- /dev/null
+++ b/src/ce/ve.ce.RangeState.js
@@ -0,0 +1,214 @@
+/*!
+ * VisualEditor Content Editable Range State class
+ *
+ * @copyright 2011-2014 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
+/**
+ * ContentEditable range state (a snapshot of CE selection/content state)
+ *
+ * @class
+ *
+ * @constructor
+ * @param {ve.ce.RangeState|null} old Previous range state
+ * @param {ve.ce.DocumentNode} docNode The current document node
+ * @param {boolean} selectionOnly The caller promises the content has not 
changed from old
+ */
+ve.ce.RangeState = function VeCeRangeState( old, docNode, selectionOnly ) {
+       /**
+        * @property {boolean} branchNodeChanged Whether the CE branch node 
changed
+        */
+       this.branchNodeChanged = null;
+
+       /**
+        * @property {boolean} selectionChanged Whether the DOM range changed
+        */
+       this.selectionChanged = null;
+
+       /**
+        * @property {boolean} contentChanged Whether the content changed
+        */
+       this.contentChanged = null;
+
+       /**
+        * @property {boolean} leftBlockSlug Whether the range left a block slug
+        */
+       this.leftBlockSlug = null;
+
+       /**
+        * @property {boolean} enteredBlockSlug Whether the range entered a 
block slug
+        */
+       this.enteredBlockSlug = null;
+
+       /**
+        * @property {ve.Range|null} veRange The current selection range
+        */
+       this.veRange = null;
+
+       /**
+        * @property {ve.ce.BranchNode|null} node The current branch node
+        */
+       this.node = null;
+
+       /**
+        * @property {jQuery|null} $slugWrapper The current slug wrapper
+        */
+       this.$slugWrapper = null;
+
+       /**
+        * @property {string} text Plain text of current branch node
+        */
+       this.text = null;
+
+       /**
+        * @property {string} DOM Hash of current branch node
+        */
+       this.hash = null;
+
+       this.saveState( old, docNode, selectionOnly );
+};
+
+/* Inheritance */
+
+OO.initClass( ve.ce.RangeState );
+
+/* Methods */
+
+/**
+ * Saves a snapshot of the current range state
+ * @method
+ * @param {ve.ce.RangeState|null} old Previous range state
+ * @param {ve.ce.DocumentNode} docNode The current document node
+ * @param {boolean} selectionOnly The caller promises the content has not 
changed from old
+ */
+ve.ce.RangeState.prototype.saveState = function ( old, docNode, selectionOnly 
) {
+       var $nodeOrSlug, selection, anchorNodeChanged;
+
+       // Freeze selection out of live object.
+       selection = ( function ( liveSelection ) {
+               return {
+                       focusNode: liveSelection.focusNode,
+                       focusOffset: liveSelection.focusOffset,
+                       anchorNode: liveSelection.anchorNode,
+                       anchorOffset: liveSelection.anchorOffset
+               };
+       } ( docNode.getElementDocument().getSelection() ) );
+
+       // Get new range information
+       if ( old && !old.compareSelection( selection ) ) {
+               // No change; use old values for speed
+               this.selectionChanged = false;
+               this.veRange = old.veRange;
+               this.$slugWrapper = old.$slugWrapper;
+               this.leftBlockSlug = false;
+               this.enteredBlockSlug = false;
+       } else {
+               this.selectionChanged = true;
+               try {
+                       this.veRange = new ve.Range(
+                               ve.ce.getOffset( selection.anchorNode, 
selection.anchorOffset ),
+                               ve.ce.getOffset( selection.focusNode, 
selection.focusOffset )
+                       );
+               } catch ( e ) {
+                       this.veRange = null;
+               }
+       }
+
+       anchorNodeChanged = !old || old.compareAnchorNode( selection );
+
+       if ( !anchorNodeChanged ) {
+               this.node = old.node;
+               this.$slugWrapper = old.$slugWrapper;
+       } else {
+               $nodeOrSlug = $( selection.anchorNode ).closest(
+                       '.ve-ce-branchNode, .ve-ce-branchNode-blockSlugWrapper'
+               );
+               if ( $nodeOrSlug.length === 0 ) {
+                       this.node = null;
+                       this.$slugWrapper = null;
+               } else if ( $nodeOrSlug.hasClass( 
've-ce-branchNode-blockSlugWrapper' ) ) {
+                       this.node = null;
+                       this.$slugWrapper = $nodeOrSlug;
+               } else {
+                       this.node = $nodeOrSlug.data( 'view' );
+                       this.$slugWrapper = null;
+                       // Check this node belongs to our document
+                       if ( this.node && this.node.root !== docNode ) {
+                               this.node = null;
+                               this.veRange = null;
+                       }
+               }
+       }
+
+       this.branchNodeChanged = ( !old || this.node !== old.node );
+
+       // Compute text/hash, for change comparison
+       if ( selectionOnly && !anchorNodeChanged ) {
+               this.text = old.text;
+               this.hash = old.hash;
+       } else if ( this.node === null ) {
+               this.text = null;
+               this.hash = null;
+       } else {
+               this.text = ve.ce.getDomText( this.node.$element[0] );
+               this.hash = ve.ce.getDomHash( this.node.$element[0] );
+       }
+
+       this.leftBlockSlug = (
+               old &&
+               old.$slugWrapper &&
+               !old.$slugWrapper.is( this.$slugWrapper )
+       );
+       this.enteredBlockSlug = (
+               old &&
+               this.$slugWrapper &&
+               this.$slugWrapper.length > 0 &&
+               !this.$slugWrapper.is( old.$slugWrapper )
+       );
+
+       // Only set contentChanged if we're still in the same branch node/block 
slug
+       this.contentChanged = (
+               !selectionOnly &&
+               old &&
+               old.node === this.node && (
+                       old.hash === null ||
+                       old.text === null ||
+                       old.hash !== this.hash ||
+                       old.text !== this.text
+               )
+       );
+
+       // Save selection for future comparisons. (But it is not properly 
frozen, because the nodes
+       // are live and mutable, and therefore the offsets may come to point to 
places that are
+       // misleadingly different from when the selection was saved).
+       this.misleadingSelection = selection;
+};
+
+/**
+ * Compare a selection object for changes from the snapshotted state.
+ *
+ * The meaning of "changes" is slightly misleading, because the offsets were 
taken
+ * at two different instants, between which content outside of the selection 
may
+ * have changed. This can in theory cause false negatives (unnoticed changes).
+ *
+ * @param {Object} selection Selection to compare
+ * @returns {boolean} Whether there is a change
+ */
+ve.ce.RangeState.prototype.compareSelection = function ( selection ) {
+       return (
+               this.misleadingSelection.focusNode !== selection.focusNode ||
+               this.misleadingSelection.focusOffset !== selection.focusOffset 
||
+               this.misleadingSelection.anchorNode !== selection.anchorNode ||
+               this.misleadingSelection.anchorOffset !== selection.anchorOffset
+       );
+};
+
+/**
+ * Compare a selection object for a change of anchor node from the snapshotted 
state.
+ *
+ * @param {Object} selection Selection to compare
+ * @returns {boolean} Whether the anchor node has changed
+ */
+ve.ce.RangeState.prototype.compareAnchorNode = function ( selection ) {
+       return this.misleadingSelection.anchorNode !== selection.anchorNode;
+};
diff --git a/src/ce/ve.ce.SurfaceObserver.js b/src/ce/ve.ce.SurfaceObserver.js
index ad1564a..95b4da7 100644
--- a/src/ce/ve.ce.SurfaceObserver.js
+++ b/src/ce/ve.ce.SurfaceObserver.js
@@ -25,9 +25,7 @@
        this.disabled = false;
        this.timeoutId = null;
        this.pollInterval = 250; // ms
-
-       // Initialization
-       this.clear();
+       this.rangeState = null;
 };
 
 /* Inheritance */
@@ -74,15 +72,9 @@
  * Clear polling data.
  *
  * @method
- * @param {ve.Range} range Initial range to use
  */
-ve.ce.SurfaceObserver.prototype.clear = function ( range ) {
-       this.domRange = null;
-       this.range = range || null;
-       this.node = null;
-       this.text = null;
-       this.hash = null;
-       this.$slugWrapper = null;
+ve.ce.SurfaceObserver.prototype.clear = function () {
+       this.rangeState = null;
 };
 
 /**
@@ -209,113 +201,61 @@
  * @fires slugEnter
  */
 ve.ce.SurfaceObserver.prototype.pollOnceInternal = function ( emitChanges, 
selectionOnly ) {
-       var $nodeOrSlug, node, text, hash, range, domRange, $slugWrapper,
-               anchorNodeChange = false,
-               enteredSlug = false,
-               leftSlug = false,
+       var oldState, newState,
                observer = this;
 
        if ( !this.domDocument || this.disabled ) {
                return;
        }
 
-       range = this.range;
-       node = this.node;
-       domRange = ve.ce.DomRange.newFromDocument( this.domDocument );
+       oldState = this.rangeState;
+       newState = new ve.ce.RangeState(
+               oldState,
+               this.documentView.getDocumentNode(),
+               selectionOnly
+       );
 
-       if ( !domRange.equals( this.domRange ) ) {
-               if ( !this.domRange || this.domRange.anchorNode !== 
domRange.anchorNode ) {
-                       anchorNodeChange = true;
-               }
-               range = domRange.getRange();
-               this.domRange = domRange;
+       if ( newState.leftBlockSlug ) {
+               oldState.$slugWrapper
+                       .addClass( 
've-ce-branchNode-blockSlugWrapper-unfocused' )
+                       .removeClass( 
've-ce-branceNode-blockSlugWrapper-focused' );
        }
 
-       if ( anchorNodeChange ) {
-               node = null;
-               $nodeOrSlug = $( domRange.anchorNode ).closest( 
'.ve-ce-branchNode, .ve-ce-branchNode-blockSlugWrapper' );
-               if ( $nodeOrSlug.length ) {
-                       if ( $nodeOrSlug.hasClass( 
've-ce-branchNode-blockSlugWrapper' ) ) {
-                               $slugWrapper = $nodeOrSlug;
-                       } else {
-                               node = $nodeOrSlug.data( 'view' );
-                               // Check this node belongs to our document
-                               if ( node && node.root !== 
this.documentView.getDocumentNode() ) {
-                                       node = null;
-                                       range = null;
-                               }
+       if ( newState.enteredBlockSlug ) {
+               newState.$slugWrapper
+                       .addClass( 've-ce-branchNode-blockSlugWrapper-focused' )
+                       .removeClass( 
've-ce-branchNode-blockSlugWrapper-unfocused' );
+       }
+
+       this.rangeState = newState;
+
+       if ( newState.enteredBlockSlug || newState.leftBlockSlug ) {
+               // Emit 'position' on the surface view after the animation 
completes
+               this.setTimeout( function () {
+                       if ( observer.surface ) {
+                               observer.surface.emit( 'position' );
                        }
-               }
-
-               if ( this.$slugWrapper && !this.$slugWrapper.is( $slugWrapper ) 
) {
-                       this.$slugWrapper
-                               .addClass( 
've-ce-branchNode-blockSlugWrapper-unfocused' )
-                               .removeClass( 
've-ce-branchNode-blockSlugWrapper-focused' );
-                       this.$slugWrapper = null;
-                       leftSlug = true;
-               }
-
-               if ( $slugWrapper && $slugWrapper.length && !$slugWrapper.is( 
this.$slugWrapper ) ) {
-                       this.$slugWrapper = $slugWrapper
-                               .addClass( 
've-ce-branchNode-blockSlugWrapper-focused' )
-                               .removeClass( 
've-ce-branchNode-blockSlugWrapper-unfocused' );
-                       enteredSlug = true;
-               }
-
-               if ( enteredSlug || leftSlug ) {
-                       // Emit 'position' on the surface view after the 
animation completes
-                       this.setTimeout( function () {
-                               if ( observer.surface ) {
-                                       observer.surface.emit( 'position' );
-                               }
-                       }, 200 );
-               }
+               }, 200 );
        }
 
-       if ( this.node !== node ) {
-               if ( node === null ) {
-                       this.text = null;
-                       this.hash = null;
-                       this.node = null;
-               } else {
-                       this.text = ve.ce.getDomText( node.$element[0] );
-                       this.hash = ve.ce.getDomHash( node.$element[0] );
-                       this.node = node;
-               }
-       } else if ( !selectionOnly && node !== null ) {
-               text = ve.ce.getDomText( node.$element[0] );
-               hash = ve.ce.getDomHash( node.$element[0] );
-               if ( this.text !== text || this.hash !== hash ) {
-                       if ( emitChanges ) {
-                               this.emit(
-                                       'contentChange',
-                                       node,
-                                       {
-                                               text: this.text,
-                                               hash: this.hash,
-                                               range: this.range
-                                       },
-                                       { text: text, hash: hash, range: range }
-                               );
-                       }
-                       this.text = text;
-                       this.hash = hash;
-               }
+       if ( !selectionOnly && newState.node !== null && 
newState.contentChanged && emitChanges ) {
+               this.emit(
+                       'contentChange',
+                       newState.node,
+                       { text: oldState.text, hash: oldState.hash, range: 
oldState.veRange },
+                       { text: newState.text, hash: newState.hash, range: 
newState.veRange }
+               );
        }
 
-       // Only emit rangeChange event if there's a meaningful range difference
-       if ( ( this.range && range ) ? !this.range.equals( range ) : ( 
this.range !== range ) ) {
-               if ( emitChanges ) {
-                       this.emit(
-                               'rangeChange',
-                               this.range,
-                               range
-                       );
-               }
-               this.range = range;
+       if ( newState.selectionChanged && emitChanges ) {
+               this.emit(
+                       'rangeChange',
+                       ( oldState ? oldState.veRange : null ),
+                       newState.veRange
+               );
        }
 
-       if ( emitChanges && enteredSlug ) {
+       if ( newState.enteredBlockSlug && emitChanges ) {
                this.emit( 'slugEnter' );
        }
 };
@@ -338,5 +278,8 @@
  * @return {ve.Range} Range
  */
 ve.ce.SurfaceObserver.prototype.getRange = function () {
-       return this.range;
+       if ( !this.rangeState ) {
+               return null;
+       }
+       return this.rangeState.veRange;
 };
diff --git a/tests/index.html b/tests/index.html
index 4778413..393d97e 100644
--- a/tests/index.html
+++ b/tests/index.html
@@ -173,7 +173,7 @@
                <script 
src="../src/dm/metaitems/ve.dm.CommentMetaItem.js"></script>
                <script src="../src/dm/nodes/ve.dm.CommentNode.js"></script>
                <script src="../src/ce/ve.ce.js"></script>
-               <script src="../src/ce/ve.ce.DomRange.js"></script>
+               <script src="../src/ce/ve.ce.RangeState.js"></script>
                <script src="../src/ce/ve.ce.AnnotationFactory.js"></script>
                <script src="../src/ce/ve.ce.NodeFactory.js"></script>
                <script src="../src/ce/ve.ce.Document.js"></script>

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

Gerrit-MessageType: merged
Gerrit-Change-Id: Iccb6d39022ee06c8bd1fb687343efa160dbac44f
Gerrit-PatchSet: 19
Gerrit-Project: VisualEditor/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Divec <[email protected]>
Gerrit-Reviewer: Catrope <[email protected]>
Gerrit-Reviewer: Divec <[email protected]>
Gerrit-Reviewer: Esanders <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to