Refactored commands into a registry

* Now ve.Factory inherits from the more general ve.Registry
* New class ve.CommandRegistry
* Refactored setupToolbar and command setup code into setupComands

Change-Id: Ic548e5de95b77889727362d3e66d7be83c12a603
This commit is contained in:
Trevor Parscal 2012-11-07 12:09:18 -08:00 committed by Catrope
parent ab57bed7da
commit 443c5438ab
7 changed files with 189 additions and 142 deletions

View file

@ -154,9 +154,11 @@ $wgResourceModules += array(
'ext.visualEditor.core' => $wgVisualEditorResourceTemplate + array(
'scripts' => array(
// ve
've/ve.Registry.js',
've/ve.Factory.js',
've/ve.Position.js',
've/ve.Command.js',
've/ve.CommandRegistry.js',
've/ve.Range.js',
've/ve.Node.js',
've/ve.BranchNode.js',

View file

@ -88,9 +88,11 @@ $html = '<div>' . file_get_contents( $page ) . '</div>';
<!-- Generated by maintenance/makeStaticLoader.php (cont.) -->
<!-- ext.visualEditor.core -->
<script src="../../modules/ve/ve.Registry.js"></script>
<script src="../../modules/ve/ve.Factory.js"></script>
<script src="../../modules/ve/ve.Position.js"></script>
<script src="../../modules/ve/ve.Command.js"></script>
<script src="../../modules/ve/ve.CommandRegistry.js"></script>
<script src="../../modules/ve/ve.Range.js"></script>
<script src="../../modules/ve/ve.Node.js"></script>
<script src="../../modules/ve/ve.BranchNode.js"></script>

View file

@ -25,9 +25,11 @@
<script src="../../ve/init/sa/ve.init.sa.js"></script>
<script src="../../ve/init/sa/ve.init.sa.Platform.js"></script>
<!-- ext.visualEditor.core -->
<script src="../../ve/ve.Registry.js"></script>
<script src="../../ve/ve.Factory.js"></script>
<script src="../../ve/ve.Position.js"></script>
<script src="../../ve/ve.Command.js"></script>
<script src="../../ve/ve.CommandRegistry.js"></script>
<script src="../../ve/ve.Range.js"></script>
<script src="../../ve/ve.Node.js"></script>
<script src="../../ve/ve.BranchNode.js"></script>

View file

@ -0,0 +1,68 @@
/**
* VisualEditor CommandRegistry class.
*
* @copyright 2011-2012 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* Command registry.
*
* @class
* @constructor
* @extends {ve.Registry}
*/
ve.CommandRegistry = function VeCommandRegistry() {
// Parent constructor
ve.Registry.call( this );
};
/* Inheritance */
ve.inheritClass( ve.CommandRegistry, ve.Registry );
/* Methods */
/**
* Register a constructor with the factory.
*
* @method
* @param {String|String[]} name Symbolic name or list of symbolic names
* @param {String|String[]} trigger Command string of keys that should trigger the command
* @param {String} action Action to execute when command is triggered
* @param {String} method Method to call on action when executing
* @param {Mixed} [...] Additional data to pass to the action when executing
* @throws 'trigger must be a string or array'
* @throws 'action must be a string'
* @throws 'method must be a string'
*/
ve.CommandRegistry.prototype.register = function ( name, trigger, action, method ) {
if ( typeof trigger !== 'string' && !ve.isArray( trigger ) ) {
throw new Error( 'trigger must be a string or array, cannot be a ' + typeof trigger );
}
if ( typeof action !== 'string' ) {
throw new Error( 'action must be a string, cannot be a ' + typeof action );
}
if ( typeof method !== 'string' ) {
throw new Error( 'method must be a string, cannot be a ' + typeof method );
}
ve.Registry.prototype.register.call(
this, name, { 'trigger': trigger, 'action': Array.prototype.slice.call( arguments, 2 ) }
);
};
/* Initialization */
ve.commandRegistry = new ve.CommandRegistry();
// TODO: Move these somewhere else
ve.commandRegistry.register(
'bold', ['cmd+b', 'ctrl+b'], 'annotation', 'toggle', 'textStyle/bold'
);
ve.commandRegistry.register(
'italic', ['cmd+i', 'ctrl+i'], 'annotation', 'toggle', 'textStyle/italic'
);
ve.commandRegistry.register( 'link', ['cmd+k', 'ctrl+k'], 'inspector', 'open', 'link' );
ve.commandRegistry.register( 'undo', ['cmd+z', 'ctrl+z'], 'history', 'undo' );
ve.commandRegistry.register( 'redo', ['cmd+shift+z', 'ctrl+shift+z'], 'history', 'redo' );

View file

@ -11,11 +11,11 @@
* @class
* @abstract
* @constructor
* @extends {ve.EventEmitter}
* @extends {ve.Registry}
*/
ve.Factory = function VeFactory() {
// Parent constructor
ve.EventEmitter.call( this );
ve.Registry.call( this );
// Properties
this.registry = [];
@ -23,36 +23,23 @@ ve.Factory = function VeFactory() {
/* Inheritance */
ve.inheritClass( ve.Factory, ve.EventEmitter );
ve.inheritClass( ve.Factory, ve.Registry );
/* Methods */
/**
* Register a constructor with the factory.
*
* Arguments will be passed through directly to the constructor.
* @see {ve.Factory.prototype.create}
*
* @method
* @param {String|String[]} name Symbolic name or list of symbolic names
* @param {Function} constructor Constructor to use when creating object
* @throws 'Constructor must be a function, cannot be a string'
* @throws 'constructor must be a function'
*/
ve.Factory.prototype.register = function ( name, constructor ) {
var i, len;
if ( typeof constructor !== 'function' ) {
throw new Error( 'Constructor must be a function, cannot be a ' + typeof constructor );
}
if ( ve.isArray( name ) ) {
for ( i = 0, len = name.length; i < len; i++ ) {
this.register( name[i], constructor );
}
} else if ( typeof name === 'string' ) {
this.registry[name] = constructor;
this.emit( 'register', name, constructor );
} else {
throw new Error( 'Name must be a string or array of strings, cannot be a ' + typeof name );
throw new Error( 'constructor must be a function, cannot be a ' + typeof constructor );
}
ve.Registry.prototype.register.call( this, name, constructor );
};
/**
@ -72,7 +59,7 @@ ve.Factory.prototype.create = function ( name ) {
constructor = this.registry[name];
if ( constructor === undefined ) {
throw new Error( 'Unknown object name: ' + name );
throw new Error( 'No class registered by that name: ' + name );
}
// Convert arguments to array and shift the first argument (name) off
@ -87,14 +74,3 @@ ve.Factory.prototype.create = function ( name ) {
constructor.apply( obj, args );
return obj;
};
/**
* Gets a constructor for a given name.
*
* @method
* @param {String} name Object name
* @returns {Function|undefined} Constructor for name
*/
ve.Factory.prototype.lookup = function ( name ) {
return this.registry[name];
};

64
modules/ve/ve.Registry.js Normal file
View file

@ -0,0 +1,64 @@
/**
* VisualEditor Registry class.
*
* @copyright 2011-2012 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* Generic object factory.
*
* @class
* @abstract
* @constructor
* @extends {ve.EventEmitter}
*/
ve.Registry = function VeRegistry() {
// Parent constructor
ve.EventEmitter.call( this );
// Properties
this.registry = [];
};
/* Inheritance */
ve.inheritClass( ve.Registry, ve.EventEmitter );
/* Methods */
/**
* Associate one or more symbolic names with some data.
*
* @method
* @param {String|String[]} name Symbolic name or list of symbolic names
* @param {Mixed} data Data to associate with symbolic name
* @throws 'name must be a string'
*/
ve.Registry.prototype.register = function ( name, data ) {
if ( typeof name !== 'string' && !ve.isArray( name ) ) {
throw new Error( 'name must be a string or array, cannot be a ' + typeof name );
}
var i, len;
if ( ve.isArray( name ) ) {
for ( i = 0, len = name.length; i < len; i++ ) {
this.register( name[i], data );
}
} else if ( typeof name === 'string' ) {
this.registry[name] = data;
this.emit( 'register', name, data );
} else {
throw new Error( 'Name must be a string or array of strings, cannot be a ' + typeof name );
}
};
/**
* Gets data for a given symbolic name.
*
* @method
* @param {String} name Symbolic name
* @returns {Mixed|undefined} Data associated with symbolic name
*/
ve.Registry.prototype.lookup = function ( name ) {
return this.registry[name];
};

View file

@ -40,7 +40,7 @@ ve.Surface = function VeSurface( parent, dom, options ) {
// Initialization
this.setupToolbars();
this.addCommands( this.options.commands );
this.setupCommands();
ve.instances.push( this );
this.model.startHistoryTracking();
};
@ -50,6 +50,7 @@ ve.Surface = function VeSurface( parent, dom, options ) {
ve.Surface.defaultOptions = {
'toolbars': {
'top': {
'float': true,
'tools': [
{ 'name': 'history', 'items' : ['undo', 'redo'] },
{ 'name': 'textStyle', 'items' : ['format'] },
@ -62,50 +63,6 @@ ve.Surface.defaultOptions = {
'commands': ['bold', 'italic', 'link', 'undo', 'redo']
};
/**
* Common commands that can be invoked by their symbolic names.
*
* @static
* @member
*/
ve.Surface.commands = {
'bold': {
'trigger': ['cmd+b', 'ctrl+b'],
'action': ['annotation', 'toggle', 'textStyle/bold']
},
'italic': {
'trigger': ['cmd+i', 'ctrl+i'],
'action': ['annotation', 'toggle', 'textStyle/italic']
},
'link': {
'trigger': ['cmd+k', 'ctrl+k'],
'action': ['inspector', 'open', 'link']
},
'undo': {
'trigger': ['cmd+z', 'ctrl+z'],
'action': ['history', 'undo']
},
'redo': {
'trigger': ['cmd+shift+z', 'ctrl+shift+z'],
'action': ['history', 'redo']
}
};
/* Static Methods */
/**
* Adds a command that can be referenced by a symbolic name.
*
* @static
* @method
* @param {String} name Symbolic name of command
* @param {String|String[]} trigger One or more canonical representations of keyboard triggers
* @param {Array} action Array containing the action name, method and additional arguments
*/
ve.Surface.registerCommand = function ( name, trigger, action ) {
ve.Surface.commands[name] = { 'trigger': trigger, 'action': action };
};
/* Methods */
/**
@ -176,53 +133,33 @@ ve.Surface.prototype.execute = function ( action, method ) {
};
/**
* Adds a link between a keyboard trigger and an action.
* Adds all commands from initialization options.
*
* Commands must be registered through {ve.commandRegsitry} prior to constructing the surface.
*
* @method
* @param {String|String[]} trigger One or more canonical representations of keyboard triggers
* @param {Array} action Array containing the action name, method and additional arguments
* @param {String[]} commands Array of symbolic names of registered commands
*/
ve.Surface.prototype.addCommand = function ( triggers, action ) {
var i, len, trigger;
if ( !ve.isArray( triggers ) ) {
triggers = [triggers];
}
for ( i = 0, len = triggers.length; i < len; i++ ) {
// Normalize
trigger = ( new ve.Command( triggers[i] ) ).toString();
// Validate
if ( trigger.length === 0 ) {
throw new Error( 'Incomplete command: ' + triggers[i] );
ve.Surface.prototype.setupCommands = function () {
var i, iLen, j, jLen, triggers, trigger, command,
commands = this.options.commands;
for ( i = 0, iLen = commands.length; i < iLen; i++ ) {
command = ve.commandRegistry.lookup( commands[i] );
if ( !command ) {
throw new Error( 'No command registered by that name: ' + commands[i] );
}
this.commands[trigger] = action;
}
};
/**
* Adds multiple links between a keyboard triggers and an actions.
*
* Each object's trigger and action properties will be passed directly into
* {ve.Surface.prototype.addCommand}.
*
* @method
* @param {String[]|Object[]} commands Array of symbolic names of known commands, or objects that
* each contain a trigger and action property
*/
ve.Surface.prototype.addCommands = function ( commands ) {
var i, len;
for ( i = 0, len = commands.length; i < len; i++ ) {
if ( typeof commands[i] === 'string' ) {
if ( !( commands[i] in ve.Surface.commands ) ) {
throw new Error( 'Unknown command: ' + commands[i] );
triggers = command.trigger;
if ( !ve.isArray( triggers ) ) {
triggers = [triggers];
}
for ( j = 0, jLen = triggers.length; j < jLen; j++ ) {
// Normalize
trigger = ( new ve.Command( triggers[j] ) ).toString();
// Validate
if ( trigger.length === 0 ) {
throw new Error( 'Incomplete command: ' + triggers[j] );
}
this.addCommand(
ve.Surface.commands[commands[i]].trigger,
ve.Surface.commands[commands[i]].action
);
} else if ( ve.isPlainObject( commands[i] ) ) {
this.addCommand( commands[i].trigger, commands[i].action );
} else {
throw new Error( 'Invalid command, must be name of known command or command object' );
this.commands[trigger] = command.action;
}
}
};
@ -235,36 +172,32 @@ ve.Surface.prototype.addCommands = function ( commands ) {
* @method
*/
ve.Surface.prototype.setupToolbars = function () {
var surface = this;
// Build each toolbar
$.each( this.options.toolbars, function ( name, config ) {
if ( config !== null ) {
var name, config, toolbar,
toolbars = this.options.toolbars;
for ( name in toolbars ) {
config = toolbars[name];
if ( ve.isPlainObject( config ) ) {
this.toolbars[name] = toolbar = { '$': $( '<div class="ve-ui-toolbar"></div>' ) };
if ( name === 'top' ) {
surface.toolbars[name] = {
'$wrapper': $( '<div class="ve-ui-toolbar-wrapper"></div>' ),
'$': $( '<div class="ve-ui-toolbar"></div>' )
.append(
$( '<div class="ve-ui-actions"></div>' ),
$( '<div style="clear:both"></div>' ),
$( '<div class="ve-ui-toolbar-shadow"></div>' )
)
};
surface.toolbars[name].$wrapper.append( surface.toolbars[name].$ );
surface.$.before( surface.toolbars[name].$wrapper );
// Add extra sections to the toolbar
toolbar.$.append(
'<div class="ve-ui-actions"></div>' +
'<div style="clear:both"></div>' +
'<div class="ve-ui-toolbar-shadow"></div>'
);
// Wrap toolbar for floating
toolbar.$wrapper = $( '<div class="ve-ui-toolbar-wrapper"></div>' )
.append( this.toolbars[name].$ );
// Add toolbar to surface
this.$.before( toolbar.$wrapper );
if ( 'float' in config && config.float === true ) {
// Float top toolbar
surface.floatTopToolbar();
this.floatTopToolbar();
}
}
surface.toolbars[name].instance = new ve.ui.Toolbar(
surface.toolbars[name].$,
surface,
config.tools
);
toolbar.instance = new ve.ui.Toolbar( toolbar.$, this, config.tools );
}
} );
}
};
/*