Divec has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/82858


Change subject: WIP: Refactor CE Surface to reduce event feedback
......................................................................

WIP: Refactor CE Surface to reduce event feedback

Restructure SurfaceObserver methods so that the method calls are more precise.
Sequence all key*/mouse*/composition* events using EventSequencer.

modules/ve/ce/ve.ce.SurfaceObserver.js
* Move timing code into startTimerLoop / stopTimerLoop / timerLoop
* Split sync method calls into syncBothWays / syncToModel
* Move postponed sync logic into syncBothWaysAfter / syncToModelAfter

modules/ve/ce/ve.ce.Surface.js
* Use EventSequencer for keyboard/mouse events
* Change all surfaceObserver calls to use new API

modules/ve/ui/ve.ui.Surface.js
* Change all surfaceObserver calls to use new API

modules/ve/ve.EventSequencer.js
* Implement clearListeners

Change-Id: I0085e4a53c5a776733dce6944b867b8d2228ba4b
---
M modules/ve/ce/ve.ce.Surface.js
M modules/ve/ce/ve.ce.SurfaceObserver.js
M modules/ve/ui/ve.ui.Surface.js
M modules/ve/ve.EventSequencer.js
4 files changed, 140 insertions(+), 139 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/VisualEditor 
refs/changes/58/82858/1

diff --git a/modules/ve/ce/ve.ce.Surface.js b/modules/ve/ce/ve.ce.Surface.js
index 327ff09..fa85aa6 100644
--- a/modules/ve/ce/ve.ce.Surface.js
+++ b/modules/ve/ce/ve.ce.Surface.js
@@ -35,6 +35,12 @@
        this.selectionTimeout = null;
        this.keyPressTimeout = null;
        this.$document = $( this.getElementDocument() );
+       this.eventSequencer = new ve.EventSequencer(
+               this.$document,
+               ['keydown', 'keypress', 'keyup', 'mousedown', 'mouseup', 
'mousemove',
+                       'compositionstart', 'compositionend'],
+               ve.bind( ve.log, ve )
+       );
        this.clipboard = {};
        this.renderLocks = 0;
        this.dragging = false;
@@ -285,18 +291,27 @@
  * @param {jQuery.Event} e Focus event
  */
 ve.ce.Surface.prototype.documentOnFocus = function () {
-       this.$document.off( '.ve-ce-Surface' );
-       this.$document.on( {
-               'keydown.ve-ce-Surface': ve.bind( this.onDocumentKeyDown, this 
),
-               'keyup.ve-ce-Surface': ve.bind( this.onDocumentKeyUp, this ),
-               'keypress.ve-ce-Surface': ve.bind( this.onDocumentKeyPress, 
this ),
-               'mousedown.ve-ce-Surface': ve.bind( this.onDocumentMouseDown, 
this ),
-               'mouseup.ve-ce-Surface': ve.bind( this.onDocumentMouseUp, this 
),
-               'mousemove.ve-ce-Surface': ve.bind( this.onDocumentMouseMove, 
this ),
-               'compositionstart.ve-ce-Surface': ve.bind( 
this.onDocumentCompositionStart, this ),
-               'compositionend.ve-ce-Surface': ve.bind( 
this.onDocumentCompositionEnd, this )
-       } );
-       this.surfaceObserver.start( true, true );
+       var eventName, preListeners = {
+               'keydown': ve.bind( this.onDocumentKeyDown, this ),
+               'keyup': ve.bind( this.onDocumentKeyUp, this ),
+               'keypress': ve.bind( this.onDocumentKeyPress, this ),
+               'mousedown': ve.bind( this.onDocumentMouseDown, this ),
+               'mouseup': ve.bind( this.onDocumentMouseUp, this ),
+               'mousemove': ve.bind( this.onDocumentMouseMove, this ),
+               'compositionstart': ve.bind( this.onDocumentCompositionStart, 
this ),
+               'compositionend': ve.bind( this.onDocumentCompositionEnd, this )
+       }, postListeners = {
+               'keypress': ve.bind( this.onDocumentPostKeyPress, this )
+       };
+
+       this.eventSequencer.clearListeners();
+       for ( eventName in preListeners ) {
+               this.eventSequencer.addPreListener( eventName, 
preListeners[eventName] );
+       }
+       for ( eventName in postListeners ) {
+               this.eventSequencer.addPostListener( eventName, 
postListeners[eventName] );
+       }
+       this.surfaceObserver.startTimerLoop();
 };
 
 /**
@@ -306,8 +321,9 @@
  * @param {jQuery.Event} e Element blur event
  */
 ve.ce.Surface.prototype.documentOnBlur = function () {
-       this.$document.off( '.ve-ce-Surface' );
-       this.surfaceObserver.stop( true, true );
+       this.eventSequencer.clearListeners();
+       this.surfaceObserver.stopTimerLoop();
+       this.surfaceObserver.pollOnce();
        this.dragging = false;
 };
 
