ve.ui.CommandFactory: Initial implementation

Based on https://github.com/ccampbell/mousetrap.

Cleaned up to fit our coding standards, pass JSHint, and assume
jQuery's fixes where possible (e.g. no need for an addEvent
utility, no need for filling e.target, e.which, etc. cross-browser
which jQuery.Event already does).

Initially all were local functions in the constructor, but to
allow some customisations in subclasses moved various methods
to the prototype instead and marked with @private.

Really, only .register() must be called from the outside. The rest
assumes normalisation etc. or might break things if called
incorrectly.

Change-Id: Ic69a3c70759052094aefbeab623d885f8b118e14
This commit is contained in:
Timo Tijhof 2012-10-23 00:27:42 +02:00 committed by Gerrit Code Review
parent 5b8aa91fe8
commit 34cbc729db
6 changed files with 918 additions and 0 deletions

View file

@ -24,10 +24,12 @@ here for consistency.
* @abstract
* @constructor
* @extends {Type}
* @private
* @static
* @method
* @until Text: Optional details.
* @source
* @context {Type} The type of the `this` value.
* @param {Type} varName Optional description.
* @returns {Type} Optional description.

View file

@ -233,6 +233,7 @@ $wgResourceModules += array(
// ui
've/ui/ve.ui.js',
've/ui/ve.ui.CommandFactory.js',
've/ui/ve.ui.Inspector.js',
've/ui/ve.ui.Tool.js',
've/ui/ve.ui.Toolbar.js',

View file

@ -153,6 +153,7 @@ $html = '<div>' . file_get_contents( $page ) . '</div>';
<script src="../../modules/ve/ce/nodes/ve.ce.TableSectionNode.js"></script>
<script src="../../modules/ve/ce/nodes/ve.ce.TextNode.js"></script>
<script src="../../modules/ve/ui/ve.ui.js"></script>
<script src="../../modules/ve/ui/ve.ui.CommandFactory.js"></script>
<script src="../../modules/ve/ui/ve.ui.Inspector.js"></script>
<script src="../../modules/ve/ui/ve.ui.Tool.js"></script>
<script src="../../modules/ve/ui/ve.ui.Toolbar.js"></script>

View file

@ -102,6 +102,7 @@
<script src="../../ve/ce/nodes/ve.ce.TableSectionNode.js"></script>
<script src="../../ve/ce/nodes/ve.ce.TextNode.js"></script>
<script src="../../ve/ui/ve.ui.js"></script>
<script src="../../ve/ui/ve.ui.CommandFactory.js"></script>
<script src="../../ve/ui/ve.ui.Inspector.js"></script>
<script src="../../ve/ui/ve.ui.Tool.js"></script>
<script src="../../ve/ui/ve.ui.Toolbar.js"></script>
@ -151,6 +152,7 @@
<script src="ce/ve.ce.BranchNode.test.js"></script>
<script src="ce/ve.ce.LeafNode.test.js"></script>
<script src="ce/nodes/ve.ce.TextNode.test.js"></script>
<script src="ui/ve.ui.CommandFactory.test.js"></script>
</head>
<body>
<div id="qunit"></div>

View file

@ -0,0 +1,163 @@
/**
* VisualEditor CommandFactory tests.
*
* @copyright 2011-2012 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
( function () {
// When runSequence is called multiple times,
// be sure to keep track of how far in the future we've scheduled,
// since mixed sequences are (intentionally) invalidated.
var sequenceSimulatorOffset = 0;
QUnit.module( 've.ui.CommandFactory', {
setup: function () {
// Clear registry after each test
ve.ui.commandFactory = new ve.ui.CommandFactory();
},
teardown: function () {
// Can't restore because the constructor does .off(), so the
// old one is destroyed
ve.ui.commandFactory = new ve.ui.CommandFactory();
}
} );
/**
* @param {string|undefined} action
* @param {string} char Single character.
* @param {Object} props [optional]
*/
function runAction( action, char, props ) {
props = props || {};
props.which = char.charCodeAt( 0 );
$( 'body' ).trigger(
$.Event( action || 'keypress', props )
);
}
/**
* @param {string} action
* @param {number} interval Time between actions (in milliseconds).
* @param {string} chars
*/
function runSequence( action, interval, chars ) {
var i;
chars = chars.split( ' ' );
/**
* Utility function to avoid making functions
* in a loop, causing scope issues with 'i'.
*/
function schedule( char, delay ) {
setTimeout( function () {
runAction( action, char );
}, delay );
}
for ( i = 0; i < chars.length; i++ ) {
sequenceSimulatorOffset += interval;
schedule( chars[i], sequenceSimulatorOffset );
}
}
QUnit.test( 'register: Single characters', 18, function ( assert ) {
$.each( {
// Default should work with keypress
'default': [ undefined, 'keypress' ],
'keypress': [ 'keypress', 'keypress' ]
}, function ( action, event ) {
$.each( '0 9 a Z ! > - +'.split( ' ' ), function ( i, char ) {
ve.ui.commandFactory.register( char, function () {
assert.ok( true, action + ': "' + char + '" - ' + event[1] + ' trigger without modifier keys' );
}, event[0] );
runAction( event[1], char );
} );
} );
ve.ui.commandFactory.register( 'a', function () {
assert.ok( false, 'keypress: "a" - trigger A is ignored' );
}, 'keypress' );
runAction( 'keypress', 'A' );
ve.ui.commandFactory.register( 'a', function () {
assert.ok( false, 'keypress: "a" - trigger A with Shift is ignored' );
}, 'keypress' );
runAction( 'keypress', 'A', { shiftKey: true } );
ve.ui.commandFactory.register( 'Z', function () {
assert.ok( true, 'keypress: "Z" - trigger Z with Shift' );
}, 'keypress' );
runAction( 'keypress', 'Z', { shiftKey: true } );
ve.ui.commandFactory.register( 'g', function () {
assert.ok( false, 'keypress: "g" - register removes old callbacks' );
}, 'keypress' );
ve.ui.commandFactory.register( 'g', function () {
// Undocumented behavior, tested so we detect if/when it changes
assert.ok( true, 'keypress: "g" - register keeps only the last callback' );
}, 'keypress' );
runAction( 'keypress', 'g' );
} );
QUnit.asyncTest( 'register: Sequences', 3, function ( assert ) {
ve.ui.commandFactory.register( '1 2 3', function () {
assert.ok( true, 'Number sequence with 20ms interval' );
} );
runSequence( 'keypress', 20, '1 2 3' );
ve.ui.commandFactory.register( 'a b c Z', function () {
assert.ok( true, 'Letter sequence with 10ms interval' );
} );
runSequence( 'keypress', 10, 'a b c Z' );
ve.ui.commandFactory.register( 'o r d e r', function () {
assert.ok( false, 'Out of order is invalid' );
} );
runSequence( 'keypress', 10, 'o r d r e' );
ve.ui.commandFactory.register( 'm i s s', function () {
assert.ok( false, 'Missing in-between is invalid' );
} );
runSequence( 'keypress', 10, 'm s s' );
ve.ui.commandFactory.register( 'e x t', function () {
assert.ok( false, 'Extra in-between is invalid' );
} );
runSequence( 'keypress', 10, 'e r x t' );
ve.ui.commandFactory.register( 's l', function () {
assert.ok( true, 'Total sequence may take over a second (2 * 600ms)' );
} );
runSequence( 'keypress', 600, 's l' );
ve.ui.commandFactory.register( 'g a', function () {
assert.ok( false, 'Sequences with gaps > 1 second are invalid (2 x 1100ms)' );
} );
runSequence( 'keypress', 1100, 'g a' );
setTimeout( QUnit.start, sequenceSimulatorOffset + 1 );
} );
QUnit.test( 'register: Combinations', 2, function ( assert ) {
ve.ui.commandFactory.register( 'ctrl+i', function ( e, combo ) {
assert.ok( true, combo );
} );
// XXX: Apparently i/ctrl doesn't work, needs to be uppercase I.
// This is internally normalized back to 'a'.
// Google Chrome (and others) use keycode for 'A' when typing a + modifier.
runAction( 'keydown', 'I', { ctrlKey: true } );
ve.ui.commandFactory.register( 'cmd+b', function ( e, combo ) {
assert.ok( true, combo );
} );
runAction( 'keydown', 'B', { metaKey: true } );
} );
}() );

