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

Change subject: EventSequencer: New hooks for useful moments
......................................................................


EventSequencer: New hooks for useful moments

ve.EventSequencer.js - Implement new types of event listener:
* onLoop( f ) to set a listener for the start of the event loop
* afterLoop( f ) to set a listener for the end of the event loop
* afterOne( f ) to set a one-time listener for after an event
* afterLoopOne( f ) to set a one-time listener for the end of the event loop

Change-Id: Ie388e0e9edcfccaa20e04c649a8b85d028ddde9c
---
M modules/ve/ve.EventSequencer.js
1 file changed, 185 insertions(+), 26 deletions(-)

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



diff --git a/modules/ve/ve.EventSequencer.js b/modules/ve/ve.EventSequencer.js
index e34d8d1..d1517e4 100644
--- a/modules/ve/ve.EventSequencer.js
+++ b/modules/ve/ve.EventSequencer.js
@@ -13,15 +13,22 @@
  * idiom, except that they are guaranteed to execute before any subsequent
  * on-event listener. Therefore, events are executed in the 'right order'.
  *
- * This matters when many events are added to the event queue in one go.
+ * This matters when many events are added to the task queue in one go.
  * For instance, browsers often queue 'keydown' and 'keypress' in immediate
  * sequence, so a setTimeout(f, 0) defined in the keydown listener will run
  * *after* the keypress listener (i.e. in the 'wrong' order). EventSequencer
  * ensures that this does not happen.
  *
- * All listeners receive the jQuery event as an argument. If an on-event
+ * All these listeners receive the jQuery event as an argument. If an on-event
  * listener needs to pass information to a corresponding after-event listener,
  * it can do so by adding properties into the jQuery event itself.
+ *
+ * There are also 'onLoop' and 'afterLoop' listeners, which only fire once per
+ * Javascript event loop iteration, respectively before and after all the
+ * other listeners fire.
+ *
+ * For further event loop / task queue information, see:
+ * 
http://www.whatwg.org/specs/web-apps/current-work/multipage/webappapis.html#event-loops
  *
  * @class ve.EventSequencer
  */
@@ -66,14 +73,19 @@
        this.pendingCalls = [];
 
        /**
-        * @property {Object.<string,Function>}
+        * @property {Object.<string,Function[]>}
         */
        this.onListenersForEvent = {};
 
        /**
-        * @property {Object.<string,Function>}
+        * @property {Object.<string,Function[]>}
         */
        this.afterListenersForEvent = {};
+
+       /**
+        * @property {Object.<string,Function[]>}
+        */
+       this.afterOneListenersForEvent = {};
 
        for ( i = 0, len = eventNames.length; i < len; i++ ) {
                eventName = eventNames[i];
@@ -81,6 +93,31 @@
                this.afterListenersForEvent[eventName] = [];
                this.eventHandlers[eventName] = makeEventHandler( eventName );
        }
