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