mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-11-25 14:56:20 +00:00
6d34e344ee
Following on from getDomFromModel, this returns a document model instead of element linear data. The only instance that hasn't been replaced is in rich paste, where we need to sanitize the converted data before constructing the document model. This should be cleaned up in a later commit. Change-Id: I37a2b641632af2cb515e3409deed5cd1fa358af5
317 lines
8.1 KiB
JavaScript
317 lines
8.1 KiB
JavaScript
/*!
|
|
* VisualEditor UserInterface Surface class.
|
|
*
|
|
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
|
|
* @license The MIT License (MIT); see LICENSE.txt
|
|
*/
|
|
|
|
/**
|
|
* A surface is a top-level object which contains both a surface model and a surface view.
|
|
*
|
|
* @class
|
|
* @extends OO.ui.Element
|
|
*
|
|
* @constructor
|
|
* @param {HTMLDocument|Array|ve.dm.LinearData|ve.dm.Document} dataOrDoc Document data to edit
|
|
* @param {Object} [config] Configuration options
|
|
*/
|
|
ve.ui.Surface = function VeUiSurface( dataOrDoc, config ) {
|
|
var documentModel;
|
|
|
|
// Parent constructor
|
|
OO.ui.Element.call( this, config );
|
|
|
|
// Mixin constructor
|
|
OO.EventEmitter.call( this, config );
|
|
|
|
// Properties
|
|
this.$globalOverlay = this.$( '<div>' );
|
|
this.$localOverlay = this.$( '<div>' );
|
|
this.$localOverlayBlockers = this.$( '<div>' );
|
|
this.$localOverlayControls = this.$( '<div>' );
|
|
this.$localOverlayMenus = this.$( '<div>' );
|
|
if ( dataOrDoc instanceof ve.dm.Document ) {
|
|
// ve.dm.Document
|
|
documentModel = dataOrDoc;
|
|
} else if ( dataOrDoc instanceof ve.dm.LinearData || ve.isArray( dataOrDoc ) ) {
|
|
// LinearData or raw linear data
|
|
documentModel = new ve.dm.Document( dataOrDoc );
|
|
} else {
|
|
// HTMLDocument
|
|
documentModel = ve.dm.converter.getModelFromDom( dataOrDoc );
|
|
}
|
|
this.model = new ve.dm.Surface( documentModel );
|
|
this.view = new ve.ce.Surface( this.model, this, { '$': this.$ } );
|
|
this.context = new ve.ui.Context( this, { '$': this.$ } );
|
|
this.dialogs = new ve.ui.WindowSet( this, ve.ui.dialogFactory, { '$': this.$ } );
|
|
this.commands = {};
|
|
this.triggers = {};
|
|
this.pasteRules = {};
|
|
this.enabled = true;
|
|
|
|
// Initialization
|
|
this.$element
|
|
.addClass( 've-ui-surface' )
|
|
.append( this.view.$element );
|
|
this.$localOverlay
|
|
.addClass( 've-ui-surface-overlay ve-ui-surface-overlay-local' )
|
|
.append( this.$localOverlayBlockers )
|
|
.append( this.$localOverlayControls )
|
|
.append( this.$localOverlayMenus );
|
|
this.$localOverlayMenus
|
|
.append( this.context.$element );
|
|
this.$globalOverlay
|
|
.addClass( 've-ui-surface-overlay ve-ui-surface-overlay-global' )
|
|
.append( this.dialogs.$element );
|
|
|
|
// Make instance globally accessible for debugging
|
|
ve.instances.push( this );
|
|
};
|
|
|
|
/* Inheritance */
|
|
|
|
OO.inheritClass( ve.ui.Surface, OO.ui.Element );
|
|
|
|
OO.mixinClass( ve.ui.Surface, OO.EventEmitter );
|
|
|
|
/* Events */
|
|
|
|
/**
|
|
* When the surface changes its position (only if it happens
|
|
* after initialize has already been called).
|
|
*
|
|
* @event position
|
|
*/
|
|
|
|
/**
|
|
* When a command is added to the surface.
|
|
*
|
|
* @event addCommand
|
|
* @param {string} name Symbolic name of command and trigger
|
|
* @param {ve.ui.Command} command Command that's been registered
|
|
* @param {ve.ui.Trigger} trigger Trigger to associate with command
|
|
*/
|
|
|
|
/* Methods */
|
|
|
|
/**
|
|
* Initialize surface.
|
|
*
|
|
* This must be called after the surface has been attached to the DOM.
|
|
*/
|
|
ve.ui.Surface.prototype.initialize = function () {
|
|
this.view.$element.after( this.$localOverlay );
|
|
this.$( 'body' ).append( this.$globalOverlay );
|
|
|
|
this.view.initialize();
|
|
// By re-asserting the current selection and forcing a poll we force selection to be something
|
|
// reasonable - otherwise in Firefox, the initial selection is (0,0), causing bug 42277
|
|
this.model.getFragment().select();
|
|
this.view.surfaceObserver.pollOnce();
|
|
this.model.startHistoryTracking();
|
|
};
|
|
|
|
/**
|
|
* Check if editing is enabled.
|
|
*
|
|
* @method
|
|
* @returns {boolean} Editing is enabled
|
|
*/
|
|
ve.ui.Surface.prototype.isEnabled = function () {
|
|
return this.enabled;
|
|
};
|
|
|
|
/**
|
|
* Get the surface model.
|
|
*
|
|
* @method
|
|
* @returns {ve.dm.Surface} Surface model
|
|
*/
|
|
ve.ui.Surface.prototype.getModel = function () {
|
|
return this.model;
|
|
};
|
|
|
|
/**
|
|
* Get the surface view.
|
|
*
|
|
* @method
|
|
* @returns {ve.ce.Surface} Surface view
|
|
*/
|
|
ve.ui.Surface.prototype.getView = function () {
|
|
return this.view;
|
|
};
|
|
|
|
/**
|
|
* Get the context menu.
|
|
*
|
|
* @method
|
|
* @returns {ve.ui.Context} Context user interface
|
|
*/
|
|
ve.ui.Surface.prototype.getContext = function () {
|
|
return this.context;
|
|
};
|
|
|
|
/**
|
|
* Get dialogs window set.
|
|
*
|
|
* @method
|
|
* @returns {OO.ui.WindowSet} Dialogs window set
|
|
*/
|
|
ve.ui.Surface.prototype.getDialogs = function () {
|
|
return this.dialogs;
|
|
};
|
|
|
|
/**
|
|
* Get list of commands keyed by trigger string.
|
|
*
|
|
* @method
|
|
* @returns {Object.<string,ve.ui.Command>} Commands
|
|
*/
|
|
ve.ui.Surface.prototype.getCommands = function () {
|
|
return this.commands;
|
|
};
|
|
|
|
/**
|
|
* Get list of triggers keyed by symbolic name.
|
|
*
|
|
* @method
|
|
* @returns {Object.<string,ve.ui.Trigger>} Triggers
|
|
*/
|
|
ve.ui.Surface.prototype.getTriggers = function () {
|
|
return this.triggers;
|
|
};
|
|
|
|
/**
|
|
* Destroy the surface, releasing all memory and removing all DOM elements.
|
|
*
|
|
* @method
|
|
* @returns {ve.ui.Context} Context user interface
|
|
*/
|
|
ve.ui.Surface.prototype.destroy = function () {
|
|
ve.instances.splice( ve.instances.indexOf( this ), 1 );
|
|
this.view.destroy();
|
|
this.$element.remove();
|
|
this.$globalOverlay.remove();
|
|
this.$localOverlay.remove();
|
|
};
|
|
|
|
/**
|
|
* Disable editing.
|
|
*
|
|
* @method
|
|
*/
|
|
ve.ui.Surface.prototype.disable = function () {
|
|
this.view.disable();
|
|
this.model.disable();
|
|
this.enabled = false;
|
|
};
|
|
|
|
/**
|
|
* Enable editing.
|
|
*
|
|
* @method
|
|
*/
|
|
ve.ui.Surface.prototype.enable = function () {
|
|
this.enabled = true;
|
|
this.view.enable();
|
|
this.model.enable();
|
|
};
|
|
|
|
/**
|
|
* Execute an action or command.
|
|
*
|
|
* @method
|
|
* @param {ve.ui.Trigger|string} action Trigger or symbolic name of action
|
|
* @param {string} [method] Action method name
|
|
* @param {Mixed...} [args] Additional arguments for action
|
|
* @returns {boolean} Action or command was executed
|
|
*/
|
|
ve.ui.Surface.prototype.execute = function ( action, method ) {
|
|
var trigger, obj, ret;
|
|
|
|
if ( !this.enabled ) {
|
|
return;
|
|
}
|
|
|
|
if ( action instanceof ve.ui.Trigger ) {
|
|
// Lookup command by trigger
|
|
trigger = action.toString();
|
|
if ( trigger in this.commands ) {
|
|
// Have command call execute with action arguments
|
|
return this.commands[trigger].execute( this );
|
|
}
|
|
} else if ( typeof action === 'string' && typeof method === 'string' ) {
|
|
// Validate method
|
|
if ( ve.ui.actionFactory.doesActionSupportMethod( action, method ) ) {
|
|
// Create an action object and execute the method on it
|
|
obj = ve.ui.actionFactory.create( action, this );
|
|
ret = obj[method].apply( obj, Array.prototype.slice.call( arguments, 2 ) );
|
|
return ret === undefined || !!ret;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Add all commands from initialization options.
|
|
*
|
|
* Commands and triggers must be registered under the same name prior to adding them to the surface.
|
|
*
|
|
* @method
|
|
* @param {string[]} names List of symbolic names of commands in the command registry
|
|
* @throws {Error} If command has not been registered
|
|
* @throws {Error} If trigger has not been registered
|
|
* @throws {Error} If trigger is not complete
|
|
*/
|
|
ve.ui.Surface.prototype.addCommands = function ( names ) {
|
|
var i, len, key, command, trigger;
|
|
|
|
for ( i = 0, len = names.length; i < len; i++ ) {
|
|
command = ve.ui.commandRegistry.lookup( names[i] );
|
|
if ( !command ) {
|
|
throw new Error( 'No command registered by that name: ' + names[i] );
|
|
}
|
|
// Normalize trigger key
|
|
trigger = ve.ui.triggerRegistry.lookup( names[i] );
|
|
if ( !trigger ) {
|
|
throw new Error( 'No trigger registered by that name: ' + names[i] );
|
|
}
|
|
key = trigger.toString();
|
|
// Validate trigger
|
|
if ( key.length === 0 ) {
|
|
throw new Error( 'Incomplete trigger: ' + trigger );
|
|
}
|
|
this.commands[key] = command;
|
|
this.triggers[names[i]] = trigger;
|
|
this.emit( 'addCommand', names[i], command, trigger );
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get sanitization rules for rich paste
|
|
*
|
|
* @returns {Object} Paste rules
|
|
*/
|
|
ve.ui.Surface.prototype.getPasteRules = function () {
|
|
return this.pasteRules;
|
|
};
|
|
|
|
/**
|
|
* Set sanitization rules for rich paste
|
|
*
|
|
* @param {Object} pasteRules Paste rules
|
|
* @param {string[]} [pasteRules.blacklist] Blacklist of model types which aren't allowed
|
|
* @param {boolean} [pasteRules.removeHtmlAttributes] Remove all left over HTML attributes
|
|
*/
|
|
ve.ui.Surface.prototype.setPasteRules = function ( pasteRules ) {
|
|
this.pasteRules = pasteRules;
|
|
};
|
|
|
|
/**
|
|
* Surface 'dir' property (GUI/User-Level Direction)
|
|
* @returns {string} 'ltr' or 'rtl'
|
|
*/
|
|
ve.ui.Surface.prototype.getDir = function () {
|
|
return this.$element.css( 'direction' );
|
|
};
|