mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-28 00:00:49 +00:00
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
This commit is contained in:
parent
8f7e9c27a7
commit
793172e41e
|
@ -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",
|
||||
|
|
|
@ -264,6 +264,7 @@ $wgResourceModules += array(
|
|||
've/ve.LeafNode.js',
|
||||
've/ve.Element.js',
|
||||
've/ve.Document.js',
|
||||
've/ve.EventSequencer.js',
|
||||
|
||||
// dm
|
||||
've/dm/ve.dm.js',
|
||||
|
|
73
demos/ve/eventSequencer.html
Normal file
73
demos/ve/eventSequencer.html
Normal file
|
@ -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>
|
|
@ -116,6 +116,7 @@ $html = file_get_contents( $page );
|
|||
<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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
133
modules/ve/ve.EventSequencer.js
Normal file
133
modules/ve/ve.EventSequencer.js
Normal file
|
@ -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 = [];
|
||||
};
|
Loading…
Reference in a new issue