mediawiki-extensions-Visual.../modules/ve/ui/ve.ui.Context.js
Trevor Parscal f31dc45da8 (bug 43841) Major ve.ui rewrite, especially ve.ui.LinkInspector
Objectives:

* Make the link inspector easier to use
* Try to resolve a few bugs (bug 43841, bug 43063, bug 42986)
* Stop using jquery.multiSuggest (which didn't really understand annotations)
* Better divide MediaWiki specifics from generic implementations

Changes:

VisualEditor.php, modules/ve/test/index.php, demos/ve/index.php
* Updated links to files

ve.Registry
* Fixed mistake where registry was initialized as an array - this didn't cause any errors because you can add arbitrary properties to an array and use it like any other object

ve.Factory
* Removed duplicate initialization of registry property
* Added entries property, which is an array that's appended to for tracking the order of registrations

ve.CommandRegistry
* Added mwLink command which opens the mwLink inspector

ve.ui.TextInputWidget
* Added basic widget class for text inputs

ve.ui.TextInputMenuWidget
* Added widget that provides a menu of options for a text input widget

ve.ui.MWLinkTargetInputWidget
* Added MediaWiki specific link target widget

ve.ui.MenuWidget
* Converted ve.ui.Menu into a widget
* Moved the body of onSelect to onMouseUp

ve.ui.LinkTargetInputWidget
* Added link target widget which adds link annotation functionality to a normal text input

ve.ui.InputWidget
* Added generic input widget which emits reliable and instant change events and synchronizes a value property with the DOM value

ve.ui.Widget
* Added base widget class
* Widgets can be used in any frame

ve.ui.Tool
* Fixed line length issues

ve.ui.InspectorFactory
* Made use of new entries property for factories to select the most recently added inspector if more than one match a given annotation

ve.ui.Inspector
* Added auto-focus on the first visible input element on open
* Moved afterClose event to after re-focus on document on close
* Added documentation

ve.ui.Frame
* Adjusted documentation
* Added binding of $$ to the frame context so it can be passed around
* Added documentation

ve.ui.Context
* Added ve.ui.Widget.css to iframes
* Updated code as per moving of ve.ui.Menu to ve.ui.MenuWidget
* Removed unused positionBelowOverlay method
* Added CSS settings to set overlay left and width properties according to context size
* Added documentation

ve.ui.DropdownTool
* Updated code as per moving of ve.ui.Menu to ve.ui.MenuWidget

ve.ui.FormatDropdownTool
* Added documentation

ve.ui.MWLinkButtonTool
* Added MediaWiki specific version of ve.ui.LinkButtonTool, which opens the mwLink inspector

ve.ui.Widget.css
* Added styles for all widgets

ve.ui.Tool.css, ve.init.sa.css, ve.init.mw.ViewPageTarget.css, ve.init.mw.ViewPageTarget-apex.css
* Updated code as per moving of ve.ui.Menu to ve.ui.MenuWidget

ve.ui.Menu.css
* Deleted (merged into ve.ui.Widget.css)

ve.ui.Menu.css
* Deleted suggest styles (no longer used)

pending.gif, pending.psd
* Added diagonal stripe animation to indicate a pending request to the API

ve.ui.MWLinkInspector
* Added MediaWiki specific inspector which uses MediaWiki specific annotations and widgets

ve.ui.LinkInspector
* Removed mw global hint (not needed anymore)
* Switched from comparing targets to annotations (since the target text is ambiguous in some situations)
* Switched to using input widget, which is configured using a static property
* Removed use of jquery.multiSuggest
* Moved MediaWiki specifics to their own class (ve.ui.MWLinkInspector)

ve.init.mw.ViewPageTarget
* Added MediaWiki specific toolbar and command options

Change-Id: I859b5871a9d2f17d970c002067c8ff24f3513e9f
2013-01-15 15:05:11 -08:00

383 lines
9.3 KiB
JavaScript

/*!
* VisualEditor user interface Context class.
*
* @copyright 2011-2012 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* Creates an ve.ui.Context object.
*
* @class
* @constructor
* @param surface
* @param {jQuery} $overlay DOM selection to add nodes to
*/
ve.ui.Context = function VeUiContext( surface, $overlay ) {
// Properties
this.surface = surface;
this.inspectors = {};
this.inspector = null;
this.position = null;
this.visible = false;
this.selecting = false;
this.selection = null;
this.frame = null;
this.menu = null;
this.toolbar = null;
this.$ = $( '<div class="ve-ui-context"></div>' );
this.$callout = $( '<div class="ve-ui-context-callout"></div>' );
this.$inner = $( '<div class="ve-ui-context-inner"></div>' );
this.$overlay = $( '<div class="ve-ui-context-frame-overlay"></div>' );
this.$menu = $( '<div class="ve-ui-context-menu"></div>' );
this.$inspectors = $( '<div class="ve-ui-context-inspectors"></div>' );
// Initialization
this.$.append( this.$callout, this.$inner, this.$overlay );
this.$inner.append( this.$menu, this.$inspectors );
( $overlay || $( 'body' ) ).append( this.$ );
this.frame = new ve.ui.Frame( this.constructor.static.frameOptions, this.$inspectors );
// Events
this.surface.getModel().addListenerMethods( this, { 'change': 'onChange' } );
this.surface.getView()
.addListenerMethods( this, {
'selectionStart': 'onSelectionStart',
'selectionEnd': 'onSelectionEnd'
} );
$( window ).on( {
'resize': ve.bind( this.update, this ),
'focus': ve.bind( this.onWindowFocus, this )
} );
};
/* Static Members */
ve.ui.Context.static = {};
/**
* Options for frame object.
*
* @static
* @property
* @type {Object}
* @see ve.ui.Frame
*/
ve.ui.Context.static.frameOptions = {
'stylesheets': [
ve.init.platform.getModulesUrl() + '/ve/ui/styles/ve.ui.Widget.css',
ve.init.platform.getModulesUrl() + '/ve/ui/styles/ve.ui.Inspector.css',
ve.init.platform.getModulesUrl() + (
window.devicePixelRatio > 1 ?
'/ve/ui/styles/ve.ui.Inspector.Icons-vector.css' :
'/ve/ui/styles/ve.ui.Inspector.Icons-raster.css'
)
]
};
/* Methods */
/**
* Responds to change events on the model.
*
* Changes are ignored while the user is selecting text.
*
* @method
* @param {ve.dm.Transaction} tx Change transaction
* @param {ve.Range} selection Change selection
*/
ve.ui.Context.prototype.onChange = function ( tx, selection ) {
if ( selection && selection.start === 0 ) {
return;
}
if ( selection && !this.selecting ) {
this.update();
}
};
/**
* Responds to selection start events on the view.
*
* @method
*/
ve.ui.Context.prototype.onSelectionStart = function () {
this.selecting = true;
this.hide();
};
/**
* Responds to selection end events on the view.
*
* @method
*/
ve.ui.Context.prototype.onSelectionEnd = function () {
this.selecting = false;
this.update();
};
ve.ui.Context.prototype.onWindowFocus = function () {
this.hide();
};
/**
* Responds to an inspector being opened.
*
* @method
* @param {string} name Name of inspector being opened (this is not part of the normal event, it's
* mixed in when we bound to the event in {initInspector})
*/
ve.ui.Context.prototype.onBeforeInspectorOpen = function ( name ) {
var inspector = this.inspectors[name];
// Close menu
if ( this.menu ) {
this.obscure( this.$menu );
}
// Remember which inspector is open
this.inspector = name;
// Resize frame to the size of the inspector.
this.frame.setSize( inspector.$.outerWidth(), inspector.$.outerHeight() );
// Cache selection, in the case of manually opened inspector.
this.selection = this.surface.getModel().getSelection();
// Show context
this.show();
};
/**
* Responds to an inspector being closed.
*
* @method
* @param {string} name Name of inspector being closed
* @param {boolean} remove Annotation should be removed
*/
ve.ui.Context.prototype.onAfterInspectorClose = function () {
this.obscure( this.$inspectors );
this.inspector = null;
this.hide();
this.update();
};
/**
* Gets the surface this context is being used in.
*
* @method
* @returns {ve.Surface} Surface of context
*/
ve.ui.Context.prototype.getSurface = function () {
return this.surface;
};
/**
* Gets the frame that inspectors are being rendered in.
*
* @method
* @returns {ve.ui.Frame} Inspector frame
*/
ve.ui.Context.prototype.getFrame = function () {
return this.frame;
};
/**
* Destroy the context, removing all DOM elements.
*
* @method
* @returns {ve.ui.Context} Context user interface
*/
ve.ui.Context.prototype.destroy = function () {
this.$.remove();
};
/**
* Updates the context menu.
*
* @method
*/
ve.ui.Context.prototype.update = function () {
var inspectors,
fragment = this.surface.getModel().getFragment(),
selection = fragment.getRange(),
annotations = fragment.getAnnotations();
// Update the inspector if the change didn't affect the selection
if ( this.inspector && selection.equals( this.selection ) ) {
this.show();
} else {
this.hide();
}
if ( !this.inspector ) {
inspectors = ve.ui.inspectorFactory.getInspectorsForAnnotations( annotations );
if ( inspectors.length ) {
// The selection is inspectable but not being inspected
this.$menu.empty();
// Create inspector toolbar
this.toolbar = new ve.ui.Toolbar(
$( '<div class="ve-ui-context-toolbar"></div>' ),
this.surface,
[{ 'name': 'inspectors', 'items' : inspectors }]
);
// Note: Menu attaches the provided $tool element to the container.
this.menu = new ve.ui.MenuWidget(
[ { 'name': 'tools', '$': this.toolbar.$ } ], // Tools
null, // Callback
this.$menu, // Container
this.$inner // Parent
);
if ( !this.visible ) {
this.show();
}
}
}
// Remember selection for next time
this.selection = selection.clone();
};
/**
* Shows the context menu.
*
* @method
*/
ve.ui.Context.prototype.show = function () {
var selectionRect = this.surface.getView().getSelectionRect();
this.position = { 'left': selectionRect.end.x, 'top': selectionRect.end.y };
this.$.css( this.position );
// Show context
this.$.css( 'visibility', 'visible' );
if ( this.inspector ) {
// Reveal inspector
this.reveal( this.$inspectors );
this.$overlay.show();
} else {
if ( !this.visible ) {
// Fade in the context.
this.$.fadeIn( 'fast' );
this.visible = true;
}
// Reveal menu
this.reveal( this.$menu );
}
// Position inner context.
this.positionInner();
};
/**
* Hides the context menu.
*
* @method
*/
ve.ui.Context.prototype.hide = function () {
if ( this.inspector ) {
this.closeInspector();
this.$overlay.hide();
}
if ( this.menu ) {
this.obscure( this.$menu );
this.menu = null;
}
this.$.css( 'visibility', 'hidden' );
this.visible = false;
};
/**
* Positions the context
*
* @param {jQuery} $overlay
* @param {jQuery} $element
*/
ve.ui.Context.prototype.positionInner = function () {
var $container = this.inspector ? this.$inspectors : this.$menu,
width = $container.outerWidth( true ),
height = $container.outerHeight( true ),
left = -( width / 2 );
// Clamp on left boundary
if ( this.position.left < width / 2 ) {
left = -( this.$.children().outerWidth( true ) / 2 ) - ( this.position.left / 2 );
// Clamp on right boundary
} else if ( $( 'body' ).width() - this.position.left < width ) {
left = -( width - ( ( $( 'body' ).width() - this.position.left ) / 2) );
}
// Apply dimensions to inner
this.$inner.css( { 'left': left, 'height': height, 'width': width } );
this.$overlay.css( { 'left': left, 'width': width } );
};
/**
* Initializes a given inspector.
*
* @method
* @param {string} name Symbolic name of inspector
* @returns {boolean} Inspector had to be created
*/
ve.ui.Context.prototype.initInspector = function ( name ) {
var inspector;
// Add inspector on demand.
if ( ve.ui.inspectorFactory.lookup( name ) ) {
if ( !( name in this.inspectors ) ) {
inspector = this.inspectors[name] = ve.ui.inspectorFactory.create( name, this );
inspector.on( 'beforeOpen', ve.bind( this.onBeforeInspectorOpen, this, name ) );
inspector.on( 'afterClose', ve.bind( this.onAfterInspectorClose, this ) );
inspector.$.hide();
this.frame.$.append( inspector.$ );
this.obscure( this.$inspectors );
}
return true;
}
return false;
};
/**
* Opens a given inspector.
*
* @method
* @param {string} name Symbolic name of inspector
*/
ve.ui.Context.prototype.openInspector = function ( name ) {
// Auto-initialize the inspector
if ( !this.initInspector( name ) ) {
throw new Error( 'Missing inspector. Cannot open nonexistent inspector: ' + name );
}
// Only allow one inspector open at a time
if ( this.inspector ) {
this.closeInspector();
}
// Open the inspector
this.inspectors[name].open();
};
/**
* Closes currently open inspector.
*
* @method
* @param {boolean} remove Remove annotation while closing
*/
ve.ui.Context.prototype.closeInspector = function ( remove ) {
// Quietly ignore if nothing is open
if ( this.inspector ) {
// Close the current inspector
this.inspectors[this.inspector].close( remove );
}
};
/**
* Brings inspector into view.
*
* @method
*/
ve.ui.Context.prototype.reveal = function ( $element ) {
$element.css( 'top', 0 );
};
/**
* Make inspector invisible without affecting it's visiblity or display properties.
*
* @method
*/
ve.ui.Context.prototype.obscure = function ( $element ) {
$element.css( 'top', -5000 );
};