@@ -327,7 +343,8 @@
        // this.$$( e.target ).closest( '.ve-ce-documentNode' ).length === 0
 
        if ( e.which === 1 ) {
-               this.surfaceObserver.stop( true, true );
+               this.surfaceObserver.stopTimerLoop();
+               this.surfaceObserver.pollOnce(); // TODO: guard with 
incRenderLock?
        }
 
        // Handle triple click
@@ -353,7 +370,8 @@
  * @emits selectionEnd
  */
 ve.ce.Surface.prototype.onDocumentMouseUp = function ( e ) {
-       this.surfaceObserver.start( false, true );
+       this.surfaceObserver.startTimerLoop();
+       this.surfaceObserver.pollOnce(); // TODO: guard with incRenderLock?
        if ( !e.shiftKey && this.selecting ) {
                this.emit( 'selectionEnd' );
                this.selecting = false;
@@ -454,7 +472,6 @@
  */
 ve.ce.Surface.prototype.onDocumentKeyDown = function ( e ) {
        var trigger;
-       this.forceKeyPressTimeout();
 
        // Ignore keydowns while in IME mode but do not preventDefault them (so 
text actually appear on
        // the screen).
@@ -472,7 +489,13 @@
                return;
        }
 
-       this.surfaceObserver.stop( true, false );
+       this.surfaceObserver.stopTimerLoop();
+       this.incRenderLock();
+       try {
+               this.surfaceObserver.pollOnce(); // TODO: is this correct?
+       } finally {
+               this.decRenderLock();
+       }
        switch ( e.keyCode ) {
                case ve.Keys.LEFT:
                case ve.Keys.RIGHT:
@@ -507,7 +530,13 @@
                        }
                        break;
        }
-       this.surfaceObserver.start( false, false );
+       this.incRenderLock();
+       try {
+               this.surfaceObserver.pollOnce();
+       } finally {
+               this.decRenderLock();
+       }
+       this.surfaceObserver.startTimerLoop();
 };
 
 /**
@@ -518,8 +547,6 @@
  */
 ve.ce.Surface.prototype.onDocumentKeyPress = function ( e ) {
        var selection, prevNode, documentModel = this.model.getDocument();
-
-       this.forceKeyPressTimeout();
 
        // Prevent IE from editing Aliens/Entities
        // TODO: Better comment about what's going on here is needed.
@@ -546,39 +573,10 @@
        }
 
        this.handleInsertion();
-       this.setKeyPressTimeout();
 };
 
