Divec has uploaded a new change for review.

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


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

ve.ce.EventSequencer: Post-event listening.

modules/ve/ce/ve.ce.EventSequener.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
---
A demos/ve/eventSequencer.html
A modules/ve/ce/ve.ce.EventSequencer.js
2 files changed, 193 insertions(+), 0 deletions(-)


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

diff --git a/demos/ve/eventSequencer.html b/demos/ve/eventSequencer.html
new file mode 100644
index 0000000..1254fc0
--- /dev/null
+++ b/demos/ve/eventSequencer.html
@@ -0,0 +1,64 @@
+<html>
+<meta charset="UTF-8">
+<style type='text/css'>#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/rangy/rangy-core-1.3.js"></script>
+<script src="../../modules/rangy/rangy-position-1.3.js"></script>
+-->
+<script src="../../modules/ve/ve.js"></script>
+<script src="../../modules/ve/ce/ve.ce.js"></script>
+<script src="../../modules/ve/ce/ve.ce.EventSequencer.js"></script>
+<script type="text/javascript">
+function onbodyload () {
+       var i, eventSequencer,
+               eventNames = ['compositionstart', 'compositionend', 'keydown', 
'keyup', 'keypress'],
+               badDiv = document.getElementById( 'bad' ),
+               goodDiv = document.getElementById( 'good' );
+
+       eventSequencer = new ve.ce.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>
+<body onload="onbodyload()">
+Good (ve.ce.EventSequencer):
+<div id="good" contenteditable="true"></div>
+Bad (setTimeout):
+<div id="bad" contenteditable="true"></div>
diff --git a/modules/ve/ce/ve.ce.EventSequencer.js 
b/modules/ve/ce/ve.ce.EventSequencer.js
new file mode 100644
index 0000000..a2c0326
--- /dev/null
+++ b/modules/ve/ce/ve.ce.EventSequencer.js
@@ -0,0 +1,129 @@
+/*!
+ * VisualEditor ContentEditable 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 {Object} node Node to which listeners should be attached
+ * @param {string[]} List of event Names to listen to
+ * @param {boundLogFunc} Logging function, pre-bound using ve.bind etc
+ */
+
+ve.ce.EventSequencer = function ( node, eventNames, boundLogFunc ) {
+       qqq = this;
+       var i, eventName;
+       this.node = node;
+       this.preListenersForEvent = {};
+       this.postListenersForEvent = {};
+       this.log = boundLogFunc || function () {};
+       this.pendingCalls = []; // [{id: x, func: x, ev: x, eventName: x} ... ]
+       for ( i = 0; i < eventNames.length; i++ ) {
+               eventName = eventNames[i];
+               node.addEventListener( 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.ce.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.ce.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.ce.EventSequencer.prototype.onEvent = function( eventName, ev ) {
+       var i, preListener, postListener, pendingCall;
+       this.log( '(EventSequencer: onEvent', eventName, ev, ')' );
+       this.runAllPendingCallsNow();
+       for ( i = 0; i < this.preListenersForEvent[eventName].length; i++ ) {
+               preListener = this.preListenersForEvent[eventName][i];
+               this.log( '(EventSequencer: preListener', eventName, ev, ')' );
+               try {
+                       preListener( ev );
+               } catch ( error ) {
+                       ve.log( error );
+                       this.log( error );
+               }
+       }
+       for ( i = 0; i < this.postListenersForEvent[eventName].length; i++ ) {
+               postListener = this.postListenersForEvent[eventName][i];
+               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; // TODO: why is this necessary?
+                               }
+                               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.ce.EventSequencer.prototype.runAllPendingCallsNow = function () {
+       var i, pendingCall;
+       this.log( '(EventSequencer: runAllPendingCallsNow', this.pendingCalls, 
')' );
+       for ( i = 0; i < this.pendingCalls.length; i++ ) {
+               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
+               try {
+                       pendingCall.func( pendingCall.ev );
+               }
+               catch ( error ) {
+                       ve.log( error );
+                       this.log( error );
+               }
+       }
+       this.pendingCalls = [];
+};

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: I4ddb10a30c2f44015136a7978a185d0b13f0690b
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