View file

@ -0,0 +1,749 @@
/**
* VisualEditor user interface CommandFactory class.
*
* @copyright 2011-2012 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
( function ( ve ) {
/* Private static */
var
i,
/**
* @var {Object}
* Mapping of special keycodes to the internal symolic names we use for binding.
*
* Everything in this map cannot use keypress events
* so it has to be here to map to the correct keycodes for
* keyup/keydown events (???).
*/
mapSpecialFromCode,
/**
* @var {undefined|Object}
* Inversed version of mapSpecialFromCode.
* Lazy-loaded, use getSpecialFromNameMap()
*/
mapSpecialFromName,
/**
* @var {Object}
* This is a map to allow usage of lesser common or cross-platform
* varying names of special keys. These are used to normalize
* commands for detection through mapSpecialFromCode.
*/
mapSpecialAliases,
/**
* @var {Object}
* Mapping of special characters (only used for binding in a
* keyup or keydown event to one of these keys).
*/
mapSpecialChars,
/**
* @var {Object}
* Mapping from keys that require shift on a US keypad
* back to the non shift equivalents.
*
* This is so you can use keyup events with these keys.
*/
mapShiftChars;
mapSpecialFromCode = {
8: 'backspace',
9: 'tab',
13: 'enter',
16: 'shift',
17: 'ctrl',
18: 'alt',
20: 'capslock',
27: 'esc',
32: 'space',
33: 'pageup',
34: 'pagedown',
35: 'end',
36: 'home',
37: 'left',
38: 'up',
39: 'right',
40: 'down',
45: 'ins',
46: 'del',
91: 'meta',
93: 'meta',
224: 'meta'
};
// Add F1 to F19 programatically
for ( i = 1; i <= 19; i++ ) {
mapSpecialFromCode[111 + i] = 'f' + i;
}
// Add numbers from the numeric keypad
for ( i = 0; i <= 9; i++ ) {
mapSpecialFromCode[96 + i] = i;
}
mapSpecialAliases = {
'option': 'alt',
'delete': 'del',
'return': 'enter',
'escape': 'esc',
'apple': 'meta',
'cmd': 'meta',
'command': 'meta'
};
mapSpecialChars = {
106: '*',
107: '+',
109: '-',
110: '.',
111 : '/',
186: ';',
187: '=',
188: ',',
189: '-',
190: '.',
191: '/',
192: '`',
219: '[',
220: '\\',
221: ']',
222: '\''
};
mapShiftChars = {
'~': '`',
'!': '1',
'@': '2',
'#': '3',
'$': '4',
'%': '5',
'^': '6',
'&': '7',
'*': '8',
'(': '9',
')': '0',
'_': '-',
'+': '=',
':': ';',
'\"': '\'',
'<': ',',
'>': '.',
'?': '/',
'|': '\\'
};
/**
* @return {Object}
*/
function getSpecialFromNameMap() {
if ( !mapSpecialFromName ) {
mapSpecialFromName = {};
for ( var key in mapSpecialFromCode ) {
// Pull out the numeric keypad from here because keypress should
// be able to detect the keys from the character.
if ( key > 95 && key < 112 ) {
continue;
}
if (mapSpecialFromCode.hasOwnProperty(key)) {
mapSpecialFromName[mapSpecialFromCode[key]] = key;
}
}
}
return mapSpecialFromName;
}
/**
* Extract the key character (as a string) from the event.
*
* @param {jQuery.Event} e
* @return {String}
*/
function characterFromEvent( e ) {
var char;
// Keypress play nice and set e.which correctly.
if ( e.type === 'keypress' ) {
return String.fromCharCode( e.which );
}
// For non-keypress events we need the special maps.
char = mapSpecialFromCode[ e.which ];
if ( char !== undefined ) {
return char;
}
char = mapSpecialChars[ e.which ];
if ( char !== undefined ) {
return char;
}
// Fallback for non-keypress events for non-special stuff.
return String.fromCharCode( e.which ).toLowerCase();
}
/**
* Asserts that two arrays have equal values
* (non-recursive, uses toString to assert equality).
*
* @param {Array} a
* @param {Array} b
* @returns {boolean}
*/
function arrayEqual( a, b ) {
return a.sort().join( ',' ) === b.sort().join( ',' );
}
/**
* Get a list of names of any modifier keys pressed during this key event.
*
* @param {jQuery.Event} e
* @returns {Array}
*/
function getModifiersFromEvent( e ) {
var modifiers = [];
if ( e.shiftKey ) {
modifiers.push('shift');
}
if ( e.altKey ) {
modifiers.push('alt');
}
if ( e.ctrlKey ) {
modifiers.push('ctrl');
}
if ( e.metaKey ) {
modifiers.push('meta');
}
return modifiers;
}
/**
* Determine if the key is a modifier key or not.
*
* @param {string} key Name of key (lower case).
* @returns {boolean}
*/
function isModifier( key ) {
return key === 'shift' || key === 'ctrl' || key === 'alt' || key === 'meta';
}
/**
* Guess the best action based on the key combo.
* This is used for the binding if `register()` is called without a specific action.
*
* @param {number} key Code for key.
* @param {Array} modifiers
* @param {string=} action passed in
*/
function guessAction( key, modifiers, action ) {
// if no action was picked in we should try to pick the one
// that we think would work best for this key
if ( !action ) {
action = getSpecialFromNameMap()[ key ] ? 'keydown' : 'keypress';
}
// modifier keys don't work as expected with keypress,
// switch to keydown
if ( action === 'keypress' && modifiers.length ) {
action = 'keydown';
}
return action;
}
/**
* Creates a command factory.
*
* @class
* @constructor
*/
ve.ui.CommandFactory = function VeUiCommandFactory() {
var cf = this;
/* Private properties */
/**
* @var {Object}
* List of callbacks keyed by character.
*/
cf.callbackList = {},
/**
* @var {undefined|number}
* ID of the setTimeout for the sequence detection.
*/
cf.sequenceTimer = undefined;
/**
* @var {Object}
*
* Keep track of what level each sequence is at since multiple
* sequences can start out with the same sequence.
*/
cf.sequenceTracker = {};
/**
* @var {Boolean}
* Whether we are currently witnissing a sequence being entered.
*/
cf.isInsideSequence = false;
/**
* @var {Boolean|String}
* Temporary state where we will ignore the next keyup (???).
*/
cf.ignoreNextKeyup = false;
/**
* Handler for the character from a (valid) key event.
*
* @param {string} character A single character
* @param {jQuery.Event} e
*/
function handleCharacter( character, e ) {
var callbacks = cf.getMatches( character, getModifiersFromEvent( e ), e ),
i,
doNotReset = {},
processedSequenceCallback = false;
// loop through matching callbacks for this key event
for ( i = 0; i < callbacks.length; i++ ) {
// fire for all sequence callbacks
// this is because if for example you have multiple sequences
// bound such as "g i" and "g t" they both need to fire the
// callback for matching g cause otherwise you can only ever
// match the first one
if ( callbacks[i].seq ) {
processedSequenceCallback = true;
// keep a list of which sequences were matches for later
doNotReset[callbacks[i].seq] = 1;
cf.fireCallback( callbacks[i].callback, e, callbacks[i].combo );
continue;
}
// if there were no sequence matches but we are still here
// that means this is a regular match so we should fire that
if ( !processedSequenceCallback && !cf.isInsideSequence ) {
cf.fireCallback( callbacks[i].callback, e, callbacks[i].combo );
}
}
// if you are inside of a sequence and the key you are pressing
// is not a modifier key then we should reset all sequences
// that were not matched by this key event
if ( e.type === cf.isInsideSequence && !isModifier( character ) ) {
cf.resetSequences(doNotReset);
}
}
/**
* Wrapper handler for key events.
*
* @context {HTMLElement}
* @param {jQuery.Event} e
*/
function handleKey( e ) {
var char = characterFromEvent( e );
// Stop if this key event is for a character we can't detect.
if ( !char ) {
return;
}
if ( e.type === 'keyup' && cf.ignoreNextKeyup === char ) {
cf.ignoreNextKeyup = false;
return;
}
handleCharacter( char, e );
}
$( 'body' )
.off( '.ve-commandfactory' )
.on( 'keypress.ve-commandfactory keydown.ve-commandfactory keyup.ve-commandfactory', handleKey );
};
/* Methods */
/**
* Reset all sequence counters except for the ones passed in.
*
* @private
* @method
* @param {Object} [doNotReset]
*/
ve.ui.CommandFactory.prototype.resetSequences = function ( doNotReset ) {
var key, activeSequences;
doNotReset = doNotReset || {};
activeSequences = false;
for ( key in this.sequenceTracker ) {
if (doNotReset[key]) {
activeSequences = true;
continue;
}
this.sequenceTracker[key] = 0;
}
if ( !activeSequences ) {
this.isInsideSequence = false;
}
};
/**
* Set a 1 second timeout on the specified sequence.
*
* This is so after each key press in the sequence you have 1 second
* to press the next key before you have to start over.
*
* @private
* @method
*/
ve.ui.CommandFactory.prototype.resetSequenceTimer = function () {
clearTimeout( this.sequenceTimer );
this.sequenceTimer = setTimeout( ve.bind( this.resetSequences, this ), 1000 );
};
/**
* Find all callbacks that match based on the keycode, modifiers,
* and action.
*
* @private
* @method
* @param {string} character
* @param {Array} modifiers
* @param {jQuery.Event|Object} e
* @param {boolean=} remove - should we remove any matches
* @param {string=} combo
* @returns {Array}
*/
ve.ui.CommandFactory.prototype.getMatches = function ( character, modifiers, e, remove, combo ) {
var i,
callback,
matches = [],
action = e.type;
// if there are no events related to this keycode
if ( !this.callbackList[character] ) {
return [];
}
// if a modifier key is coming up on its own we should allow it
if ( action === 'keyup' && isModifier( character ) ) {
modifiers = [ character ];
}
// loop through all callbacks for the key that was pressed
// and see if any of them match
for ( i = 0; i < this.callbackList[character].length; i++ ) {
callback = this.callbackList[character][i];
// if this is a sequence but it is not at the right level
// then move onto the next match
if ( callback.seq && this.sequenceTracker[callback.seq] !== callback.level ) {
continue;
}
// if the action we are looking for doesn't match the action we got
// then we should keep going
if ( action !== callback.action ) {
continue;
}
// if this is a keypress event and the meta key and control key
// are not pressed that means that we need to only look at the
// character, otherwise check the modifiers as well
//
// chrome will not fire a keypress if meta or control is down
// safari will fire a keypress if meta or meta+shift is down
// firefox will fire a keypress if meta or control is down
if ((action === 'keypress' && !e.metaKey && !e.ctrlKey) || arrayEqual( modifiers, callback.modifiers )) {
// remove is used so if you change your mind and call bind a
// second time with a new function the first one is overwritten
if ( remove && callback.combo === combo ) {
this.callbackList[ character ].splice( i, 1 );
}
matches.push(callback);
}
}
return matches;
};
/**
* Internal helper for binding a callback to a key sequence.
* Uses `bindSingle` underneath to track progress on the sequence.
*
* @private
* @method
* @param {string} combo The sequence as passed to `register()`.
* @param {Array} keys The individual characters in the sequence, in order.
* @param {Function} callback See `register()`.
* @param {string=guessAction()} action [optional] See `register()`.
*/
ve.ui.CommandFactory.prototype.bindSequence = function ( combo, keys, callback, action ) {
var i,
cf = this;
/**
* callback to increase the sequence level for this sequence and reset
* all other sequences that were active
*
* @param {jQuery.Event} e
*/
function increaseSequence() {
cf.isInsideSequence = action;
cf.sequenceTracker[combo]++;
cf.resetSequenceTimer();
}
/**
* wraps the specified callback inside of another function in order
* to reset all sequence counters as soon as this sequence is done
*
* @param {jQuery.Event} e
*/
function callbackAndReset( e ) {
cf.fireCallback( callback, e, combo );
// we should ignore the next key up if the action is key down
// or keypress. this is so if you finish a sequence and
// release the key the final key will not trigger a keyup
if ( action !== 'keyup' ) {
cf.ignoreNextKeyup = characterFromEvent( e );
}
// weird race condition if a sequence ends with the key
// another sequence begins with
setTimeout( cf.resetSequences, 10 );
}
// start off by adding a sequence level record for this combo
// and setting the level to 0
cf.sequenceTracker[combo] = 0;
// if there is no action guess the best one for the first key
// in the sequence
if ( !action ) {
action = guessAction( keys[0], [] );
}
// loop through keys one at a time and bind the appropriate callback
// function. for any key leading up to the final one it should
// increase the sequence. after the final, it should reset all sequences
for ( i = 0; i < keys.length; i++ ) {
cf.bindSingle(
keys[i],
i < keys.length - 1 ? increaseSequence : callbackAndReset,
action,
combo,
i
);
}
};
/**
* Internal helper for binding a (single) key command.
*
* @private
* @method
* @param {string} combo
* @param {Function} callback
* @param {string} action [optional]
* @param {string} sequenceName [optional] (internal) Name of sequence if part of sequence.
* @param {number} level [optional] (internal) What part of the sequence the command is.
*/
ve.ui.CommandFactory.prototype.bindSingle = function ( combo, callback, action, sequenceName, level ) {
// Normalize: Make sure multiple spaces in a row become a single space.
combo = combo.replace( /\s+/g, ' ' );
var i,
key,
keys,
cf = this,
sequence = combo.split( ' ' ),
modifiers = [];
// if this pattern is a sequence of keys then run through this method
// to reprocess each pattern one key at a time.
if ( sequence.length > 1 ) {
// bindSequence will call bindSingle again to track progress on each
// individual key in the sequence.
this.bindSequence( combo, sequence, callback, action );
return;
}
// take the keys from this pattern and figure out what the actual
// pattern is all about
keys = combo === '+' ? [ '+' ] : combo.split( '+' );
// More normalization
for ( i = 0; i < keys.length; i++ ) {
key = keys[ i ];
// Aliases for certain names
if ( mapSpecialAliases[ key ] ) {
key = mapSpecialAliases[ key ];
}
// If this is not a keypress event then we should be smart about using shift keys.
// E.g. "shift+1" will never match browers will give keycode for "!" instead.
// This will only work for US keyboards however.
if (action && action !== 'keypress' && mapShiftChars[key]) {
key = mapShiftChars[ key ];
modifiers.push( 'shift' );
}
// if this key is a modifier then add it to the list of modifiers
if ( isModifier( key ) ) {
modifiers.push( key );
}
}
// depending on what the key combo is
// we will try to pick the best event for it
action = guessAction( key, modifiers, action );
// make sure to initialize array if this is the first time
// a callback is added for this key
if ( !cf.callbackList[ key ] ) {
cf.callbackList[ key ] = [];
}
// remove an existing match if there is one
cf.getMatches( key, modifiers, { type: action }, !sequenceName, combo );
// add this call back to the array
// if it is a sequence put it at the beginning
// if not put it at the end
//
// this is important because the way these are processed expects
// the sequence ones to come first
cf.callbackList[ key ][ sequenceName ? 'unshift' : 'push' ]( {
callback: callback,
modifiers: modifiers,
action: action,
seq: sequenceName,
level: level,
combo: combo
} );
};
/*
* Whether this event should be rejected before further processing and
* (eventually) firing off callbacks.
*
* @method
* @param {jQuery.Event} e
* @param {HTMLElement} element
* @param {string} combo
* @return {boolean} Return false to reject, true to keep. For
* compatibility with Mousetrap's isRejected and Mousetrap.stopCallback,
* returning undefined will trigger the 'keep' behavior. Only strict false
* will reject it.
*/
ve.ui.CommandFactory.prototype.filter = function ( e, element ) {
var tag;
// if the element has the class "mousetrap" then no need to stop
if ( $( element ).hasClass( 'mousetrap' ) ) {
return true;
}
tag = element.nodeName && element.nodeName.toLowerCase();
if (
// Reject key events from input contexts.
tag === 'input' || tag === 'select' || tag === 'textarea' ||
( element.contentEditable && element.contentEditable === 'true' )
) {
return false;
}
return true;
};
/**
* Register a command. This is the main method to be used from the outside.
*
* @example
* <code>
* ve.ui.commandFactory.register( 'ctrl+b', fn );
* ve.ui.commandFactory.register( ['a b c', '1 2 3'], fn );
* ve.ui.commandFactory.register( 'a ctrl+b c', fn );
* </code>
*
* @method
* @param {String|String[]} commands Keyboard command or array of keyboard commands.
* A command is either a combo (keys separated by plus sign) or a sequence of keys
* (separated by a space).
*
* NB: Do *NOT* add superfluous spaces around the plus sign, as it will trigger a
* sequence instead of a combination (e.g. 'a + b' will normalize to 'a b', or
* something else unexpected).
*
* NB: Don't use uppercase characters except for single characters like 'A' or 'A B'.
* (e.g. 'ctrl+B' will not work as expected, instead use 'ctrl+b' or 'ctrl+shift+b').
* @param {Function} callback.
* @param {String} action One of 'keypress', 'keydown' or 'keyup'. Default determined
* by `guessAction()`.
*/
ve.ui.CommandFactory.prototype.register = function ( commands, callback, action ) {
if ( commands ) {
if ( !$.isArray( commands ) ) {
commands = [commands];
}
for ( var i = 0; i < commands.length; i++ ) {
this.bindSingle( commands[i], callback, action );
}
}
};
/**
* Internal method for firing a callback function from the callback list.
*
* If your callback function returns false this will apply the same
* behavior as jQuery does: prevent default and stop propogation.
*
* @private
* @method
* @param {Function} callback
* @param {jQuery.Event} e
* @param {string} combo
*/
ve.ui.CommandFactory.prototype.fireCallback = function ( callback, e, combo ) {
// If this event should not happen, stop here.
if ( this.filter( e, e.target, combo ) === false ) {
return;
}
if ( callback( e, combo ) === false ) {
e.preventDefault();
e.stopPropagation();
}
};
/* Initialization */
// TODO: Move instantiation to a different file
ve.ui.commandFactory = new ve.ui.CommandFactory();
}( ve ) );