-/**
- * Append a call to onKeyPressTimeout to the event queue.
- * @method
- */
-ve.ce.Surface.prototype.setKeyPressTimeout = function () {
-       this.keyPressTimeout = setTimeout( ve.bind( function() {
-               this.keyPressTimeout = null;
-               this.onKeyPressTimeout();
-       }, this ) );
-};
-
-/**
- * If there is a pending call to onKeyPressTimeout in the event queue, delete 
it and call now
- * @method
- */
-ve.ce.Surface.prototype.forceKeyPressTimeout = function () {
-       if ( this.keyPressTimeout === null ) {
-               return;
-       }
-       clearTimeout( this.keyPressTimeout );
-       this.keyPressTimeout = null;
-       this.onKeyPressTimeout();
-};
-
-/**
- * post-keypress handler: re-sync the surface and model
- * @method
- */
-ve.ce.Surface.prototype.onKeyPressTimeout = function () {
-       this.surfaceObserver.start( false, true );
+ve.ce.Surface.prototype.onDocumentPostKeyPress = function () {
+       this.surfaceObserver.pollOnce();
 };
 
 /**
@@ -603,7 +601,8 @@
  * @param {jQuery.Event} e Cut event
  */
 ve.ce.Surface.prototype.onCut = function ( e ) {
-       this.surfaceObserver.stop( false, true );
+       // TODO: is it correct that we don't need to pollOnce here?
+       this.surfaceObserver.stopTimerLoop();
        this.onCopy( e );
        setTimeout( ve.bind( function () {
                var selection, tx;
@@ -620,7 +619,8 @@
 
                this.model.change( tx, new ve.Range( selection.start ) );
                this.surfaceObserver.clear();
-               this.surfaceObserver.start( false, true );
+               this.surfaceObserver.startTimerLoop();
+               this.surfaceObserver.pollOnce();
        }, this ) );
 };
 
@@ -680,7 +680,8 @@
                eventPasteKey = clipboardData && clipboardData.getData( 
'text/xcustom' ),
                eventPasteText = clipboardData && clipboardData.getData( 
'text/plain' );
 
-       this.surfaceObserver.stop( false, true );
+       // TODO: is it correct that we don't need to pollOnce here?
+       this.surfaceObserver.stopTimerLoop();
 
        // Pasting into a range? Remove first.
        if ( !rangy.getSelection( this.$document[0] ).isCollapsed ) {
@@ -784,7 +785,13 @@
  */
 ve.ce.Surface.prototype.onDocumentCompositionEnd = function () {
        this.inIme = false;
-       this.surfaceObserver.start( false, false );
+       this.incRenderLock();
+       try {
+               this.surfaceObserver.pollOnce();
+       } finally {
+               this.decRenderLock();
+       }
+       this.surfaceObserver.startTimerLoop();
 };
 
 /*! Custom Events */
@@ -836,7 +843,7 @@
 /**
  * Handle selection change events.
  *
- * @see ve.ce.SurfaceObserver#poll
+ * @see ve.ce.SurfaceObserver#pollOnce
  *
  * @method
  * @param {ve.Range} oldRange
@@ -847,18 +854,13 @@
                // Ignore when the newRange is just a flipped oldRange
                return;
        }
-       this.incRenderLock();
-       try {
-               this.model.change( null, newRange );
-       } finally {
-               this.decRenderLock();
-       }
+       this.model.change( null, newRange );
 };
 
 /**
  * Handle content change events.
  *
- * @see ve.ce.SurfaceObserver#poll
+ * @see ve.ce.SurfaceObserver#pollOnce
  *
  * @method
  * @param {HTMLElement} node DOM node the change occured in
@@ -1031,7 +1033,7 @@
  * @method
  */
 ve.ce.Surface.prototype.onLock = function () {
-       this.surfaceObserver.stop( false, true );
+       this.surfaceObserver.locked = true;
 };
 
 /**
@@ -1040,8 +1042,8 @@
  * @method
  */
 ve.ce.Surface.prototype.onUnlock = function () {
-       this.surfaceObserver.clear( this.model.getSelection() );
-       this.surfaceObserver.start( false, true );
+       this.surfaceObserver.locked = false;
+       // TODO: Do we need to pollOnce?
 };
 
 /*! Relocation */
@@ -1088,8 +1090,13 @@
 
        // Selection is going to be displayed programmatically so prevent 
default browser behaviour
        e.preventDefault();
-       // Stop with final poll cycle so we have correct information in model
-       this.surfaceObserver.stop( true, false );
+       this.surfaceObserver.stopTimerLoop(); // TODO: onDocumentKeyDown did 
this already
+       this.incRenderLock();
+       try {
+               this.surfaceObserver.pollOnce(); // TODO: onDocumentKeyDown did 
this already
+       } finally {
+               this.decRenderLock();
+       }
        selection = this.model.getSelection();
        if ( this.$$( e.target ).css( 'direction' ) === 'rtl' ) {
                // If the language direction is RTL, switch left/right 
directions:
@@ -1105,7 +1112,8 @@
                e.shiftKey
        );
        this.model.change( null, range );
-       this.surfaceObserver.start( false, true );
+       this.surfaceObserver.startTimerLoop(); // TODO: onDocumentKeyDown does 
this anyway
+       this.surfaceObserver.pollOnce();
 };
 
 /**
@@ -1122,7 +1130,8 @@
                nativeSel.modify( 'extend', 'left', 'character' );
                return;
        }
-       this.surfaceObserver.stop( true, true );
+       this.surfaceObserver.stopTimerLoop(); // TODO: onDocumentKeyDown did 
this already
+       this.surfaceObserver.pollOnce(); // TODO: onDocumentKeyDown did this 
already
        selection = this.model.getSelection();
        rangySelection = rangy.getSelection( this.$document[0] );
        // Perform programatic handling only for selection that is expanded and 
backwards according to
@@ -1146,18 +1155,18 @@
                        if ( !$element.hasClass( 've-ce-branchNode-slug' ) ) {
                                $element.remove();
                        }
-                       this.surfaceObserver.start( false, true );
-                       this.surfaceObserver.stop( false, true );
+                       this.surfaceObserver.pollOnce();
                        if ( e.shiftKey === true ) { // expanded range
                                range = new ve.Range( selection.from, 
this.model.getSelection().to );
                        } else { // collapsed range (just a cursor)
                                range = new ve.Range( 
this.model.getSelection().to );
                        }
                        this.model.change( null, range );
-                       this.surfaceObserver.start( false, true );
+                       this.surfaceObserver.pollOnce();
                }, this ), 0 );
        } else {
-               this.surfaceObserver.start( false, true );
+               this.surfaceObserver.startTimerLoop(); // TODO: 
onDocumentKeyDown does this anyway
+               this.surfaceObserver.pollOnce();
        }
 };
 
@@ -1213,7 +1222,8 @@
                }
        }
 
-       this.surfaceObserver.stop( true, true );
+       this.surfaceObserver.stopTimerLoop();
+       this.surfaceObserver.pollOnce();
 };
 
 /**
diff --git a/modules/ve/ce/ve.ce.SurfaceObserver.js 
b/modules/ve/ce/ve.ce.SurfaceObserver.js
index e49f406..edca74b 100644
--- a/modules/ve/ce/ve.ce.SurfaceObserver.js
+++ b/modules/ve/ce/ve.ce.SurfaceObserver.js
@@ -24,8 +24,9 @@
        this.documentView = documentView;
        this.domDocument = null;
        this.polling = false;
+       this.locked = false;
        this.timeoutId = null;
-       this.frequency = 250; //ms
+       this.frequency = 250; // ms
 
        // Initialization
        this.clear();
@@ -51,6 +52,7 @@
  * @param {Object} next.text New plain text content
  * @param {Object} next.hash New DOM hash
  * @param {ve.Range} next.range New selection
+ * @param {boolean} fromSurface Whether the change originates in the Surface
  */
 
 /**
@@ -61,6 +63,7 @@
  * @event selectionChange
  * @param {ve.Range} oldRange
  * @param {ve.Range} newRange
+ * @param {boolean} fromSurface Whether the change originates in the Surface
  */
 
 /* Methods */
@@ -80,35 +83,41 @@
 };
 
 /**
- * Start polling.
- *
- * If {postpone} is false or undefined the first poll cycle will occur 
immediately and synchronously.
+ * Start the setTimeout synchronisation loop
  *
  * @method
- * @param {boolean} postpone Add the first poll to the end of the event queue
- * @param {boolean} emitContentChanges Allow contentChange to be emitted
  */
