%PDF- %PDF-
| Direktori : /www/varak.net/wiki.varak.net/extensions/VisualEditor/lib/ve/src/ui/ |
| Current File : //www/varak.net/wiki.varak.net/extensions/VisualEditor/lib/ve/src/ui/ve.ui.Surface.js |
/*!
* VisualEditor UserInterface Surface class.
*
* @copyright 2011-2016 VisualEditor Team and others; see http://ve.mit-license.org
*/
/**
* A surface is a top-level object which contains both a surface model and a surface view.
*
* @class
* @abstract
* @extends OO.ui.Widget
*
* @constructor
* @param {HTMLDocument|Array|ve.dm.LinearData|ve.dm.Document} dataOrDoc Document data to edit
* @param {Object} [config] Configuration options
* @cfg {jQuery} [$scrollContainer] The scroll container of the surface
* @cfg {ve.ui.CommandRegistry} [commandRegistry] Command registry to use
* @cfg {ve.ui.SequenceRegistry} [sequenceRegistry] Sequence registry to use
* @cfg {ve.ui.DataTransferHandlerFactory} [dataTransferHandlerFactory] Data transfer handler factory to use
* @cfg {string[]|null} [includeCommands] List of commands to include, null for all registered commands
* @cfg {string[]} [excludeCommands] List of commands to exclude
* @cfg {Object} [importRules] Import rules
* @cfg {string} [placeholder] Placeholder text to display when the surface is empty
* @cfg {string} [inDialog] The name of the dialog this surface is in
*/
ve.ui.Surface = function VeUiSurface( dataOrDoc, config ) {
var documentModel;
config = config || {};
// Parent constructor
ve.ui.Surface.super.call( this, config );
// Properties
this.$scrollContainer = config.$scrollContainer || $( this.getElementWindow() );
this.inDialog = config.inDialog || '';
this.globalOverlay = new ve.ui.Overlay( { classes: [ 've-ui-overlay-global' ] } );
this.localOverlay = new ve.ui.Overlay( { classes: [ 've-ui-overlay-local' ] } );
this.$selections = $( '<div>' );
this.$blockers = $( '<div>' );
this.$controls = $( '<div>' );
this.$menus = $( '<div>' );
this.$placeholder = $( '<div>' ).addClass( 've-ui-surface-placeholder' );
this.commandRegistry = config.commandRegistry || ve.ui.commandRegistry;
this.sequenceRegistry = config.sequenceRegistry || ve.ui.sequenceRegistry;
this.dataTransferHandlerFactory = config.dataTransferHandlerFactory || ve.ui.dataTransferHandlerFactory;
this.commands = OO.simpleArrayDifference(
config.includeCommands || this.commandRegistry.getNames(), config.excludeCommands || []
);
this.triggerListener = new ve.TriggerListener( this.commands, this.commandRegistry );
if ( dataOrDoc instanceof ve.dm.Document ) {
// ve.dm.Document
documentModel = dataOrDoc;
} else if ( dataOrDoc instanceof ve.dm.LinearData || Array.isArray( dataOrDoc ) ) {
// LinearData or raw linear data
documentModel = new ve.dm.Document( dataOrDoc );
} else {
// HTMLDocument
documentModel = ve.dm.converter.getModelFromDom( dataOrDoc );
}
this.model = this.createModel( documentModel );
this.view = this.createView( this.model );
this.dialogs = this.createDialogWindowManager();
this.importRules = config.importRules || {};
this.context = this.createContext();
this.progresses = [];
this.showProgressDebounced = ve.debounce( this.showProgress.bind( this ) );
this.filibuster = null;
this.debugBar = null;
this.setPlaceholder( config.placeholder );
this.toolbarHeight = 0;
this.toolbarDialogs = new ve.ui.ToolbarDialogWindowManager( this, {
factory: ve.ui.windowFactory,
modal: false
} );
// Events
this.getView().connect( this, { keyup: 'scrollCursorIntoView' } );
this.getModel().getDocument().connect( this, { transact: 'onDocumentTransact' } );
// Initialization
this.$menus.append( this.context.$element );
this.$element
.addClass( 've-ui-surface' )
.append( this.view.$element );
this.view.$element.after( this.localOverlay.$element );
this.localOverlay.$element.append( this.$selections, this.$blockers, this.$controls, this.$menus );
this.globalOverlay.$element.append( this.dialogs.$element );
};
/* Inheritance */
OO.inheritClass( ve.ui.Surface, OO.ui.Widget );
/* Events */
/**
* When a surface is destroyed.
*
* @event destroy
*/
/**
* The surface was scrolled programmatically
*
* @event scroll
*/
/* Static Properties */
/**
* The surface is for use on mobile devices
*
* @static
* @inheritable
* @property {boolean}
*/
ve.ui.Surface.static.isMobile = false;
/* Methods */
/**
* Destroy the surface, releasing all memory and removing all DOM elements.
*
* @method
* @chainable
* @fires destroy
*/
ve.ui.Surface.prototype.destroy = function () {
// Stop periodic history tracking in model
this.model.stopHistoryTracking();
// Destroy the ce.Surface, the ui.Context and window managers
this.context.destroy();
this.dialogs.destroy();
this.toolbarDialogs.destroy();
this.view.destroy();
if ( this.debugBar ) {
this.debugBar.destroy();
}
// Remove DOM elements
this.$element.remove();
this.globalOverlay.$element.remove();
// Let others know we have been destroyed
this.emit( 'destroy' );
return this;
};
/**
* Initialize surface.
*
* This must be called after the surface has been attached to the DOM.
*
* @chainable
*/
ve.ui.Surface.prototype.initialize = function () {
// Attach globalOverlay to the global <body>, not the local frame's <body>
$( 'body' ).append( this.globalOverlay.$element );
if ( ve.debug ) {
this.setupDebugBar();
}
// The following classes can be used here:
// ve-ui-surface-dir-ltr
// ve-ui-surface-dir-rtl
this.$element.addClass( 've-ui-surface-dir-' + this.getDir() );
this.getView().initialize();
this.getModel().initialize();
return this;
};
/**
* Get the DOM representation of the surface's current state.
*
* @return {HTMLDocument} HTML document
*/
ve.ui.Surface.prototype.getDom = function () {
return ve.dm.converter.getDomFromModel( this.getModel().getDocument() );
};
/**
* Get the HTML representation of the surface's current state.
*
* @return {string} HTML
*/
ve.ui.Surface.prototype.getHtml = function () {
return ve.properInnerHtml( this.getDom().body );
};
/**
* Create a context.
*
* @method
* @abstract
* @return {ve.ui.Context} Context
*/
ve.ui.Surface.prototype.createContext = null;
/**
* Create a dialog window manager.
*
* @method
* @abstract
* @return {ve.ui.WindowManager} Dialog window manager
*/
ve.ui.Surface.prototype.createDialogWindowManager = null;
/**
* Create a surface model
*
* @param {ve.dm.Document} doc Document model
* @return {ve.dm.Surface} Surface model
*/
ve.ui.Surface.prototype.createModel = function ( doc ) {
return new ve.dm.Surface( doc );
};
/**
* Create a surface view
*
* @param {ve.dm.Surface} model Surface model
* @return {ve.ce.Surface} Surface view
*/
ve.ui.Surface.prototype.createView = function ( model ) {
return new ve.ce.Surface( model, this );
};
/**
* Check if the surface is for use on mobile devices
*
* @return {boolean} The surface is for use on mobile devices
*/
ve.ui.Surface.prototype.isMobile = function () {
return this.constructor.static.isMobile;
};
/**
* Set up the debug bar and insert it into the DOM.
*/
ve.ui.Surface.prototype.setupDebugBar = function () {
this.debugBar = new ve.ui.DebugBar( this );
this.$element.append( this.debugBar.$element );
};
/**
* Get the bounding rectangle of the surface, relative to the viewport.
*
* @return {Object|null} Object with top, bottom, left, right, width and height properties.
* Null if the surface is not attached.
*/
ve.ui.Surface.prototype.getBoundingClientRect = function () {
// We would use getBoundingClientRect(), but in iOS7 that's relative to the
// document rather than to the viewport
return this.$element[ 0 ].getClientRects()[ 0 ] || null;
};
/**
* Get vertical measurements of the visible area of the surface viewport
*
* @return {Object|null} Object with top, left, bottom, and height properties. Null if the surface is not attached.
*/
ve.ui.Surface.prototype.getViewportDimensions = function () {
var top, bottom,
rect = this.getBoundingClientRect();
if ( !rect ) {
return null;
}
top = Math.max( this.toolbarHeight - rect.top, 0 );
bottom = $( this.getElementWindow() ).height() - rect.top;
return {
top: top,
left: rect.left,
bottom: bottom,
height: bottom - top
};
};
/**
* Check if editing is enabled.
*
* @deprecated Use #isDisabled
* @method
* @return {boolean} Editing is enabled
*/
ve.ui.Surface.prototype.isEnabled = function () {
return !this.isDisabled();
};
/**
* Get the surface model.
*
* @method
* @return {ve.dm.Surface} Surface model
*/
ve.ui.Surface.prototype.getModel = function () {
return this.model;
};
/**
* Get the surface view.
*
* @method
* @return {ve.ce.Surface} Surface view
*/
ve.ui.Surface.prototype.getView = function () {
return this.view;
};
/**
* Get the context menu.
*
* @method
* @return {ve.ui.Context} Context user interface
*/
ve.ui.Surface.prototype.getContext = function () {
return this.context;
};
/**
* Get dialogs window set.
*
* @method
* @return {ve.ui.WindowManager} Dialogs window set
*/
ve.ui.Surface.prototype.getDialogs = function () {
return this.dialogs;
};
/**
* Get toolbar dialogs window set.
*
* @return {ve.ui.WindowManager} Toolbar dialogs window set
*/
ve.ui.Surface.prototype.getToolbarDialogs = function () {
return this.toolbarDialogs;
};
/**
* Get the local overlay.
*
* Local overlays are attached to the same frame as the surface.
*
* @method
* @return {ve.ui.Overlay} Local overlay
*/
ve.ui.Surface.prototype.getLocalOverlay = function () {
return this.localOverlay;
};
/**
* Get the global overlay.
*
* Global overlays are attached to the top-most frame.
*
* @method
* @return {ve.ui.Overlay} Global overlay
*/
ve.ui.Surface.prototype.getGlobalOverlay = function () {
return this.globalOverlay;
};
/**
* @inheritdoc
*/
ve.ui.Surface.prototype.setDisabled = function ( disabled ) {
if ( disabled !== this.disabled && this.disabled !== null ) {
if ( disabled ) {
this.view.disable();
this.model.disable();
} else {
this.view.enable();
this.model.enable();
}
}
// Parent method
return ve.ui.Surface.super.prototype.setDisabled.call( this, disabled );
};
/**
* Disable editing.
*
* @deprecated Use #setDisabled
* @method
* @chainable
*/
ve.ui.Surface.prototype.disable = function () {
return this.setDisabled( true );
};
/**
* Enable editing.
*
* @deprecated Use #setDisabled
* @method
* @chainable
*/
ve.ui.Surface.prototype.enable = function () {
return this.setDisabled( false );
};
/**
* Handle transact events from the document model
*
* @param {ve.dm.Transaction} Transaction
*/
ve.ui.Surface.prototype.onDocumentTransact = function () {
if ( this.placeholder ) {
this.updatePlaceholder();
}
};
/**
* Scroll the cursor into view.
*
* This is required when the cursor disappears under the floating toolbar.
*/
ve.ui.Surface.prototype.scrollCursorIntoView = function () {
var view, nativeRange, clientRect, cursorTop, scrollTo, toolbarBottom;
if ( !this.toolbarHeight ) {
return;
}
view = this.getView();
nativeRange = view.getNativeRange();
if ( !nativeRange ) {
return;
}
if ( OO.ui.contains( view.$pasteTarget[ 0 ], nativeRange.startContainer, true ) ) {
return;
}
clientRect = RangeFix.getBoundingClientRect( nativeRange );
if ( !clientRect ) {
return;
}
cursorTop = clientRect.top - 5;
toolbarBottom = this.toolbarHeight;
if ( cursorTop < toolbarBottom ) {
scrollTo = this.$scrollContainer.scrollTop() + cursorTop - toolbarBottom;
this.scrollTo( scrollTo );
}
};
/**
* Scroll the scroll container to a specific offset
*
* @param {number} offset Scroll offset
* @fires scroll
*/
ve.ui.Surface.prototype.scrollTo = function ( offset ) {
this.$scrollContainer.scrollTop( offset );
this.emit( 'scroll' );
};
/**
* Set placeholder text
*
* @param {string} [placeholder] Placeholder text, clears placeholder if not set
*/
ve.ui.Surface.prototype.setPlaceholder = function ( placeholder ) {
this.placeholder = placeholder;
if ( this.placeholder ) {
this.$placeholder.prependTo( this.$element );
this.updatePlaceholder();
} else {
this.$placeholder.detach();
}
};
/**
* Update placeholder rendering
*/
ve.ui.Surface.prototype.updatePlaceholder = function () {
var firstNode, $wrapper,
hasContent = this.getModel().getDocument().data.hasContent();
this.$placeholder.toggleClass( 'oo-ui-element-hidden', hasContent );
if ( !hasContent ) {
// Use a clone of the first node in the document so the placeholder
// styling matches the text the users sees when they start typing
firstNode = this.getView().documentView.documentNode.getNodeFromOffset( 1 );
if ( firstNode ) {
$wrapper = firstNode.$element.clone();
if ( ve.debug ) {
// In debug mode a background colour from the render animation may be present
$wrapper.removeAttr( 'style' );
}
} else {
$wrapper = $( '<p>' );
}
this.$placeholder.empty().append( $wrapper.text( this.placeholder ) );
}
};
/**
* Get list of commands available on this surface.
*
* @return {string[]} Commands
*/
ve.ui.Surface.prototype.getCommands = function () {
return this.commands;
};
/**
* Execute an action or command.
*
* @method
* @param {ve.ui.Trigger|string} triggerOrAction Trigger or symbolic name of action
* @param {string} [method] Action method name
* @param {...Mixed} [args] Additional arguments for action
* @return {boolean} Action or command was executed
*/
ve.ui.Surface.prototype.execute = function ( triggerOrAction, method ) {
var command, obj, ret;
if ( this.isDisabled() ) {
return;
}
if ( triggerOrAction instanceof ve.ui.Trigger ) {
command = this.triggerListener.getCommandByTrigger( triggerOrAction.toString() );
if ( command ) {
// Have command call execute with action arguments
return command.execute( this );
}
} else if ( typeof triggerOrAction === 'string' && typeof method === 'string' ) {
// Validate method
if ( ve.ui.actionFactory.doesActionSupportMethod( triggerOrAction, method ) ) {
// Create an action object and execute the method on it
obj = ve.ui.actionFactory.create( triggerOrAction, this );
ret = obj[ method ].apply( obj, Array.prototype.slice.call( arguments, 2 ) );
return ret === undefined || !!ret;
}
}
return false;
};
/**
* Execute a command by name
*
* @param {string} commandName Command name
* @return {boolean} The command was executed
*/
ve.ui.Surface.prototype.executeCommand = function ( commandName ) {
var command = this.commandRegistry.lookup( commandName );
if ( command ) {
return command.execute( this );
}
return false;
};
/**
* Set the current height of the toolbar.
*
* Used for scroll-into-view calculations.
*
* @param {number} toolbarHeight Toolbar height
*/
ve.ui.Surface.prototype.setToolbarHeight = function ( toolbarHeight ) {
this.toolbarHeight = toolbarHeight;
};
/**
* Create a progress bar in the progress dialog
*
* @param {jQuery.Promise} progressCompletePromise Promise which resolves when the progress action is complete
* @param {jQuery|string|Function} label Progress bar label
* @param {boolean} nonCancellable Progress item can't be cancelled
* @return {jQuery.Promise} Promise which resolves with a progress bar widget and a promise which fails if cancelled
*/
ve.ui.Surface.prototype.createProgress = function ( progressCompletePromise, label, nonCancellable ) {
var progressBarDeferred = $.Deferred();
this.progresses.push( {
label: label,
cancellable: !nonCancellable,
progressCompletePromise: progressCompletePromise,
progressBarDeferred: progressBarDeferred
} );
this.showProgressDebounced();
return progressBarDeferred.promise();
};
ve.ui.Surface.prototype.showProgress = function () {
var dialogs = this.dialogs,
progresses = this.progresses;
dialogs.openWindow( 'progress', { progresses: progresses } );
this.progresses = [];
};
/**
* Get sanitization rules for rich paste
*
* @return {Object} Import rules
*/
ve.ui.Surface.prototype.getImportRules = function () {
return this.importRules;
};
/**
* Surface 'dir' property (GUI/User-Level Direction)
*
* @return {string} 'ltr' or 'rtl'
*/
ve.ui.Surface.prototype.getDir = function () {
return this.$element.css( 'direction' );
};
ve.ui.Surface.prototype.initFilibuster = function () {
var surface = this;
this.filibuster = new ve.Filibuster()
.wrapClass( ve.EventSequencer )
.wrapNamespace( ve.dm, 've.dm', [
// blacklist
ve.dm.LinearSelection.prototype.getDescription,
ve.dm.TableSelection.prototype.getDescription,
ve.dm.NullSelection.prototype.getDescription
] )
.wrapNamespace( ve.ce, 've.ce' )
.wrapNamespace( ve.ui, 've.ui', [
// blacklist
ve.ui.Surface.prototype.startFilibuster,
ve.ui.Surface.prototype.stopFilibuster
] )
.setObserver( 'dm doc', function () {
return JSON.stringify( ve.Filibuster.static.clonePlain(
surface.model.documentModel.data.data
) );
} )
.setObserver( 'dm selection', function () {
var selection = surface.model.selection;
if ( !selection ) {
return 'null';
}
return selection.getDescription();
} )
.setObserver( 'DOM doc', function () {
return ve.serializeNodeDebug( surface.view.$element[ 0 ] );
} )
.setObserver( 'DOM selection', function () {
var nativeRange,
nativeSelection = surface.view.nativeSelection;
if ( nativeSelection.rangeCount === 0 ) {
return 'null';
}
nativeRange = nativeSelection.getRangeAt( 0 );
return JSON.stringify( {
startContainer: ve.serializeNodeDebug( nativeRange.startContainer ),
startOffset: nativeRange.startOffset,
endContainer: (
nativeRange.startContainer === nativeRange.endContainer ?
'(=startContainer)' :
ve.serializeNodeDebug( nativeRange.endContainer )
),
endOffset: nativeRange.endOffset
} );
} );
};
ve.ui.Surface.prototype.startFilibuster = function () {
if ( !this.filibuster ) {
this.initFilibuster();
} else {
this.filibuster.clearLogs();
}
this.filibuster.start();
};
ve.ui.Surface.prototype.stopFilibuster = function () {
this.filibuster.stop();
};
/**
* Get the name of the dialog this surface is in
*
* @return {string} The name of the dialog this surface is in
*/
ve.ui.Surface.prototype.getInDialog = function () {
return this.inDialog;
};