mediawiki-extensions-Visual.../modules/ve/ui/ve.ui.Context.js
Trevor Parscal a5aeca3ff5 Major UI refactoring and improvements
Objective:

Refactor UI widgets, improve usability and accessibility of menus, general cleanup and style improvements.

Extras:
Fixed documentation in a few other files to make descriptions of jQuery event arguments more consistent, classes inherit correctly, and made use of the @cfg functionality in jsduck.

Changes:

.docs/config.json
* Added window, HTMLDocument, HTMLElement, DocumentFragment and XMLHttpRequest to externals, so jsduck doesn't throw warnings when they are used

demos/ve/index.php, modules/ve/test/index.php, VisualEditor.php
* Moved widgets above tools (since tools use widgets)

demos/ve/index.php
* Refactored widget initialization to use options
* Renamed variables to match widget names

ve.init.mw.ViewPageTarget.css
* Adjusted text sizes to make widgets work normally
* Added margins for buttons in toolbar (since button widgets
don't have any)
* Removed styles for init buttons (button widgets now)

ve.init.mw.ViewPageTarget.js
* Switched to using button widgets (involved moving things around
a bit)

ve.ui.LinkInspector.js, ve.ui.MWLinkInspector.js
* Renamed static property "inputWidget" to
"linkTargetInputWidget" to better reflect the required base class
for the properties value

icons.ai, check.png, check.svg
* Added "check" icon, used in menu right now to show which item
is selected

ve.ui.Icons-raster.css, ve.ui.Icons-vector.css
* Added check icon
* Removed :before pseudo selectors from most of the icon classes (not need by button tool anymore, makes them more reusable now)

ve.ui.Tool.css
* Adjusted drop down tool styles so menu appears below, instead
of on top, of the label
* Adjusted paragraph font size to better match actual content
* Updated class names to still work with menu widget changes
(items are their own widgets now)
* Updated selectors as per changes in the structure of button tools

ve.ui.Widget.css
* Added styles for buttons and menu items
* Adjusted menu styles

ve.ui.*ButtonTool.js
* Added config options argument passthrough

ve.ui.ButtonTool.js
* Moved var statement to the top inside constructor
* Switched to using "a" tag to get cross-browser :active support
* Added icon to inside of button to make icon styles more reusable
* Removed disabled support (now provided by widget parent class)

ve.ui.FormatDropDownTool.js
* Updated options initialization to construct menu item objects
* Modified handling of items to account for changes in menu and
item classes
* Optimized onUpdateState method a bit, adding early exit to
inner loop

ve.ui.ButtonTool.js, ve.ui.DropdownTool.js, ve.ui.Context.js,
ve.ui.Frame, ve.ui.Tool.js, ve.ui.Widget.js
* Added chain ability to non-getter methods

ve.ui.DropdownTool.js
* Removed items argument to constructor
* Updated code as per changes in menu class
* Fixed inconsistent naming of event handler methods
* Removed item event handling (now handled by items directly)
* Made use of this.$$ to ensure tool works in other frames

ve.ui.Tool.js
* Made tools inherit from widget
* Moved trigger registry event handler to a method

ve.ui.Context.js
* Switched from using menu to contain toolbar to a simple wrapper

ve.ui.js
* Added get$$ method, a convenience function for binding jQuery
to a specific document context

ve.ui.*Widget.js
* Switched to using a config options object instead of individual arguments
* Added options
* Factored out flags and labels into their own classes
* Refactored value setting methods for inputs

ve.ui.MenuWidget.js, ve.ui.MenuItemWidget.js
* Broke items out into their own classes
* Redesigned API
* Updated code that uses these classes
* Added support for keyboard interaction
* Made items flash when selected (delaying the hiding of the menu for 200ms)

ve.ui.LinkTargetInputWidget.js, ve.ui.MWLinkTargetInputWidget
* Refactored annotation setting methods

Change-Id: I7769bd5a5b79f1ab36f258ef9f2be583ca503ce6
2013-02-26 12:29:08 -08:00

405 lines
9.3 KiB
JavaScript

/*!
* VisualEditor UserInterface Context class.
*
* @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
* @license The MIT License (MIT); see LICENSE.txt
*/
/**
* UserInterface context.
*
* @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.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 Properties */
/**
* @static
* @property
* @inheritable
*/
ve.ui.Context.static = {};
/**
* Options for iframe.
*
* @see ve.ui.Frame
*
* @static
* @property
* @type {Object}
*/
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 */
/**
* Handle 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();
}
};
/**
* Handle selection start events on the view.
*
* @method
*/
ve.ui.Context.prototype.onSelectionStart = function () {
this.selecting = true;
this.hide();
};
/**
* Handle selection end events on the view.
*
* @method
*/
ve.ui.Context.prototype.onSelectionEnd = function () {
this.selecting = false;
this.update();
};
/**
* Handle window focus events on the view.
*
* @method
*/
ve.ui.Context.prototype.onWindowFocus = function () {
this.hide();
};
/**
* Handle 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
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();
};
/**
* Handle 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 the 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 UserInterface
* @chainable
*/
ve.ui.Context.prototype.destroy = function () {
this.$.remove();
return this;
};
/**
* Updates the context menu.
*
* @method
* @chainable
*/
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
// Reset and rebuild the menu
this.$menu.empty();
this.toolbar = new ve.ui.Toolbar(
$( '<div class="ve-ui-context-toolbar"></div>' ),
this.surface,
[{ 'name': 'inspectors', 'items' : inspectors }]
);
this.$menu.append( this.toolbar.$ );
if ( !this.visible ) {
this.obscure( this.$inspectors );
this.show();
}
}
}
// Remember selection for next time
this.selection = selection.clone();
return this;
};
/**
* Shows the context menu.
*
* @method
* @chainable
*/
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();
return this;
};
/**
* Hides the context menu.
*
* @method
* @chainable
*/
ve.ui.Context.prototype.hide = function () {
if ( this.inspector ) {
this.closeInspector();
this.$overlay.hide();
}
this.obscure( this.$menu );
this.$.css( 'visibility', 'hidden' );
this.visible = false;
return this;
};
/**
* Positions the context
*
* @param {jQuery} $overlay
* @param {jQuery} $element
* @chainable
*/
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 } );
return this;
};
/**
* Initializes a given inspector.
*
* @method
* @param {string} name Symbolic name of inspector
* @chainable
* @throws {Error} If inspector is unknown
*/
ve.ui.Context.prototype.initInspector = function ( name ) {
var inspector;
// Add inspector on demand.
if ( !ve.ui.inspectorFactory.lookup( name ) ) {
throw new Error( 'Unknown inspector: ' + 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 this;
};
/**
* Opens a given inspector.
*
* @method
* @param {string} name Symbolic name of inspector
* @chainable
*/
ve.ui.Context.prototype.openInspector = function ( name ) {
// Auto-initialize the inspector
this.initInspector( name );
// Only allow one inspector open at a time
if ( this.inspector ) {
this.closeInspector();
}
// Open the inspector
this.inspectors[name].open();
return this;
};
/**
* Closes currently open inspector.
*
* @method
* @param {boolean} remove Remove annotation while closing
* @chainable
*/
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 );
}
return this;
};
/**
* Brings inspector into view.
*
* @method
* @chainable
*/
ve.ui.Context.prototype.reveal = function ( $element ) {
$element.css( 'top', 0 );
return this;
};
/**
* Make inspector invisible without affecting its visiblity or display properties.
*
* @method
* @chainable
*/
ve.ui.Context.prototype.obscure = function ( $element ) {
$element.css( 'top', -5000 );
return this;
};