-ve.ce.SurfaceObserver.prototype.start = function ( postpone, 
emitContentChanges ) {
+ve.ce.SurfaceObserver.prototype.startTimerLoop = function () {
        this.domDocument = 
this.documentView.getDocumentNode().getElementDocument();
        this.polling = true;
-       this.poll( postpone, emitContentChanges );
+       this.timerLoop( true ); // will not sync immediately, because timeoutId 
should be null
 };
 
 /**
- * Stop polling.
- *
- * If {poll} is false or undefined than no final poll cycle will occur and 
changes can be lost. If
- * it's true then a final poll cycle will occur immediately and synchronously.
+ * Loop once with `setTimeout`
+ * @method
+ * @param {boolean} firstTime Wait before polling
+ */
+ve.ce.SurfaceObserver.prototype.timerLoop = function ( firstTime ) {
+       if ( this.timeoutId ) {
+               // in case we're not running from setTimeout
+               clearTimeout( this.timeoutId );
+               this.timeoutId = null;
+       }
+       if ( ! firstTime && ! this.locked ) {
+               this.pollOnce();
+       }
+       // only reach this point if syncBothWays does not throw an exception
+       this.timeoutId = setTimeout( ve.bind( this.timerLoop, this ), 
this.frequency );
+};
+
+/**
+ * Stop polling
  *
  * @method
- * @param {boolean} poll Poll one last time before stopping future polling
- * @param {boolean} emitContentChanges Allow contentChange to be emitted
  */
-ve.ce.SurfaceObserver.prototype.stop = function ( poll, emitContentChanges ) {
+ve.ce.SurfaceObserver.prototype.stopTimerLoop = function () {
        if ( this.polling === true ) {
-               if ( poll === true ) {
-                       this.poll( false, emitContentChanges );
-               }
                this.polling = false;
                clearTimeout( this.timeoutId );
                this.timeoutId = null;
@@ -122,36 +131,15 @@
  *
  * TODO: fixing selection in certain cases, handling selection across multiple 
nodes in Firefox
  *
- * FIXME: Does not work well (selectionChange is not emited) when cursor is 
placed inside a slug
+ * FIXME: Does not work well (selectionChange is not emitted) when cursor is 
placed inside a slug
  * with a mouse.
  *
  * @method
- * @param {boolean} postpone Append the poll action to the end of the event 
queue
- * @param {boolean} emitContentChanges Allow contentChange to be emitted
  * @emits contentChange
  * @emits selectionChange
  */
-ve.ce.SurfaceObserver.prototype.poll = function ( postpone, emitContentChanges 
) {
+ve.ce.SurfaceObserver.prototype.pollOnce = function () {
        var $nodeOrSlug, node, text, hash, range, rangyRange;
-
-       if ( this.polling === false ) {
-               return;
-       }
-
-       if ( this.timeoutId !== null ) {
-               clearTimeout( this.timeoutId );
-               this.timeoutId = null;
-       }
-
-       if ( postpone === true ) {
-               this.timeoutId = setTimeout(
-                       ve.bind( this.poll, this ),
-                       0,
-                       false,
-                       emitContentChanges
-               );
-               return;
-       }
 
        range = this.range;
        node = this.node;
@@ -183,14 +171,12 @@
                text = ve.ce.getDomText( node.$[0] );
                hash = ve.ce.getDomHash( node.$[0] );
                if ( this.text !== text || this.hash !== hash ) {
-                       if ( emitContentChanges ) {
-                               this.emit(
-                                       'contentChange',
-                                       node,
-                                       { 'text': this.text, 'hash': this.hash, 
'range': this.range },
-                                       { 'text': text, 'hash': hash, 'range': 
range }
-                               );
-                       }
+                       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;
                }
@@ -205,11 +191,4 @@
                );
                this.range = range;
        }
-
-       this.timeoutId = setTimeout(
-               ve.bind( this.poll, this ),
-               this.frequency,
-               false,
-               emitContentChanges
-       );
 };
diff --git a/modules/ve/ui/ve.ui.Surface.js b/modules/ve/ui/ve.ui.Surface.js
index 1510eff..a8349cb 100644
--- a/modules/ve/ui/ve.ui.Surface.js
+++ b/modules/ve/ui/ve.ui.Surface.js
@@ -82,7 +82,7 @@
        // By re-asserting the current selection and forcing a poll we force 
selection to be something
        // reasonable - otherwise in Firefox, the initial selection is (0,0), 
causing bug 42277
        this.model.getFragment().select();
-       this.view.surfaceObserver.poll( false, true );
+       this.view.surfaceObserver.pollOnce();
        this.model.startHistoryTracking();
 };
 
diff --git a/modules/ve/ve.EventSequencer.js b/modules/ve/ve.EventSequencer.js
index fdb4bc3..83c910b 100644
--- a/modules/ve/ve.EventSequencer.js
+++ b/modules/ve/ve.EventSequencer.js
@@ -25,11 +25,10 @@
  * @param {Function} [boundLogFunc] Logging function, pre-bound with ve.bind
  */
 ve.EventSequencer = function ( node, eventNames, boundLogFunc ) {
-       var i, len, eventName, $node = $( node );
        this.node = node;
-       this.preListenersForEvent = {};
-       this.postListenersForEvent = {};
+       this.$node = $( node );
        this.log = boundLogFunc || function () {};
+       this.eventNames = eventNames;
 
        /**
         * @property {Object[]}
@@ -39,9 +38,22 @@
          *  - eventName {string} Name, such as keydown
          */
        this.pendingCalls = [];
-       for ( i = 0, len = eventNames.length; i < len; i++ ) {
-               eventName = eventNames[i];
-               $node.on( eventName, ve.bind( this.onEvent, this, eventName ) );
+       this.clearListeners();
+};
+
+/**
+ * Clear all listeners
+ * @method
+ */
+ve.EventSequencer.prototype.clearListeners = function () {
+       var i, len, eventName;
+       this.preListenersForEvent = {};
+       this.postListenersForEvent = {};
+       // TODO: what should we do with pending calls?
+       for ( i = 0, len = this.eventNames.length; i < len; i++ ) {
+               eventName = this.eventNames[i];
+               this.$node.off( eventName );
+               this.$node.on( eventName, ve.bind( this.onEvent, this, 
eventName ) );
                this.preListenersForEvent[eventName] = [];
                this.postListenersForEvent[eventName] = [];
        }

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: I0085e4a53c5a776733dce6944b867b8d2228ba4b
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Divec <[email protected]>

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

Reply via email to