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

Change subject: Timoize the EventSequencer into a sleek, purring monster
......................................................................


Timoize the EventSequencer into a sleek, purring monster

Change-Id: Ia925d76d9a41b777861621724ced4db8371786b8
---
M demos/ve/eventSequencer.html
M modules/ve/ve.EventSequencer.js
2 files changed, 139 insertions(+), 68 deletions(-)

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



diff --git a/demos/ve/eventSequencer.html b/demos/ve/eventSequencer.html
index a6bf406..be8ce5d 100644
--- a/demos/ve/eventSequencer.html
+++ b/demos/ve/eventSequencer.html
@@ -18,19 +18,34 @@
                <script src="../../modules/ve/ve.EventSequencer.js"></script>
                <script src="../../modules/ve/ce/ve.ce.js"></script>
                <script>
+                       function makeListener( message ) {
+                               return function ( e ) {
+                                       console.log( message + showEventCode( e 
) +
+                                               ' ' + JSON.stringify( 
document.getElementById(
+                                               'good' ).innerHTML ) );
+                               };
+                       };
                        function onbodyload () {
-                               var i, eventSequencer,
+                               var i, len, eventSequencer, eventName,
+                                       onEvents = {},
+                                       afterEvents = {},
                                        eventNames = ['compositionstart', 
'compositionend',
                                                'keydown', 'keyup', 'keypress'],
                                        badDiv = document.getElementById( 'bad' 
),
                                        goodDiv = document.getElementById( 
'good' );
 
-                               eventSequencer = new ve.EventSequencer( 
goodDiv, eventNames,
-                                       ve.bind( console.log, console ) );
-                               for( i = 0; i < eventNames.length; i++ ) {
-                                       addPrePostListeners( eventSequencer, 
eventNames[i] );
-                                       addSetTimeoutListeners( badDiv, 
eventNames[i] );
+                               for ( i = 0, len = eventNames.length; i < len; 
i++ ) {
+                                       eventName = eventNames[i];
+                                       onEvents[eventName] = makeListener( 'on 
' + eventName );
+                                       afterEvents[eventName] = makeListener(
+                                               'after ' + eventName );
+                                       addSetTimeoutListeners( badDiv, 
eventName );
                                }
+
+                               eventSequencer = new ve.EventSequencer( 
eventNames );
+                               eventSequencer.on( onEvents );
+                               eventSequencer.after( afterEvents );
+                               eventSequencer.attach( $( goodDiv ) );
                                goodDiv.focus();
                        }
 
@@ -43,19 +58,6 @@
                                                        showEventCode( e ) + ': 
' +
                                                        JSON.stringify( 
node.innerHTML ) );
                                        } );
-                               });
-                       }
-
-                       function addPrePostListeners ( eventSequencer, 
eventName ) {
-                               eventSequencer.addPreListener( eventName, 
function ( e ) {
-                                       console.log( '*** pre ' + eventName + 
showEventCode( e ) +
-                                               ' ' + JSON.stringify( 
document.getElementById(
-                                               'good' ).innerHTML ) );
-                               });
-                               eventSequencer.addPostListener( eventName, 
function ( e ) {
-                                       console.log( '*** post ' + eventName + 
showEventCode( e ) +
-                                               ' ' + JSON.stringify( 
document.getElementById(
-                                               'good' ).innerHTML ) );
                                });
                        }
 
diff --git a/modules/ve/ve.EventSequencer.js b/modules/ve/ve.EventSequencer.js
index fdb4bc3..cba7ff8 100644
--- a/modules/ve/ve.EventSequencer.js
+++ b/modules/ve/ve.EventSequencer.js
@@ -6,12 +6,12 @@
  */
 
 /**
- * EventSequencer class with pre-event and post-event listeners.
+ * EventSequencer class with on-event and after-event listeners.
  *
- * Post-event listeners are fired as soon as possible after the
+ * After-event listeners are fired as soon as possible after the
  * corresponding native event. They are similar to the setTimeout(f, 0)
  * idiom, except that they are guaranteed to execute before any subsequent
- * pre-event listener. Therefore, events are executed in the 'right order'.
+ * 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.
  * For instance, browsers often queue 'keydown' and 'keypress' in immediate
@@ -19,17 +19,29 @@
  * *after* the keypress listener (i.e. in the 'wrong' order). EventSequencer
  * ensures that this does not happen.
  *
- * @constructor
- * @param {HTMLElement} node Node to which listeners should be attached
- * @param {string[]} eventNames List of event Names to listen to
- * @param {Function} [boundLogFunc] Logging function, pre-bound with ve.bind
+ * All 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.
+ *
+ * @class ve.EventSequencer
  */