+
+       /**
+        * @property {Function[]}
+        */
+       this.onLoopListeners = [];
+
+       /**
+        * @property {Function[]}
+        */
+       this.afterLoopListeners = [];
+
+       /**
+        * @property {Function[]}
+        */
+       this.afterLoopOneListeners = [];
+
+       /**
+        * @property {boolean}
+        */
+       this.doneOnLoop = false;
+
+       /**
+        * @property {number}
+        */
+       this.afterLoopTimeoutId = null;
 };
 
 /**
@@ -110,6 +147,15 @@
 
 
 /**
+ * Add listeners to be fired at the start of the Javascript event loop 
iteration
+ * @method
+ * @param {Function[]} listeners Listeners that take no arguments
+ */
+ve.EventSequencer.prototype.onLoop = function ( listeners ) {
+       this.onLoopListeners.push.apply( this.onLoopListeners, listeners );
+};
+
+/**
  * Add listeners to be fired just before the browser native action
  * @method
  * @param {Object.<string,Function>} listeners Function for each event
@@ -120,7 +166,6 @@
                this.onListenersForEvent[eventName].push( listeners[eventName] 
);
        }
 };
-
 
 /**
  * Add listeners to be fired as soon as possible after the native action
@@ -135,6 +180,36 @@
 };
 
 /**
+ * Add listeners to be fired once, as soon as possible after the native action
+ * @method
+ * @param {Object.<string,Function[]>} listeners Function for each event
+ */
+ve.EventSequencer.prototype.afterOne = function ( listeners ) {
+       var eventName;
+       for ( eventName in listeners ) {
+               this.afterOneListenersForEvent[eventName].push( 
listeners[eventName] );
+       }
+};
+
+/**
+ * Add listeners to be fired at the end of the Javascript event loop iteration
+ * @method
+ * @param {Function[]} listeners Listeners that take no arguments
+ */
+ve.EventSequencer.prototype.afterLoop = function ( listeners ) {
+       Array.prototype.push.apply( this.afterLoopListeners, listeners );
+};
+
+/**
+ * Add listeners to be fired once, at the end of the Javascript event loop 
iteration
+ * @method
+ * @param {Function[]} listeners Listeners that take no arguments
+ */
+ve.EventSequencer.prototype.afterLoopOne = function ( listeners ) {
+       Array.prototype.push.apply( this.afterLoopOneListeners, listeners );
+};
+
+/**
  * Generic listener method which does the sequencing
  * @private
  * @method
@@ -142,38 +217,122 @@
  * @param {jQuery.Event} ev The browser event
  */
 ve.EventSequencer.prototype.onEvent = function ( eventName, ev ) {
-       var i, len, onListener, afterListener, pendingCall;
+       var i, len, onListener, pendingCall, me, id;
        this.runPendingCalls();
+       if ( !this.doneOnLoop ) {
+               this.doneOnLoop = true;
+               this.doOnLoop();
+       }
        // Length cache 'len' is required, as an onListener could add another 
onListener
        for ( i = 0, len = this.onListenersForEvent[eventName].length; i < len; 
i++ ) {
                onListener = this.onListenersForEvent[eventName][i];
                onListener( ev );
        }
-       // Length cache 'len' for style only
-       for ( i = 0, len = this.afterListenersForEvent[eventName].length; i < 
len; i++ ) {
-               afterListener = this.afterListenersForEvent[eventName][i];
-
+       // Queue a call to afterEvent only if there are some 
afterListeners/afterOneListeners
+       if ( this.afterListenersForEvent[eventName].length > 0 ||
+               this.afterOneListenersForEvent[eventName].length > 0 ) {
                // Create a cancellable pending call
                // - Create the pendingCall object first
                // - then create the setTimeout invocation to modify 
pendingCall.id
                // - then set pendingCall.id to the setTimeout id, so the call 
can cancel itself
-               // Must wrap everything in a function call, to create the 
required closure.
-               pendingCall = { 'func': afterListener, 'id': null, 'ev': ev, 
'eventName': eventName };
-               /*jshint loopfunc:true */
-               ( function ( pendingCall, ev ) {
-                       var id = setTimeout( function () {
-                               if ( pendingCall.id === null ) {
-                                       // clearTimeout seems not always to 
work immediately
-                                       return;
-                               }
-                               pendingCall.id = null;
-                               pendingCall.func( ev );
-                       } );
-                       pendingCall.id = id;
-               } )( pendingCall, ev );
-               /*jshint loopfunc:false */
+               pendingCall = { 'id': null, 'ev': ev, 'eventName': eventName };
+               me = this;
+               id = setTimeout( function () {
+                       if ( pendingCall.id === null ) {
+                               // clearTimeout seems not always to work 
immediately
+                               return;
+                       }
+                       me.resetAfterLoopTimeout();
+                       pendingCall.id = null;
+                       me.afterEvent( eventName, ev );
+               } );
+               pendingCall.id = id;
                this.pendingCalls.push( pendingCall );
        }
+};
+
+/**
+ * Generic after listener method which gets queued
+ * @private
+ * @method
+ * @param {string} eventName Javascript name of the event, e.g. 'keydown'
+ * @param {jQuery.Event} ev The browser event
+ */
+ve.EventSequencer.prototype.afterEvent = function ( eventName, ev ) {
+       var i, len, afterListeners, afterOneListeners;
+
+       // Snapshot the listener lists, and blank *OneListener list.
+       // This ensures reasonable behaviour if a function called adds another 
listener.
+        afterListeners = this.afterListenersForEvent[eventName].slice();
+       afterOneListeners = this.afterOneListenersForEvent[eventName].slice();
+       this.afterOneListenersForEvent[eventName].length = 0;
+
+       for ( i = 0, len = afterListeners.length; i < len; i++ ) {
+               afterListeners[i]( ev );
+       }
+
+       for ( i = 0, len = afterOneListeners.length; i < len; i++ ) {
+               afterOneListeners[i]( ev );
+       }
+};
+
+/**
+ * Call each onLoopListener once
+ * @private
+ * @method
+ */
+ve.EventSequencer.prototype.doOnLoop = function () {
+       var i, len;
+       // Length cache 'len' is required, as the functions called may add 
another listener
+       for ( i = 0, len = this.onLoopListeners.length; i < len; i++ ) {
+               this.onLoopListeners[i]();
+       }
+};
+
+/**
+ * Call each afterLoopListener once, unless the setTimeout is already cancelled
+ * @private
+ * @method
+ * @param {number} myTimeoutId The calling setTimeout id
+ */
+ve.EventSequencer.prototype.doAfterLoop = function ( myTimeoutId ) {
+       var i, len, afterLoopListeners, afterOneLoopListeners;
+
+       if ( this.afterLoopTimeoutId !== myTimeoutId ) {
+               // cancelled; do nothing
+               return;
+       }
+       this.afterLoopTimeoutId = null;
+
+       // Snapshot the listener lists, and blank *OneListener list.
+       // This ensures reasonable behaviour if a function called adds another 
listener.
+       afterLoopListeners = this.afterLoopListeners.slice();
+       afterOneLoopListeners = this.afterOneLoopListeners.slice();
+       this.afterOneLoopListeners.length = 0;
+
+       for ( i = 0, len = this.afterLoopListeners.length; i < len; i++ ) {
+               this.afterLoopListeners[i]();
+       }
+
+       for ( i = 0, len = this.afterLoopOneListeners.length; i < len; i++ ) {
+               this.afterLoopOneListeners[i]();
+       }
+};
+
+/**
+ * Push any pending doAfterLoop to end of task queue (cancel, then re-set)
+ * @private
+ * @method
+ */
+ve.EventSequencer.prototype.resetAfterLoopTimeout = function () {
+       var timeoutId, me = this;
+       if ( this.afterLoopTimeoutId !== null ) {
+               clearTimeout( this.afterLoopTimeoutId );
+       }
+       timeoutId = setTimeout( function () {
+               me.doAfterLoop( timeoutId );
+       } );
+       this.afterLoopTimeoutId = timeoutId;
 };
 
 /**
@@ -196,7 +355,7 @@
                pendingCall.id = null;
                // Force to run now. It's important that we set id to null 
before running,
                // so that there's no chance a recursive call will call the 
listener again.
-               pendingCall.func( pendingCall.ev );
+               this.afterEvent( pendingCall.eventName, pendingCall.ev );
        }
        // This is safe because we only ever appended to the list, so it's 
definitely exhausted
        // now.

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

Gerrit-MessageType: merged
Gerrit-Change-Id: Ie388e0e9edcfccaa20e04c649a8b85d028ddde9c
Gerrit-PatchSet: 5
Gerrit-Project: mediawiki/extensions/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Divec <[email protected]>
Gerrit-Reviewer: Catrope <[email protected]>
Gerrit-Reviewer: Krinkle <[email protected]>
Gerrit-Reviewer: jenkins-bot

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

Reply via email to