mediawiki-extensions-Visual.../modules/ve/ve.Surface.js
Trevor Parscal 3142a4e66f Add commands to ve.Surface
First stab at a simple command interface. This and commandFactory
will be refactored significantly before this code is put into
action.

Change-Id: I0de5d3271198c987baf06fb3011aebdc1671f498
2012-10-26 14:50:23 -07:00

372 lines
10 KiB
JavaScript

/**
* VisualEditor Surface class.
*
* @copyright 2011-2012 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* Creates an ve.Surface object.
*
* A surface is a top-level object which contains both a surface model and a surface view.
*
* @class
* @constructor
* @param {String} parent Selector of element to attach to
* @param {HTMLElement} dom HTML element of document to edit
* @param {Object} options Configuration options
*/
ve.Surface = function VeSurface( parent, dom, options ) {
// Properties
this.$ = $( '<div class="ve-surface"></div>' );
this.documentModel = new ve.dm.Document( ve.dm.converter.getDataFromDom( dom ) );
this.options = ve.extendObject( true, ve.Surface.defaultOptions, options );
this.model = new ve.dm.Surface( this.documentModel );
this.view = new ve.ce.Surface( this.$, this.model, this );
this.context = new ve.ui.Context( this );
this.toolbars = {};
this.commands = {};
// DOM Changes
$( parent ).append( this.$ );
this.$.append(
// Contain floating content
$( '<div style="clear: both;"></div>' ),
// Temporary paste buffer
// TODO: make 'paste' in surface stateful and remove this attrib
// TODO: Use a less generic id than "paste", or get rid of the ID alltogether
$( '<div id="paste" class="paste" contenteditable="true"></div>' )
);
// Initialization
this.setupToolbars();
this.addCommands( this.options.commands );
ve.instances.push( this );
this.model.startHistoryTracking();
};
/* Static Members */
ve.Surface.defaultOptions = {
'toolbars': {
'top': {
'tools': [
{ 'name': 'history', 'items' : ['undo', 'redo'] },
{ 'name': 'textStyle', 'items' : ['format'] },
{ 'name': 'textStyle', 'items' : ['bold', 'italic', 'link', 'clear'] },
{ 'name': 'list', 'items' : ['number', 'bullet', 'outdent', 'indent'] }
]
}
},
// Items can either be symbolic names or objects with trigger and action properties
'commands': ['bold', 'italic', 'link', 'undo', 'redo']
};
/**
* Common commands that can be invoked by their symbolic names.
*
* @static
* @member
*/
ve.Surface.commands = {
'bold': {
'trigger': ['command+b', 'control+b'],
'action': ['annotation', 'toggle', 'textStyle/bold']
},
'italic': {
'trigger': ['command+i', 'control+i'],
'action': ['annotation', 'toggle', 'textStyle/italic']
},
'link': {
'trigger': ['command+k', 'control+k'],
'action': ['inspector', 'open', 'link']
},
'undo': {
'trigger': ['command+z', 'control+z'],
'action': ['history', 'undo']
},
'redo': {
'trigger': ['command+shift+z', 'control+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 */
/**
* Gets a reference to the surface model.
*
* @method
* @returns {ve.dm.Surface} Surface model
*/
ve.Surface.prototype.getModel = function () {
return this.model;
};
/**
* Gets a reference to the document model.
*
* @method
* @returns {ve.dm.Document} Document model
*/
ve.Surface.prototype.getDocumentModel = function () {
return this.documentModel;
};
/**
* Gets a reference to the surface view.
*
* @method
* @returns {ve.ce.Surface} Surface view
*/
ve.Surface.prototype.getView = function () {
return this.view;
};
/**
* Gets a reference to the context user interface.
*
* @method
* @returns {ve.ui.Context} Context user interface
*/
ve.Surface.prototype.getContext = function () {
return this.context;
};
/**
* Executes an action.
*
* @method
* @param {String} action Name of action
* @param {String} method Name of method
* @param {Mixed} [...] Additional arguments for action
*/
ve.Surface.prototype.execute = function ( action, method ) {
// Validate method
if ( !ve.actionFactory.doesActionSupportMethod( action, method ) ) {
throw new Error( 'Invalid method: ' + method );
}
// Create an action object and execute the method on it
var obj = ve.actionFactory.create( action, this );
obj[method].apply( obj, Array.prototype.slice.call( arguments, 2 ) );
};
/**
* Adds a link between a keyboard trigger and an action.
*
* @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
*/
ve.Surface.prototype.addCommand = function ( trigger, action ) {
var i, len;
if ( !ve.isArray( trigger ) ) {
trigger = [trigger];
}
for ( i = 0, len = trigger.length; i < len; i++ ) {
this.commands[trigger[i]] = 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] );
}
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' );
}
}
};
/**
* Checks if a given trigger is linked to an action.
*
* @method
* @returns {Boolean} Trigger is linked to an action
*/
ve.Surface.prototype.isCommand = function( trigger ) {
return trigger in this.commands;
};
/**
* Runs a command.
*
* @method
* @param {String} trigger Canonical representation of a keyboard trigger
*/
ve.Surface.prototype.command = function( trigger ) {
if ( trigger in this.commands ) {
this.execute.apply( this, this.commands[trigger] );
}
};
/**
* Initializes the toolbar.
*
* This method uses {this.options} for it's configuration.
*
* @method
*/
ve.Surface.prototype.setupToolbars = function () {
var surface = this;
// Build each toolbar
$.each( this.options.toolbars, function ( name, config ) {
if ( config !== null ) {
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 );
if ( 'float' in config && config.float === true ) {
// Float top toolbar
surface.floatTopToolbar();
}
}
surface.toolbars[name].instance = new ve.ui.Toolbar(
surface.toolbars[name].$,
surface,
config.tools
);
}
} );
};
/*
* Overlays the toolbar to the top of the screen when it would normally be out of view.
*
* TODO: Determine if this would be better in ui.toolbar vs here.
* TODO: This needs to be refactored so that it only works on the main editor top tool bar.
*
* @method
*/
ve.Surface.prototype.floatTopToolbar = function () {
if ( !this.toolbars.top ) {
return;
}
var $wrapper = this.toolbars.top.$wrapper,
$toolbar = this.toolbars.top.$,
$window = $( window );
$window.on( {
'resize': function () {
if ( $wrapper.hasClass( 've-ui-toolbar-wrapper-floating' ) ) {
var toolbarsOffset = $wrapper.offset(),
left = toolbarsOffset.left,
right = $window.width() - $wrapper.outerWidth() - left;
$toolbar.css( {
'left': left,
'right': right
} );
}
},
'scroll': function () {
var left, right,
toolbarsOffset = $wrapper.offset(),
$editorDocument = $wrapper.parent().find('.ve-surface .ve-ce-documentNode'),
$lastBranch = $editorDocument.children( '.ve-ce-branchNode:last' );
if ( $window.scrollTop() > toolbarsOffset.top ) {
left = toolbarsOffset.left;
right = $window.width() - $wrapper.outerWidth() - left;
// If not floating, set float
if ( !$wrapper.hasClass( 've-ui-toolbar-wrapper-floating' ) ) {
$wrapper
.css( 'height', $wrapper.height() )
.addClass( 've-ui-toolbar-wrapper-floating' );
$toolbar.css( {
'left': left,
'right': right
} );
} else {
// Toolbar is floated
if (
// There's at least one branch
$lastBranch.length &&
// Toolbar is at or below the top of last node in the document
$window.scrollTop() + $toolbar.height() >= $lastBranch.offset().top
) {
if ( !$wrapper.hasClass( 've-ui-toolbar-wrapper-bottom' ) ) {
$wrapper
.removeClass( 've-ui-toolbar-wrapper-floating' )
.addClass( 've-ui-toolbar-wrapper-bottom' );
$toolbar.css({
'top': $window.scrollTop() + 'px',
'left': left,
'right': right
});
}
} else { // Unattach toolbar
if ( $wrapper.hasClass( 've-ui-toolbar-wrapper-bottom' ) ) {
$wrapper
.removeClass( 've-ui-toolbar-wrapper-bottom' )
.addClass( 've-ui-toolbar-wrapper-floating' );
$toolbar.css( {
'top': 0,
'left': left,
'right': right
} );
}
}
}
} else { // Return toolbar to top position
if (
$wrapper.hasClass( 've-ui-toolbar-wrapper-floating' ) ||
$wrapper.hasClass( 've-ui-toolbar-wrapper-bottom' )
) {
$wrapper.css( 'height', 'auto' )
.removeClass( 've-ui-toolbar-wrapper-floating' )
.removeClass( 've-ui-toolbar-wrapper-bottom' );
$toolbar.css( {
'top': 0,
'left': 0,
'right': 0
} );
}
}
}
} );
};