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

Change subject: ve.EventSequencer: Post-event listening
......................................................................


ve.EventSequencer: Post-event listening

modules/ve/ve.EventSequencer.js
* Class to sequence pre-event and post-event listening correctly

demos/ve/eventSequencer.html
* Plain HTML example page for testing EventSequencer and event sequences

Change-Id: I4ddb10a30c2f44015136a7978a185d0b13f0690b
---
M .docs/categories.json
M VisualEditor.php
A demos/ve/eventSequencer.html
M demos/ve/index.php
M modules/ve-mw/test/index.php
M modules/ve/test/index.php
A modules/ve/ve.EventSequencer.js
7 files changed, 214 insertions(+), 1 deletion(-)

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



diff --git a/.docs/categories.json b/.docs/categories.json
index cfe0e4c..1947188 100644
--- a/.docs/categories.json
+++ b/.docs/categories.json
@@ -153,7 +153,10 @@
                "groups": [
                        {
                                "name": "Utilities",
-                               "classes": ["ve", "ve.EventEmitter", 
"ve.Registry", "ve.Factory", "ve.Range", "ve.Element"]
+                               "classes": [
+                                       "ve", "ve.EventEmitter", "ve.Registry", 
"ve.Factory",
+                                       "ve.Range", "ve.Element", 
"ve.EventSequencer"
+                               ]
                        },
                        {
                                "name": "Factories",
diff --git a/VisualEditor.php b/VisualEditor.php
index aa300c7..1341b05 100644
--- a/VisualEditor.php
+++ b/VisualEditor.php
@@ -264,6 +264,7 @@
                        've/ve.LeafNode.js',
                        've/ve.Element.js',
                        've/ve.Document.js',
+                       've/ve.EventSequencer.js',
 
                        // dm
                        've/dm/ve.dm.js',
diff --git a/demos/ve/eventSequencer.html b/demos/ve/eventSequencer.html
new file mode 100644
index 0000000..a6bf406
--- /dev/null
+++ b/demos/ve/eventSequencer.html
@@ -0,0 +1,73 @@
+<!doctype html>
+<html>
+       <head>
+               <meta charset="UTF-8">
+               <style>
+                       #good, #bad {
+                               min-height: 2em;
+                               border: solid red 1px;
+                       }
+               </style>
+               <script src="../../modules/jquery/jquery.js"></script>
+               <script src="../../modules/jquery/jquery.client.js"></script>
+               <script src="../../modules/oojs/oo.js"></script>
+               <script src="../../modules/unicodejs/unicodejs.js"></script>
+               <script 
src="../../modules/unicodejs/unicodejs.graphemebreak.js"></script>
+               <script 
src="../../modules/unicodejs/unicodejs.wordbreak.js"></script>
+               <script src="../../modules/ve/ve.js"></script>
+               <script src="../../modules/ve/ve.EventSequencer.js"></script>
+               <script src="../../modules/ve/ce/ve.ce.js"></script>
+               <script>
+                       function onbodyload () {
+                               var i, eventSequencer,
+                                       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] );
+                               }
+                               goodDiv.focus();
+                       }
+
+                       function addSetTimeoutListeners( node, eventName ) {
+                               node.addEventListener( eventName, function ( e 
) {
+                                       console.log( eventName + showEventCode( 
e ) + ': ' +
+                                               JSON.stringify( node.innerHTML 
) );
+                                       setTimeout( function () {
+                                               console.log( 'setTimeout from ' 
+ eventName +
+                                                       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 ) );
+                               });
+                       }
+
+                       function showEventCode( e ) {
+                               return ( e && e.keyCode ) ? '(keyCode=' + 
e.keyCode + ')' : '';
+                       }
+               </script>
+       </head>
+       <body onload="onbodyload()">
+               Good (ve.EventSequencer):
+               <div id="good" contenteditable="true"></div>
+               Bad (setTimeout):
+               <div id="bad" contenteditable="true"></div>
+       </body>
+</html>
diff --git a/demos/ve/index.php b/demos/ve/index.php
index cd84377..a100953 100644
--- a/demos/ve/index.php
+++ b/demos/ve/index.php
@@ -116,6 +116,7 @@
                <script src="../../modules/ve/ve.LeafNode.js"></script>
                <script src="../../modules/ve/ve.Element.js"></script>
                <script src="../../modules/ve/ve.Document.js"></script>
+               <script src="../../modules/ve/ve.EventSequencer.js"></script>
                <script src="../../modules/ve/dm/ve.dm.js"></script>
                <script src="../../modules/ve/dm/ve.dm.Model.js"></script>
                <script 
src="../../modules/ve/dm/ve.dm.ModelRegistry.js"></script>
diff --git a/modules/ve-mw/test/index.php b/modules/ve-mw/test/index.php
index f504a5e..a3bbb54 100644
--- a/modules/ve-mw/test/index.php
+++ b/modules/ve-mw/test/index.php
@@ -69,6 +69,7 @@
                <script src="../../ve/ve.LeafNode.js"></script>
                <script src="../../ve/ve.Element.js"></script>
                <script src="../../ve/ve.Document.js"></script>
+               <script src="../../ve/ve.EventSequencer.js"></script>
                <script src="../../ve/dm/ve.dm.js"></script>
                <script src="../../ve/dm/ve.dm.Model.js"></script>
                <script src="../../ve/dm/ve.dm.ModelRegistry.js"></script>
diff --git a/modules/ve/test/index.php b/modules/ve/test/index.php
index a09e86a..af92ed3 100644
--- a/modules/ve/test/index.php
+++ b/modules/ve/test/index.php
@@ -69,6 +69,7 @@
                <script src="../../ve/ve.LeafNode.js"></script>
                <script src="../../ve/ve.Element.js"></script>
                <script src="../../ve/ve.Document.js"></script>
+               <script src="../../ve/ve.EventSequencer.js"></script>
                <script src="../../ve/dm/ve.dm.js"></script>
                <script src="../../ve/dm/ve.dm.Model.js"></script>
                <script src="../../ve/dm/ve.dm.ModelRegistry.js"></script>
diff --git a/modules/ve/ve.EventSequencer.js b/modules/ve/ve.EventSequencer.js
new file mode 100644
index 0000000..fdb4bc3
--- /dev/null
+++ b/modules/ve/ve.EventSequencer.js
@@ -0,0 +1,133 @@
+/*!
+ * VisualEditor EventSequencer class.
+ *
+ * @copyright 2013 VisualEditor Team and others; see AUTHORS.txt
+ * @license The MIT License (MIT); see LICENSE.txt
+ */
+
+/**
+ * EventSequencer class with pre-event and post-event listeners.
+ *
+ * Post-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'.
+ *
+ * This matters when many events are added to the event 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.
+ *
+ * @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
+ */
+ve.EventSequencer = function ( node, eventNames, boundLogFunc ) {
+       var i, len, eventName, $node = $( node );
+       this.node = node;
+       this.preListenersForEvent = {};
+       this.postListenersForEvent = {};
+       this.log = boundLogFunc || function () {};
+
+       /**
+        * @property {Object[]}
+        *  - id {number} Id for setTimeout
+        *  - func {Function} Post-event listener
+        *  - ev {jQuery.Event} Browser event
+         *  - 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.preListenersForEvent[eventName] = [];
+               this.postListenersForEvent[eventName] = [];
+       }
+};
+
+/**
+ * Add a listener to be fired just before the browser native action
+ * @method
+ * @param {string} eventName Javascript name of the event, e.g. 'keydown'
+ * @param {Function} listener Listener accepting a single argument 'event'
+ */
+ve.EventSequencer.prototype.addPreListener = function( eventName, listener ) {
+       this.preListenersForEvent[eventName].push( listener );
+};
+
+/**
+ * Add a listener to be fired as soon as possible after the native action
+ * @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 );
+};
+
+/**
+ * Generic listener method which does the sequencing
+ * @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 );
+       }
+       for ( i = 0, len = this.postListenersForEvent[eventName].length; i < 
len; i++ ) {
+               // Length cache for style
+               postListener = this.postListenersForEvent[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 };
+               /*jshint loopfunc:true */
+               ( function ( pendingCall, ev, log ) {
+                       var id = setTimeout( function () {
+                               if ( pendingCall.id === null ) {
+                                       return; // Seems to be necessary in 
Chromium
+                               }
+                               pendingCall.id = null;
+                               log( '(EventSequencer: reached postListener', 
eventName, ev, ')' );
+                               pendingCall.func( ev );
+                       } );
+                       pendingCall.id = id;
+               } )( pendingCall, ev, this.log );
+               /*jshint loopfunc:false */
+               this.pendingCalls.push( pendingCall );
+       }
+};
+
+/**
+ * Run any pending listeners, and clear the pending queue
+ * @method
+ */
+ve.EventSequencer.prototype.runAllPendingCallsNow = function () {
+       var i, pendingCall;
+       this.log( '(EventSequencer: runAllPendingCallsNow', this.pendingCalls, 
')' );
+       for ( i = 0; i < this.pendingCalls.length; i++ ) {
+               // Length cache not possible, as a pending call appends another 
pending call.
+               pendingCall = this.pendingCalls[i];
+               if ( pendingCall.id === null ) {
+                       continue; // already run
+               }
+               clearTimeout( pendingCall.id );
+               pendingCall.id = null;
+               this.log( '(EventSequencer: reached postListener', pendingCall, 
')' );
+               // Force to run now
+               pendingCall.func( pendingCall.ev );
+       }
+       this.pendingCalls = [];
+};

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

Gerrit-MessageType: merged
Gerrit-Change-Id: I4ddb10a30c2f44015136a7978a185d0b13f0690b
Gerrit-PatchSet: 9
Gerrit-Project: mediawiki/extensions/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: Inez <[email protected]>
Gerrit-Reviewer: Krinkle <[email protected]>
Gerrit-Reviewer: Trevor Parscal <[email protected]>
Gerrit-Reviewer: jenkins-bot

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

Reply via email to