-ve.EventSequencer = function ( node, eventNames, boundLogFunc ) {
-       var i, len, eventName, $node = $( node );
-       this.node = node;
-       this.preListenersForEvent = {};
-       this.postListenersForEvent = {};
-       this.log = boundLogFunc || function () {};
+
+/**
+ *
+ * To fire after-event listeners promptly, the EventSequencer may need to
+ * listen to some events for which it has no registered on-event or
+ * event-event listeners. For instance, to ensure an after-keydown listener
+ * is be fired before the native keyup action, you must include both
+ * 'keydown' and 'keyup' in the eventNames Array.
+ *
+ * @constructor
+ * @param {string[]} eventNames List of event Names to listen to
+ */
+ve.EventSequencer = function ( eventNames ) {
+       var i, len, eventName, makeEventHandler, eventSequencer = this;
+       this.$node = null;
+       this.eventNames = eventNames;
+       this.eventHandlers = {};
 
        /**
         * @property {Object[]}
@@ -39,72 +51,122 @@
          *  - eventName {string} Name, such as keydown
          */
        this.pendingCalls = [];
+
+       /**
+         * @property {Object.<string,Function>}
+        */
+       this.onListenersForEvent = {};
+
+       /**
+         * @property {Object.<string,Function>}
+        */
+       this.afterListenersForEvent = {};
+
+       makeEventHandler = function ( eventName ) {
+               return function ( ev ) {
+                       return eventSequencer.onEvent( eventName, ev );
+               };
+       };
+
        for ( i = 0, len = eventNames.length; i < len; i++ ) {
                eventName = eventNames[i];
-               $node.on( eventName, ve.bind( this.onEvent, this, eventName ) );
-               this.preListenersForEvent[eventName] = [];
-               this.postListenersForEvent[eventName] = [];
+               this.onListenersForEvent[eventName] = [];
+               this.afterListenersForEvent[eventName] = [];
+               this.eventHandlers[eventName] = makeEventHandler( eventName );
        }
 };
 
 /**
- * Add a listener to be fired just before the browser native action
+ * Attach to a node, to listen to its jQuery events
+ *
  * @method
- * @param {string} eventName Javascript name of the event, e.g. 'keydown'
- * @param {Function} listener Listener accepting a single argument 'event'
+ * @param {jQuery} $node The node to attach to
  */
-ve.EventSequencer.prototype.addPreListener = function( eventName, listener ) {
-       this.preListenersForEvent[eventName].push( listener );
+ve.EventSequencer.prototype.attach = function ( $node ) {
+       this.detach();
+       this.$node = $node.on( this.eventHandlers );
 };
 
 /**
- * Add a listener to be fired as soon as possible after the native action
+ * Detach from a node (if attached), to stop listen to its jQuery events
+ *
  * @method
- * @param {string} eventName Javascript name of the event, e.g. 'keydown'
- * @param {Function} listener Listener accepting a single argument 'event'
  */
-ve.EventSequencer.prototype.addPostListener = function( eventName, listener ) {
-       this.postListenersForEvent[eventName].push( listener );
+ve.EventSequencer.prototype.detach = function () {
+       if ( this.$node === null ) {
+               return;
+       }
+       this.runPendingCalls();
+       this.$node.off( this.eventHandlers );
+       this.$node = null;
+};
+
+
+/**
+ * Add listeners to be fired just before the browser native action
+ * @method
+ * @param {Object.<string,Function>} listeners Function for each event
+ */
+ve.EventSequencer.prototype.on = function ( listeners ) {
+       var eventName;
+       for ( eventName in listeners ) {
+               this.onListenersForEvent[eventName].push( listeners[eventName] 
);
+       }
+};
+
+
+/**
+ * Add listeners to be fired as soon as possible after the native action
+ * @method
+ * @param {Object.<string,Function>} listeners Function for each event
+ */
+ve.EventSequencer.prototype.after = function ( listeners ) {
+       var eventName;
+       for ( eventName in listeners ) {
+               this.afterListenersForEvent[eventName].push( 
listeners[eventName] );
+       }
 };
 
 /**
  * Generic listener method which does the sequencing
+ * @private
  * @method
  * @param {string} eventName Javascript name of the event, e.g. 'keydown'
  * @param {jQuery.Event} ev The browser event
  */
-ve.EventSequencer.prototype.onEvent = function( eventName, ev ) {
-       var i, len, preListener, postListener, pendingCall;
-       this.log( '(EventSequencer: onEvent', eventName, ev, ')' );
-       this.runAllPendingCallsNow();
-       for ( i = 0, len = this.preListenersForEvent[eventName].length; i < 
len; i++ ) {
-               // Length cache is required, as a preListener could add another 
preListener
-               preListener = this.preListenersForEvent[eventName][i];
-               this.log( '(EventSequencer: preListener', eventName, ev, ')' );
-               preListener( ev );
+ve.EventSequencer.prototype.onEvent = function ( eventName, ev ) {
+       var i, len, onListener, afterListener, pendingCall;
+       ve.log( 'EventSequencer: onEvent', eventName, ev );
+       this.runPendingCalls();
+       // 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];
+               ve.log( 'EventSequencer: on ', eventName, ev );
+               onListener( ev );
        }
-       for ( i = 0, len = this.postListenersForEvent[eventName].length; i < 
len; i++ ) {
-               // Length cache for style
-               postListener = this.postListenersForEvent[eventName][i];
+       // Length cache 'len' for style only
+       for ( i = 0, len = this.afterListenersForEvent[eventName].length; i < 
len; i++ ) {
+               afterListener = this.afterListenersForEvent[eventName][i];
 
                // 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': postListener, 'id': null, 'ev': ev, 
'eventName': eventName };
+               pendingCall = { 'func': afterListener, 'id': null, 'ev': ev, 
'eventName': eventName };
                /*jshint loopfunc:true */
-               ( function ( pendingCall, ev, log ) {
+               ( function ( pendingCall, ev ) {
                        var id = setTimeout( function () {
                                if ( pendingCall.id === null ) {
-                                       return; // Seems to be necessary in 
Chromium
+                                       // clearTimeout seems not always to 
work immediately
+                                       return;
                                }
                                pendingCall.id = null;
-                               log( '(EventSequencer: reached postListener', 
eventName, ev, ')' );
+                               ve.log( 'EventSequencer: timed: after', 
eventName, ev );
                                pendingCall.func( ev );
                        } );
                        pendingCall.id = id;
-               } )( pendingCall, ev, this.log );
+               } )( pendingCall, ev );
                /*jshint loopfunc:false */
                this.pendingCalls.push( pendingCall );
        }
@@ -112,22 +174,29 @@
 
 /**
  * Run any pending listeners, and clear the pending queue
+ * @private
  * @method
  */
-ve.EventSequencer.prototype.runAllPendingCallsNow = function () {
+ve.EventSequencer.prototype.runPendingCalls = function () {
        var i, pendingCall;
-       this.log( '(EventSequencer: runAllPendingCallsNow', this.pendingCalls, 
')' );
+       ve.log( 'EventSequencer: runPendingCalls', this.pendingCalls );
        for ( i = 0; i < this.pendingCalls.length; i++ ) {
                // Length cache not possible, as a pending call appends another 
pending call.
+               // It's important that this list remains mutable, in the case 
that this
+               // function indirectly recurses.
                pendingCall = this.pendingCalls[i];
                if ( pendingCall.id === null ) {
-                       continue; // already run
+                       // the call has already run
+                       continue;
                }
                clearTimeout( pendingCall.id );
                pendingCall.id = null;
-               this.log( '(EventSequencer: reached postListener', pendingCall, 
')' );
-               // Force to run now
+               ve.log( 'EventSequencer: forced: after', pendingCall );
+               // 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.pendingCalls = [];
+       // This is safe because we only ever appended to the list, so it's 
definitely exhausted
+       // now.
+       this.pendingCalls.length = 0;
 };

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

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

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

Reply via email to