mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-12-01 17:36:35 +00:00
7cec9ae04a
Allow pasting of rich (HTML) content. ve.ce.Surface * Use a sliced document clone for converting to DM HTML (copy) * Add full context to pasteTarget before copying * Add ve-pasteProtect class to spans to prevent them being dropped * Implement external paste by converting HTML to data and inserting with newFromDocumentInsertion * Remove clipboard key placeholder after read so they aren't picked up by rich paste. Hash no longer includes the placeholder. * Detect the corruption of important spans and fallback to clipboard data HTML if available. ve.dm.LinearData * Add clone method for copy ve.dm.ElementLinearData * Add compareUnannotated for use by context diffing. * Add sanitize method for cleaning data according to a set of rules. ve.dm.Transaction * Add range parameter for inserting a range of a document only, e.g. stripping the paste context. ve.dm.Document * Implement sliced document clone creation so that DM HTML is generated correctly in onCopy ve.dm.DocumentSlice * Replaces LinearDataSlice. Now has two ranges for balanced data and data with a full context. ve.init.Target.js * Define default, loose, paste rules (just remove aliens). ve.init.mw.ViewPageTarget * Define strict MW paste rules: + no links, spans, underlines + no images, divs, aliens + strip extra HTML attribues ve.init.sa.Target, ve.init.mw.ViewPageTarget, ve.ui.Surface * Pass through and store paste rules. Bug: 41193 Bug: 48170 Bug: 50128 Bug: 53828 Change-Id: I38d63e31ee3e3ee11707e3fffed5174e1d633b42
307 lines
7.8 KiB
JavaScript
307 lines
7.8 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 ) {
|
|
// 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>' );
|
|
this.model = new ve.dm.Surface(
|
|
dataOrDoc instanceof ve.dm.Document ? dataOrDoc : new ve.dm.Document( dataOrDoc )
|
|
);
|
|
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' );
|
|
};
|