wiki.techinc.nl/resources/lib/ooui/oojs-ui-toolbars.js
Eric Gardner a0766a8ac7 Update OOUI from 0.47.4 to 0.47.5
Release notes: https://gerrit.wikimedia.org/g/oojs/ui/+/v0.47.5/History.md

Depends-On: Ia581f44ca0f147b318ce59b7271e4106d930ac3f
Change-Id: Ia6819baf32b93bbe67245709d4d9e5ef1358b22c
2023-07-19 14:51:07 -07:00

2830 lines
No EOL
88 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*!
* OOUI v0.47.5
* https://www.mediawiki.org/wiki/OOUI
*
* Copyright 20112023 OOUI Team and other contributors.
* Released under the MIT license
* http://oojs.mit-license.org
*
* Date: 2023-07-19T21:37:40Z
*/
( function ( OO ) {
'use strict';
/**
* Toolbars are complex interface components that permit users to easily access a variety
* of {@link OO.ui.Tool tools} (e.g., formatting commands) and actions, which are additional
* commands that are part of the toolbar, but not configured as tools.
*
* Individual tools are customized and then registered with a
* {@link OO.ui.ToolFactory tool factory}, which creates the tools on demand. Each tool has a
* symbolic name (used when registering the tool), a title (e.g., Insert image), and an icon.
*
* Individual tools are organized in {@link OO.ui.ToolGroup toolgroups}, which can be
* {@link OO.ui.MenuToolGroup menus} of tools, {@link OO.ui.ListToolGroup lists} of tools, or a
* single {@link OO.ui.BarToolGroup bar} of tools. The arrangement and order of the toolgroups is
* customized when the toolbar is set up. Tools can be presented in any order, but each can only
* appear once in the toolbar.
*
* The toolbar can be synchronized with the state of the external "application", like a text
* editor's editing area, marking tools as active/inactive (e.g. a 'bold' tool would be shown as
* active when the text cursor was inside bolded text) or enabled/disabled (e.g. a table caption
* tool would be disabled while the user is not editing a table). A state change is signalled by
* emitting the {@link #event-updateState 'updateState' event}, which calls Tools'
* {@link OO.ui.Tool#onUpdateState onUpdateState method}.
*
* The following is an example of a basic toolbar.
*
* @example
* // Example of a toolbar
* // Create the toolbar
* var toolFactory = new OO.ui.ToolFactory();
* var toolGroupFactory = new OO.ui.ToolGroupFactory();
* var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
*
* // We will be placing status text in this element when tools are used
* var $area = $( '<p>' ).text( 'Toolbar example' );
*
* // Define the tools that we're going to place in our toolbar
*
* // Create a class inheriting from OO.ui.Tool
* function SearchTool() {
* SearchTool.super.apply( this, arguments );
* }
* OO.inheritClass( SearchTool, OO.ui.Tool );
* // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
* // of 'icon' and 'title' (displayed icon and text).
* SearchTool.static.name = 'search';
* SearchTool.static.icon = 'search';
* SearchTool.static.title = 'Search...';
* // Defines the action that will happen when this tool is selected (clicked).
* SearchTool.prototype.onSelect = function () {
* $area.text( 'Search tool clicked!' );
* // Never display this tool as "active" (selected).
* this.setActive( false );
* };
* SearchTool.prototype.onUpdateState = function () {};
* // Make this tool available in our toolFactory and thus our toolbar
* toolFactory.register( SearchTool );
*
* // Register two more tools, nothing interesting here
* function SettingsTool() {
* SettingsTool.super.apply( this, arguments );
* }
* OO.inheritClass( SettingsTool, OO.ui.Tool );
* SettingsTool.static.name = 'settings';
* SettingsTool.static.icon = 'settings';
* SettingsTool.static.title = 'Change settings';
* SettingsTool.prototype.onSelect = function () {
* $area.text( 'Settings tool clicked!' );
* this.setActive( false );
* };
* SettingsTool.prototype.onUpdateState = function () {};
* toolFactory.register( SettingsTool );
*
* // Register two more tools, nothing interesting here
* function StuffTool() {
* StuffTool.super.apply( this, arguments );
* }
* OO.inheritClass( StuffTool, OO.ui.Tool );
* StuffTool.static.name = 'stuff';
* StuffTool.static.icon = 'ellipsis';
* StuffTool.static.title = 'More stuff';
* StuffTool.prototype.onSelect = function () {
* $area.text( 'More stuff tool clicked!' );
* this.setActive( false );
* };
* StuffTool.prototype.onUpdateState = function () {};
* toolFactory.register( StuffTool );
*
* // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
* // little popup window (a PopupWidget).
* function HelpTool( toolGroup, config ) {
* OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
* padded: true,
* label: 'Help',
* head: true
* } }, config ) );
* this.popup.$body.append( '<p>I am helpful!</p>' );
* }
* OO.inheritClass( HelpTool, OO.ui.PopupTool );
* HelpTool.static.name = 'help';
* HelpTool.static.icon = 'help';
* HelpTool.static.title = 'Help';
* toolFactory.register( HelpTool );
*
* // Finally define which tools and in what order appear in the toolbar. Each tool may only be
* // used once (but not all defined tools must be used).
* toolbar.setup( [
* {
* // 'bar' tool groups display tools' icons only, side-by-side.
* type: 'bar',
* include: [ 'search', 'help' ]
* },
* {
* // 'list' tool groups display both the titles and icons, in a dropdown list.
* type: 'list',
* indicator: 'down',
* label: 'More',
* include: [ 'settings', 'stuff' ]
* }
* // Note how the tools themselves are toolgroup-agnostic - the same tool can be displayed
* // either in a 'list' or a 'bar'. There is a 'menu' tool group too, not showcased here,
* // since it's more complicated to use. (See the next example snippet on this page.)
* ] );
*
* // Create some UI around the toolbar and place it in the document
* var frame = new OO.ui.PanelLayout( {
* expanded: false,
* framed: true
* } );
* var contentFrame = new OO.ui.PanelLayout( {
* expanded: false,
* padded: true
* } );
* frame.$element.append(
* toolbar.$element,
* contentFrame.$element.append( $area )
* );
* $( document.body ).append( frame.$element );
*
* // Here is where the toolbar is actually built. This must be done after inserting it into the
* // document.
* toolbar.initialize();
* toolbar.emit( 'updateState' );
*
* The following example extends the previous one to illustrate 'menu' toolgroups and the usage of
* {@link #event-updateState 'updateState' event}.
*
* @example
* // Create the toolbar
* var toolFactory = new OO.ui.ToolFactory();
* var toolGroupFactory = new OO.ui.ToolGroupFactory();
* var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
*
* // We will be placing status text in this element when tools are used
* var $area = $( '<p>' ).text( 'Toolbar example' );
*
* // Define the tools that we're going to place in our toolbar
*
* // Create a class inheriting from OO.ui.Tool
* function SearchTool() {
* SearchTool.super.apply( this, arguments );
* }
* OO.inheritClass( SearchTool, OO.ui.Tool );
* // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
* // of 'icon' and 'title' (displayed icon and text).
* SearchTool.static.name = 'search';
* SearchTool.static.icon = 'search';
* SearchTool.static.title = 'Search...';
* // Defines the action that will happen when this tool is selected (clicked).
* SearchTool.prototype.onSelect = function () {
* $area.text( 'Search tool clicked!' );
* // Never display this tool as "active" (selected).
* this.setActive( false );
* };
* SearchTool.prototype.onUpdateState = function () {};
* // Make this tool available in our toolFactory and thus our toolbar
* toolFactory.register( SearchTool );
*
* // Register two more tools, nothing interesting here
* function SettingsTool() {
* SettingsTool.super.apply( this, arguments );
* this.reallyActive = false;
* }
* OO.inheritClass( SettingsTool, OO.ui.Tool );
* SettingsTool.static.name = 'settings';
* SettingsTool.static.icon = 'settings';
* SettingsTool.static.title = 'Change settings';
* SettingsTool.prototype.onSelect = function () {
* $area.text( 'Settings tool clicked!' );
* // Toggle the active state on each click
* this.reallyActive = !this.reallyActive;
* this.setActive( this.reallyActive );
* // To update the menu label
* this.toolbar.emit( 'updateState' );
* };
* SettingsTool.prototype.onUpdateState = function () {};
* toolFactory.register( SettingsTool );
*
* // Register two more tools, nothing interesting here
* function StuffTool() {
* StuffTool.super.apply( this, arguments );
* this.reallyActive = false;
* }
* OO.inheritClass( StuffTool, OO.ui.Tool );
* StuffTool.static.name = 'stuff';
* StuffTool.static.icon = 'ellipsis';
* StuffTool.static.title = 'More stuff';
* StuffTool.prototype.onSelect = function () {
* $area.text( 'More stuff tool clicked!' );
* // Toggle the active state on each click
* this.reallyActive = !this.reallyActive;
* this.setActive( this.reallyActive );
* // To update the menu label
* this.toolbar.emit( 'updateState' );
* };
* StuffTool.prototype.onUpdateState = function () {};
* toolFactory.register( StuffTool );
*
* // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
* // little popup window (a PopupWidget). 'onUpdateState' is also already implemented.
* function HelpTool( toolGroup, config ) {
* OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
* padded: true,
* label: 'Help',
* head: true
* } }, config ) );
* this.popup.$body.append( '<p>I am helpful!</p>' );
* }
* OO.inheritClass( HelpTool, OO.ui.PopupTool );
* HelpTool.static.name = 'help';
* HelpTool.static.icon = 'help';
* HelpTool.static.title = 'Help';
* toolFactory.register( HelpTool );
*
* // Finally define which tools and in what order appear in the toolbar. Each tool may only be
* // used once (but not all defined tools must be used).
* toolbar.setup( [
* {
* // 'bar' tool groups display tools' icons only, side-by-side.
* type: 'bar',
* include: [ 'search', 'help' ]
* },
* {
* // 'menu' tool groups display both the titles and icons, in a dropdown menu.
* // Menu label indicates which items are selected.
* type: 'menu',
* indicator: 'down',
* include: [ 'settings', 'stuff' ]
* }
* ] );
*
* // Create some UI around the toolbar and place it in the document
* var frame = new OO.ui.PanelLayout( {
* expanded: false,
* framed: true
* } );
* var contentFrame = new OO.ui.PanelLayout( {
* expanded: false,
* padded: true
* } );
* frame.$element.append(
* toolbar.$element,
* contentFrame.$element.append( $area )
* );
* $( document.body ).append( frame.$element );
*
* // Here is where the toolbar is actually built. This must be done after inserting it into the
* // document.
* toolbar.initialize();
* toolbar.emit( 'updateState' );
*
* @class
* @extends OO.ui.Element
* @mixins OO.EventEmitter
* @mixins OO.ui.mixin.GroupElement
*
* @constructor
* @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
* @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating toolgroups
* @param {Object} [config] Configuration options
* @cfg {boolean} [actions] Add an actions section to the toolbar. Actions are commands that are
* included in the toolbar, but are not configured as tools. By default, actions are displayed on
* the right side of the toolbar.
* This feature is deprecated. It is suggested to use the ToolGroup 'align' property instead.
* @cfg {string} [position='top'] Whether the toolbar is positioned above ('top') or below
* ('bottom') content.
* @cfg {jQuery} [$overlay] An overlay for the popup.
* See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
*/
OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
// Allow passing positional parameters inside the config object
if ( OO.isPlainObject( toolFactory ) && config === undefined ) {
config = toolFactory;
toolFactory = config.toolFactory;
toolGroupFactory = config.toolGroupFactory;
}
// Configuration initialization
config = config || {};
// Parent constructor
OO.ui.Toolbar.super.call( this, config );
// Mixin constructors
OO.EventEmitter.call( this );
OO.ui.mixin.GroupElement.call( this, config );
// Properties
this.toolFactory = toolFactory;
this.toolGroupFactory = toolGroupFactory;
this.groupsByName = {};
this.activeToolGroups = 0;
this.tools = {};
this.position = config.position || 'top';
this.$bar = $( '<div>' );
this.$after = $( '<div>' );
this.$actions = $( '<div>' );
this.$popups = $( '<div>' );
this.initialized = false;
this.narrow = false;
this.narrowThreshold = null;
this.onWindowResizeHandler = this.onWindowResize.bind( this );
this.$overlay = ( config.$overlay === true ? OO.ui.getDefaultOverlay() : config.$overlay ) ||
this.$element;
// Events
this.$element
.add( this.$bar ).add( this.$group ).add( this.$after ).add( this.$actions )
.on( 'mousedown keydown', this.onPointerDown.bind( this ) );
// Initialization
this.$bar.addClass( 'oo-ui-toolbar-bar' );
this.$group.addClass( 'oo-ui-toolbar-tools' );
this.$after.addClass( 'oo-ui-toolbar-tools oo-ui-toolbar-after' );
this.$popups.addClass( 'oo-ui-toolbar-popups' );
this.$bar.append( this.$group, this.$after );
if ( config.actions ) {
this.$bar.append( this.$actions.addClass( 'oo-ui-toolbar-actions' ) );
}
this.$bar.append( $( '<div>' ).css( 'clear', 'both' ) );
// Possible classes: oo-ui-toolbar-position-top, oo-ui-toolbar-position-bottom
this.$element
.addClass( 'oo-ui-toolbar oo-ui-toolbar-position-' + this.position )
.append( this.$bar );
this.$overlay.append( this.$popups );
};
/* Setup */
OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
OO.mixinClass( OO.ui.Toolbar, OO.ui.mixin.GroupElement );
/* Events */
/**
* @event updateState
*
* An 'updateState' event must be emitted on the Toolbar (by calling
* `toolbar.emit( 'updateState' )`) every time the state of the application using the toolbar
* changes, and an update to the state of tools is required.
*
* @param {...Mixed} data Application-defined parameters
*/
/**
* @event active
*
* An 'active' event is emitted when the number of active toolgroups increases from 0, or
* returns to 0.
*
* @param {boolean} There are active toolgroups in this toolbar
*/
/**
* @event resize
*
* Toolbar has resized to a point where narrow mode has changed
*/
/* Methods */
/**
* Get the tool factory.
*
* @return {OO.ui.ToolFactory} Tool factory
*/
OO.ui.Toolbar.prototype.getToolFactory = function () {
return this.toolFactory;
};
/**
* Get the toolgroup factory.
*
* @return {OO.Factory} Toolgroup factory
*/
OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
return this.toolGroupFactory;
};
/**
* @inheritdoc {OO.ui.mixin.GroupElement}
*/
OO.ui.Toolbar.prototype.insertItemElements = function ( item ) {
// Mixin method
OO.ui.mixin.GroupElement.prototype.insertItemElements.apply( this, arguments );
if ( item.align === 'after' ) {
// Toolbar only ever appends ToolGroups to the end, so we can ignore 'index'
this.$after.append( item.$element );
}
};
/**
* Handles mouse down events.
*
* @private
* @param {jQuery.Event} e Mouse down event
* @return {undefined|boolean} False to prevent default if event is handled
*/
OO.ui.Toolbar.prototype.onPointerDown = function ( e ) {
var $closestWidgetToEvent = $( e.target ).closest( '.oo-ui-widget' ),
$closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
if (
!$closestWidgetToEvent.length ||
$closestWidgetToEvent[ 0 ] ===
$closestWidgetToToolbar[ 0 ]
) {
return false;
}
};
/**
* Handle window resize event.
*
* @private
* @param {jQuery.Event} e Window resize event
*/
OO.ui.Toolbar.prototype.onWindowResize = function () {
this.setNarrow( this.$bar[ 0 ].clientWidth <= this.getNarrowThreshold() );
};
/**
* Check if the toolbar is in narrow mode
*
* @return {boolean} Toolbar is in narrow mode
*/
OO.ui.Toolbar.prototype.isNarrow = function () {
return this.narrow;
};
/**
* Set the narrow mode flag
*
* @param {boolean} narrow Toolbar is in narrow mode
*/
OO.ui.Toolbar.prototype.setNarrow = function ( narrow ) {
if ( narrow !== this.narrow ) {
this.narrow = narrow;
this.$element.add( this.$popups ).toggleClass(
'oo-ui-toolbar-narrow',
this.narrow
);
this.emit( 'resize' );
}
};
/**
* Get the (lazily-computed) width threshold for applying the oo-ui-toolbar-narrow
* class.
*
* @private
* @return {number} Width threshold in pixels
*/
OO.ui.Toolbar.prototype.getNarrowThreshold = function () {
if ( this.narrowThreshold === null ) {
this.narrowThreshold = this.$group[ 0 ].offsetWidth + this.$after[ 0 ].offsetWidth +
this.$actions[ 0 ].offsetWidth;
}
return this.narrowThreshold;
};
/**
* Sets up handles and preloads required information for the toolbar to work.
* This must be called after it is attached to a visible document and before doing anything else.
*/
OO.ui.Toolbar.prototype.initialize = function () {
if ( !this.initialized ) {
this.initialized = true;
$( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
this.onWindowResize();
}
};
/**
* Set up the toolbar.
*
* The toolbar is set up with a list of toolgroup configurations that specify the type of
* toolgroup ({@link OO.ui.BarToolGroup bar}, {@link OO.ui.MenuToolGroup menu}, or
* {@link OO.ui.ListToolGroup list}) to add and which tools to include, exclude, promote, or demote
* within that toolgroup. Please see {@link OO.ui.ToolGroup toolgroups} for more information about
* including tools in toolgroups.
*
* @param {Object[]} groups List of toolgroup configurations
* @param {string} groups.name Symbolic name for this toolgroup
* @param {string} [groups.type] Toolgroup type, e.g. "bar", "list", or "menu". Should exist in the
* {@link OO.ui.ToolGroupFactory} provided via the constructor. Defaults to "list" for catch-all
* groups where `include='*'`, otherwise "bar".
* @param {Array|string} [groups.include] Tools to include in the toolgroup, or "*" for catch-all,
* see {@link OO.ui.ToolFactory#extract}
* @param {Array|string} [groups.exclude] Tools to exclude from the toolgroup
* @param {Array|string} [groups.promote] Tools to promote to the beginning of the toolgroup
* @param {Array|string} [groups.demote] Tools to demote to the end of the toolgroup
*/
OO.ui.Toolbar.prototype.setup = function ( groups ) {
var defaultType = 'bar';
// Cleanup previous groups
this.reset();
var items = [];
// Build out new groups
for ( var i = 0, len = groups.length; i < len; i++ ) {
var groupConfig = groups[ i ];
if ( groupConfig.include === '*' ) {
// Apply defaults to catch-all groups
if ( groupConfig.type === undefined ) {
groupConfig.type = 'list';
}
if ( groupConfig.label === undefined ) {
groupConfig.label = OO.ui.msg( 'ooui-toolbar-more' );
}
}
// Check type has been registered
var type = this.getToolGroupFactory().lookup( groupConfig.type ) ?
groupConfig.type : defaultType;
var toolGroup = this.getToolGroupFactory().create( type, this, groupConfig );
items.push( toolGroup );
this.groupsByName[ groupConfig.name ] = toolGroup;
toolGroup.connect( this, {
active: 'onToolGroupActive'
} );
}
this.addItems( items );
};
/**
* Handle active events from tool groups
*
* @param {boolean} active Tool group has become active, inactive if false
* @fires active
*/
OO.ui.Toolbar.prototype.onToolGroupActive = function ( active ) {
if ( active ) {
this.activeToolGroups++;
if ( this.activeToolGroups === 1 ) {
this.emit( 'active', true );
}
} else {
this.activeToolGroups--;
if ( this.activeToolGroups === 0 ) {
this.emit( 'active', false );
}
}
};
/**
* Get a toolgroup by name
*
* @param {string} name Group name
* @return {OO.ui.ToolGroup|null} Tool group, or null if none found by that name
*/
OO.ui.Toolbar.prototype.getToolGroupByName = function ( name ) {
return this.groupsByName[ name ] || null;
};
/**
* Remove all tools and toolgroups from the toolbar.
*/
OO.ui.Toolbar.prototype.reset = function () {
this.groupsByName = {};
this.tools = {};
for ( var i = 0, len = this.items.length; i < len; i++ ) {
this.items[ i ].destroy();
}
this.clearItems();
};
/**
* Destroy the toolbar.
*
* Destroying the toolbar removes all event handlers and DOM elements that constitute the toolbar.
* Call this method whenever you are done using a toolbar.
*/
OO.ui.Toolbar.prototype.destroy = function () {
$( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
this.reset();
this.$element.remove();
};
/**
* Check if the tool is available.
*
* Available tools are ones that have not yet been added to the toolbar.
*
* @param {string} name Symbolic name of tool
* @return {boolean} Tool is available
*/
OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
return !this.tools[ name ];
};
/**
* Prevent tool from being used again.
*
* @param {OO.ui.Tool} tool Tool to reserve
*/
OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
this.tools[ tool.getName() ] = tool;
};
/**
* Allow tool to be used again.
*
* @param {OO.ui.Tool} tool Tool to release
*/
OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
delete this.tools[ tool.getName() ];
};
/**
* Get accelerator label for tool.
*
* The OOUI library does not contain an accelerator system, but this is the hook for one. To
* use an accelerator system, subclass the toolbar and override this method, which is meant to
* return a label that describes the accelerator keys for the tool passed (by symbolic name) to
* the method.
*
* @param {string} name Symbolic name of tool
* @return {string|undefined} Tool accelerator label if available
*/
OO.ui.Toolbar.prototype.getToolAccelerator = function () {
return undefined;
};
/**
* Tools, together with {@link OO.ui.ToolGroup toolgroups}, constitute
* {@link OO.ui.Toolbar toolbars}.
* Each tool is configured with a static name, title, and icon and is customized with the command
* to carry out when the tool is selected. Tools must also be registered with a
* {@link OO.ui.ToolFactory tool factory}, which creates the tools on demand.
*
* Every Tool subclass must implement two methods:
*
* - {@link #onUpdateState}
* - {@link #onSelect}
*
* Tools are added to toolgroups ({@link OO.ui.ListToolGroup ListToolGroup},
* {@link OO.ui.BarToolGroup BarToolGroup}, or {@link OO.ui.MenuToolGroup MenuToolGroup}), which
* determine how the tool is displayed in the toolbar. See {@link OO.ui.Toolbar toolbars} for an
* example.
*
* For more information, please see the [OOUI documentation on MediaWiki][1].
* [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars
*
* @abstract
* @class
* @extends OO.ui.Widget
* @mixins OO.ui.mixin.IconElement
* @mixins OO.ui.mixin.FlaggedElement
* @mixins OO.ui.mixin.TabIndexedElement
*
* @constructor
* @param {OO.ui.ToolGroup} toolGroup
* @param {Object} [config] Configuration options
* @cfg {string|Function} [title] Title text or a function that returns text. If this config is
* omitted, the value of the {@link #static-title static title} property is used.
* @cfg {boolean} [displayBothIconAndLabel] See static.displayBothIconAndLabel
* @cfg {Object} [narrowConfig] See static.narrowConfig
*
* The title is used in different ways depending on the type of toolgroup that contains the tool.
* The title is used as a tooltip if the tool is part of a {@link OO.ui.BarToolGroup bar}
* toolgroup, or as the label text if the tool is part of a {@link OO.ui.ListToolGroup list} or
* {@link OO.ui.MenuToolGroup menu} toolgroup.
*
* For bar toolgroups, a description of the accelerator key is appended to the title if an
* accelerator key is associated with an action by the same name as the tool and accelerator
* functionality has been added to the application.
* To add accelerator key functionality, you must subclass OO.ui.Toolbar and override the
* {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method.
*/
OO.ui.Tool = function OoUiTool( toolGroup, config ) {
// Allow passing positional parameters inside the config object
if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
config = toolGroup;
toolGroup = config.toolGroup;
}
// Configuration initialization
config = config || {};
// Parent constructor
OO.ui.Tool.super.call( this, config );
// Properties
this.toolGroup = toolGroup;
this.toolbar = this.toolGroup.getToolbar();
this.active = false;
this.$title = $( '<span>' );
this.$accel = $( '<span>' );
this.$link = $( '<a>' );
this.title = null;
this.checkIcon = new OO.ui.IconWidget( {
icon: 'check',
classes: [ 'oo-ui-tool-checkIcon' ]
} );
this.displayBothIconAndLabel = config.displayBothIconAndLabel !== undefined ?
config.displayBothIconAndLabel : this.constructor.static.displayBothIconAndLabel;
this.narrowConfig = config.narrowConfig || this.constructor.static.narrowConfig;
// Mixin constructors
OO.ui.mixin.IconElement.call( this, config );
OO.ui.mixin.FlaggedElement.call( this, config );
OO.ui.mixin.TabIndexedElement.call( this, $.extend( {
$tabIndexed: this.$link
}, config ) );
// Events
this.toolbar.connect( this, {
updateState: 'onUpdateState',
resize: 'onToolbarResize'
} );
// Initialization
this.$title.addClass( 'oo-ui-tool-title' );
this.$accel
.addClass( 'oo-ui-tool-accel' )
.prop( {
// This may need to be changed if the key names are ever localized,
// but for now they are essentially written in English
dir: 'ltr',
lang: 'en'
} );
this.$link
.addClass( 'oo-ui-tool-link' )
.append( this.checkIcon.$element, this.$icon, this.$title, this.$accel )
.attr( 'role', 'button' );
// Don't show keyboard shortcuts on mobile as users are unlikely to have
// a physical keyboard, and likely to have limited screen space.
if ( !OO.ui.isMobile() ) {
this.$link.append( this.$accel );
}
this.$element
.data( 'oo-ui-tool', this )
.addClass( 'oo-ui-tool' )
.addClass( 'oo-ui-tool-name-' +
this.constructor.static.name.replace( /^([^/]+)\/([^/]+).*$/, '$1-$2' ) )
.append( this.$link );
this.setTitle( config.title || this.constructor.static.title );
};
/* Setup */
OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
OO.mixinClass( OO.ui.Tool, OO.ui.mixin.IconElement );
OO.mixinClass( OO.ui.Tool, OO.ui.mixin.FlaggedElement );
OO.mixinClass( OO.ui.Tool, OO.ui.mixin.TabIndexedElement );
/* Static Properties */
/**
* @static
* @inheritdoc
*/
OO.ui.Tool.static.tagName = 'span';
/**
* Symbolic name of tool.
*
* The symbolic name is used internally to register the tool with a
* {@link OO.ui.ToolFactory ToolFactory}. It can also be used when adding tools to toolgroups.
*
* @abstract
* @static
* @inheritable
* @property {string}
*/
OO.ui.Tool.static.name = '';
/**
* Symbolic name of the group.
*
* The group name is used to associate tools with each other so that they can be selected later by
* a {@link OO.ui.ToolGroup toolgroup}.
*
* @abstract
* @static
* @inheritable
* @property {string}
*/
OO.ui.Tool.static.group = '';
/**
* Tool title text or a function that returns title text. The value of the static property is
* overridden if the #title config option is used.
*
* @abstract
* @static
* @inheritable
* @property {string|Function}
*/
OO.ui.Tool.static.title = '';
/**
* Display both icon and label when the tool is used in a {@link OO.ui.BarToolGroup bar} toolgroup.
* Normally only the icon is displayed, or only the label if no icon is given.
*
* @static
* @inheritable
* @property {boolean}
*/
OO.ui.Tool.static.displayBothIconAndLabel = false;
/**
* Add tool to catch-all groups automatically.
*
* A catch-all group, which contains all tools that do not currently belong to a toolgroup,
* can be included in a toolgroup using the wildcard selector, an asterisk (*).
*
* @static
* @inheritable
* @property {boolean}
*/
OO.ui.Tool.static.autoAddToCatchall = true;
/**
* Add tool to named groups automatically.
*
* By default, tools that are configured with a static group property are added
* to that group and will be selected when the symbolic name of the group is specified (e.g., when
* toolgroups include tools by group name).
*
* @static
* @property {boolean}
* @inheritable
*/
OO.ui.Tool.static.autoAddToGroup = true;
/**
* Check if this tool is compatible with given data.
*
* This is a stub that can be overridden to provide support for filtering tools based on an
* arbitrary piece of information (e.g., where the cursor is in a document). The implementation
* must also call this method so that the compatibility check can be performed.
*
* @static
* @inheritable
* @param {Mixed} data Data to check
* @return {boolean} Tool can be used with data
*/
OO.ui.Tool.static.isCompatibleWith = function () {
return false;
};
/**
* Config options to change when toolbar is in narrow mode
*
* Supports `displayBothIconAndLabel`, `title` and `icon` properties.
*
* @static
* @inheritable
* @property {Object|null}
*/
OO.ui.Tool.static.narrowConfig = null;
/* Methods */
/**
* Handle the toolbar state being updated. This method is called when the
* {@link OO.ui.Toolbar#event-updateState 'updateState' event} is emitted on the
* {@link OO.ui.Toolbar Toolbar} that uses this tool, and should set the state of this tool
* depending on application state (usually by calling #setDisabled to enable or disable the tool,
* or #setActive to mark is as currently in-use or not).
*
* This is an abstract method that must be overridden in a concrete subclass.
*
* @method
* @protected
* @abstract
*/
OO.ui.Tool.prototype.onUpdateState = null;
/**
* Handle the tool being selected. This method is called when the user triggers this tool,
* usually by clicking on its label/icon.
*
* This is an abstract method that must be overridden in a concrete subclass.
*
* @method
* @protected
* @abstract
*/
OO.ui.Tool.prototype.onSelect = null;
/**
* Check if the tool is active.
*
* Tools become active when their #onSelect or #onUpdateState handlers change them to appear pressed
* with the #setActive method. Additional CSS is applied to the tool to reflect the active state.
*
* @return {boolean} Tool is active
*/
OO.ui.Tool.prototype.isActive = function () {
return this.active;
};
/**
* Make the tool appear active or inactive.
*
* This method should be called within #onSelect or #onUpdateState event handlers to make the tool
* appear pressed or not.
*
* @param {boolean} [state=false] Make tool appear active
*/
OO.ui.Tool.prototype.setActive = function ( state ) {
this.active = !!state;
this.$element.toggleClass( 'oo-ui-tool-active', this.active );
this.updateThemeClasses();
};
/**
* Set the tool #title.
*
* @param {string|Function} title Title text or a function that returns text
* @chainable
* @return {OO.ui.Tool} The tool, for chaining
*/
OO.ui.Tool.prototype.setTitle = function ( title ) {
this.title = OO.ui.resolveMsg( title );
this.updateTitle();
// Update classes
this.setDisplayBothIconAndLabel( this.displayBothIconAndLabel );
return this;
};
/**
* Set the tool's displayBothIconAndLabel state.
*
* Update title classes if necessary
*
* @param {boolean} displayBothIconAndLabel
* @chainable
* @return {OO.ui.Tool} The tool, for chaining
*/
OO.ui.Tool.prototype.setDisplayBothIconAndLabel = function ( displayBothIconAndLabel ) {
this.displayBothIconAndLabel = displayBothIconAndLabel;
this.$element.toggleClass( 'oo-ui-tool-with-label', !!this.title && this.displayBothIconAndLabel );
return this;
};
/**
* Get the tool #title.
*
* @return {string} Title text
*/
OO.ui.Tool.prototype.getTitle = function () {
return this.title;
};
/**
* Get the tool's symbolic name.
*
* @return {string} Symbolic name of tool
*/
OO.ui.Tool.prototype.getName = function () {
return this.constructor.static.name;
};
/**
* Handle resize events from the toolbar
*/
OO.ui.Tool.prototype.onToolbarResize = function () {
if ( !this.narrowConfig ) {
return;
}
if ( this.toolbar.isNarrow() ) {
if ( this.narrowConfig.displayBothIconAndLabel !== undefined ) {
this.wideDisplayBothIconAndLabel = this.displayBothIconAndLabel;
this.setDisplayBothIconAndLabel( this.narrowConfig.displayBothIconAndLabel );
}
if ( this.narrowConfig.title !== undefined ) {
this.wideTitle = this.title;
this.setTitle( this.narrowConfig.title );
}
if ( this.narrowConfig.icon !== undefined ) {
this.wideIcon = this.icon;
this.setIcon( this.narrowConfig.icon );
}
} else {
if ( this.wideDisplayBothIconAndLabel !== undefined ) {
this.setDisplayBothIconAndLabel( this.wideDisplayBothIconAndLabel );
}
if ( this.wideTitle !== undefined ) {
this.setTitle( this.wideTitle );
}
if ( this.wideIcon !== undefined ) {
this.setIcon( this.wideIcon );
}
}
};
/**
* Update the title.
*/
OO.ui.Tool.prototype.updateTitle = function () {
var titleTooltips = this.toolGroup.constructor.static.titleTooltips,
accelTooltips = this.toolGroup.constructor.static.accelTooltips,
accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
tooltipParts = [];
this.$title.text( this.title );
this.$accel.text( accel );
if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
tooltipParts.push( this.title );
}
if ( accelTooltips && typeof accel === 'string' && accel.length ) {
tooltipParts.push( accel );
}
if ( tooltipParts.length ) {
this.$link.attr( 'title', tooltipParts.join( ' ' ) );
} else {
this.$link.removeAttr( 'title' );
}
};
/**
* @inheritdoc OO.ui.mixin.IconElement
*/
OO.ui.Tool.prototype.setIcon = function ( icon ) {
// Mixin method
OO.ui.mixin.IconElement.prototype.setIcon.call( this, icon );
this.$element.toggleClass( 'oo-ui-tool-with-icon', !!this.icon );
return this;
};
/**
* Destroy tool.
*
* Destroying the tool removes all event handlers and the tools DOM elements.
* Call this method whenever you are done using a tool.
*/
OO.ui.Tool.prototype.destroy = function () {
this.toolbar.disconnect( this );
this.$element.remove();
};
/**
* ToolGroups are collections of {@link OO.ui.Tool tools} that are used in a
* {@link OO.ui.Toolbar toolbar}.
* The type of toolgroup ({@link OO.ui.ListToolGroup list}, {@link OO.ui.BarToolGroup bar}, or
* {@link OO.ui.MenuToolGroup menu}) to which a tool belongs determines how the tool is arranged
* and displayed in the toolbar. Toolgroups themselves are created on demand with a
* {@link OO.ui.ToolGroupFactory toolgroup factory}.
*
* Toolgroups can contain individual tools, groups of tools, or all available tools, as specified
* using the `include` config option. See OO.ui.ToolFactory#extract on documentation of the format.
* The options `exclude`, `promote`, and `demote` support the same formats.
*
* See {@link OO.ui.Toolbar toolbars} for a full example. For more information about toolbars in
* general, please see the [OOUI documentation on MediaWiki][1].
*
* [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars
*
* @abstract
* @class
* @extends OO.ui.Widget
* @mixins OO.ui.mixin.GroupElement
*
* @constructor
* @param {OO.ui.Toolbar} toolbar
* @param {Object} [config] Configuration options
* @cfg {Array|string} [include=[]] List of tools to include in the toolgroup, see above.
* @cfg {Array|string} [exclude=[]] List of tools to exclude from the toolgroup, see above.
* @cfg {Array|string} [promote=[]] List of tools to promote to the beginning of the toolgroup,
* see above.
* @cfg {Array|string} [demote=[]] List of tools to demote to the end of the toolgroup, see above.
* This setting is particularly useful when tools have been added to the toolgroup
* en masse (e.g., via the catch-all selector).
* @cfg {string} [align='before'] Alignment within the toolbar, either 'before' or 'after'.
*/
OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
// Allow passing positional parameters inside the config object
if ( OO.isPlainObject( toolbar ) && config === undefined ) {
config = toolbar;
toolbar = config.toolbar;
}
// Configuration initialization
config = config || {};
// Parent constructor
OO.ui.ToolGroup.super.call( this, config );
// Mixin constructors
OO.ui.mixin.GroupElement.call( this, config );
// Properties
this.toolbar = toolbar;
this.tools = {};
this.pressed = null;
this.autoDisabled = false;
this.include = config.include || [];
this.exclude = config.exclude || [];
this.promote = config.promote || [];
this.demote = config.demote || [];
this.align = config.align || 'before';
this.onDocumentMouseKeyUpHandler = this.onDocumentMouseKeyUp.bind( this );
// Events
this.$group.on( {
mousedown: this.onMouseKeyDown.bind( this ),
mouseup: this.onMouseKeyUp.bind( this ),
keydown: this.onMouseKeyDown.bind( this ),
keyup: this.onMouseKeyUp.bind( this ),
focus: this.onMouseOverFocus.bind( this ),
blur: this.onMouseOutBlur.bind( this ),
mouseover: this.onMouseOverFocus.bind( this ),
mouseout: this.onMouseOutBlur.bind( this )
} );
this.toolbar.getToolFactory().connect( this, {
register: 'onToolFactoryRegister'
} );
this.aggregate( {
disable: 'itemDisable'
} );
this.connect( this, {
itemDisable: 'updateDisabled',
disable: 'onDisable'
} );
// Initialization
this.$group.addClass( 'oo-ui-toolGroup-tools' );
this.$element
.addClass( 'oo-ui-toolGroup' )
.append( this.$group );
this.onDisable( this.isDisabled() );
this.populate();
};
/* Setup */
OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget );
OO.mixinClass( OO.ui.ToolGroup, OO.ui.mixin.GroupElement );
/* Events */
/**
* @event update
*/
/**
* @event active
*
* An 'active' event is emitted when any popup is shown/hidden.
*
* @param {boolean} The popup is visible
*/
/* Static Properties */
/**
* Show labels in tooltips.
*
* @static
* @inheritable
* @property {boolean}
*/
OO.ui.ToolGroup.static.titleTooltips = false;
/**
* Show acceleration labels in tooltips.
*
* Note: The OOUI library does not include an accelerator system, but does contain
* a hook for one. To use an accelerator system, subclass the {@link OO.ui.Toolbar toolbar} and
* override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method, which is
* meant to return a label that describes the accelerator keys for a given tool (e.g., Control+M
* key combination).
*
* @static
* @inheritable
* @property {boolean}
*/
OO.ui.ToolGroup.static.accelTooltips = false;
/**
* Automatically disable the toolgroup when all tools are disabled
*
* @static
* @inheritable
* @property {boolean}
*/
OO.ui.ToolGroup.static.autoDisable = true;
/**
* @abstract
* @static
* @inheritable
* @property {string}
*/
OO.ui.ToolGroup.static.name = null;
/* Methods */
/**
* @inheritdoc
*/
OO.ui.ToolGroup.prototype.isDisabled = function () {
return this.autoDisabled ||
OO.ui.ToolGroup.super.prototype.isDisabled.apply( this, arguments );
};
/**
* @inheritdoc
*/
OO.ui.ToolGroup.prototype.updateDisabled = function () {
var allDisabled = true;
if ( this.constructor.static.autoDisable ) {
for ( var i = this.items.length - 1; i >= 0; i-- ) {
var item = this.items[ i ];
if ( !item.isDisabled() ) {
allDisabled = false;
break;
}
}
this.autoDisabled = allDisabled;
}
OO.ui.ToolGroup.super.prototype.updateDisabled.apply( this, arguments );
};
/**
* Handle disable events.
*
* @protected
* @param {boolean} isDisabled
*/
OO.ui.ToolGroup.prototype.onDisable = function ( isDisabled ) {
this.$group.toggleClass( 'oo-ui-toolGroup-disabled-tools', isDisabled );
this.$group.toggleClass( 'oo-ui-toolGroup-enabled-tools', !isDisabled );
};
/**
* Handle mouse down and key down events.
*
* @protected
* @param {jQuery.Event} e Mouse down or key down event
* @return {undefined|boolean} False to prevent default if event is handled
*/
OO.ui.ToolGroup.prototype.onMouseKeyDown = function ( e ) {
if (
!this.isDisabled() && (
e.which === OO.ui.MouseButtons.LEFT ||
e.which === OO.ui.Keys.SPACE ||
e.which === OO.ui.Keys.ENTER
)
) {
this.pressed = this.findTargetTool( e );
if ( this.pressed ) {
this.pressed.setActive( true );
this.getElementDocument().addEventListener(
'mouseup',
this.onDocumentMouseKeyUpHandler,
true
);
this.getElementDocument().addEventListener(
'keyup',
this.onDocumentMouseKeyUpHandler,
true
);
return false;
}
}
};
/**
* Handle document mouse up and key up events.
*
* @protected
* @param {MouseEvent|KeyboardEvent} e Mouse up or key up event
*/
OO.ui.ToolGroup.prototype.onDocumentMouseKeyUp = function ( e ) {
this.getElementDocument().removeEventListener(
'mouseup',
this.onDocumentMouseKeyUpHandler,
true
);
this.getElementDocument().removeEventListener(
'keyup',
this.onDocumentMouseKeyUpHandler,
true
);
// onMouseKeyUp may be called a second time, depending on where the mouse is when the button is
// released, but since `this.pressed` will no longer be true, the second call will be ignored.
this.onMouseKeyUp( e );
};
/**
* Handle mouse up and key up events.
*
* @protected
* @param {MouseEvent|KeyboardEvent} e Mouse up or key up event
*/
OO.ui.ToolGroup.prototype.onMouseKeyUp = function ( e ) {
var tool = this.findTargetTool( e );
if (
!this.isDisabled() && this.pressed && this.pressed === tool && (
e.which === OO.ui.MouseButtons.LEFT ||
e.which === OO.ui.Keys.SPACE ||
e.which === OO.ui.Keys.ENTER
)
) {
this.pressed.onSelect();
this.pressed = null;
e.preventDefault();
e.stopPropagation();
}
this.pressed = null;
};
/**
* Handle mouse over and focus events.
*
* @protected
* @param {jQuery.Event} e Mouse over or focus event
*/
OO.ui.ToolGroup.prototype.onMouseOverFocus = function ( e ) {
var tool = this.findTargetTool( e );
if ( this.pressed && this.pressed === tool ) {
this.pressed.setActive( true );
}
};
/**
* Handle mouse out and blur events.
*
* @protected
* @param {jQuery.Event} e Mouse out or blur event
*/
OO.ui.ToolGroup.prototype.onMouseOutBlur = function ( e ) {
var tool = this.findTargetTool( e );
if ( this.pressed && this.pressed === tool ) {
this.pressed.setActive( false );
}
};
/**
* Get the closest tool to a jQuery.Event.
*
* Only tool links are considered, which prevents other elements in the tool such as popups from
* triggering tool group interactions.
*
* @private
* @param {jQuery.Event} e
* @return {OO.ui.Tool|null} Tool, `null` if none was found
*/
OO.ui.ToolGroup.prototype.findTargetTool = function ( e ) {
var $item = $( e.target ).closest( '.oo-ui-tool-link' );
var tool;
if ( $item.length ) {
tool = $item.parent().data( 'oo-ui-tool' );
}
return tool && !tool.isDisabled() ? tool : null;
};
/**
* Handle tool registry register events.
*
* If a tool is registered after the group is created, we must repopulate the list to account for:
*
* - a tool being added that may be included
* - a tool already included being overridden
*
* @protected
* @param {string} name Symbolic name of tool
*/
OO.ui.ToolGroup.prototype.onToolFactoryRegister = function () {
this.populate();
};
/**
* Get the toolbar that contains the toolgroup.
*
* @return {OO.ui.Toolbar} Toolbar that contains the toolgroup
*/
OO.ui.ToolGroup.prototype.getToolbar = function () {
return this.toolbar;
};
/**
* Add and remove tools based on configuration.
*/
OO.ui.ToolGroup.prototype.populate = function () {
var toolFactory = this.toolbar.getToolFactory(),
names = {},
add = [],
remove = [],
list = this.toolbar.getToolFactory().getTools(
this.include, this.exclude, this.promote, this.demote
);
var name;
// Build a list of needed tools
for ( var i = 0, len = list.length; i < len; i++ ) {
name = list[ i ];
if (
// Tool exists
toolFactory.lookup( name ) &&
// Tool is available or is already in this group
( this.toolbar.isToolAvailable( name ) || this.tools[ name ] )
) {
// Hack to prevent infinite recursion via ToolGroupTool. We need to reserve the tool
// before creating it, but we can't call reserveTool() yet because we haven't created
// the tool.
this.toolbar.tools[ name ] = true;
var tool = this.tools[ name ];
if ( !tool ) {
// Auto-initialize tools on first use
this.tools[ name ] = tool = toolFactory.create( name, this );
tool.updateTitle();
}
this.toolbar.reserveTool( tool );
add.push( tool );
names[ name ] = true;
}
}
// Remove tools that are no longer needed
for ( name in this.tools ) {
if ( !names[ name ] ) {
this.tools[ name ].destroy();
this.toolbar.releaseTool( this.tools[ name ] );
remove.push( this.tools[ name ] );
delete this.tools[ name ];
}
}
if ( remove.length ) {
this.removeItems( remove );
}
// Update emptiness state
this.$element.toggleClass( 'oo-ui-toolGroup-empty', !add.length );
// Re-add tools (moving existing ones to new locations)
this.addItems( add );
// Disabled state may depend on items
this.updateDisabled();
};
/**
* Destroy toolgroup.
*/
OO.ui.ToolGroup.prototype.destroy = function () {
this.clearItems();
this.toolbar.getToolFactory().disconnect( this );
for ( var name in this.tools ) {
this.toolbar.releaseTool( this.tools[ name ] );
this.tools[ name ].disconnect( this ).destroy();
delete this.tools[ name ];
}
this.$element.remove();
};
/**
* A ToolFactory creates tools on demand. All tools ({@link OO.ui.Tool Tools},
* {@link OO.ui.PopupTool PopupTools}, and {@link OO.ui.ToolGroupTool ToolGroupTools}) must be
* registered with a tool factory. Tools are registered by their symbolic name. See
* {@link OO.ui.Toolbar toolbars} for an example.
*
* For more information about toolbars in general, please see the
* [OOUI documentation on MediaWiki][1].
*
* [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars
*
* @class
* @extends OO.Factory
* @constructor
*/
OO.ui.ToolFactory = function OoUiToolFactory() {
// Parent constructor
OO.ui.ToolFactory.super.call( this );
};
/* Setup */
OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
/* Methods */
/**
* Get tools from the factory.
*
* @param {Array|string} include Included tools, see #extract for format
* @param {Array|string} exclude Excluded tools, see #extract for format
* @param {Array|string} promote Promoted tools, see #extract for format
* @param {Array|string} demote Demoted tools, see #extract for format
* @return {string[]} List of tools
*/
OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
var auto = [],
used = {};
// Collect included and not excluded tools
var included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
// Promotion
var promoted = this.extract( promote, used );
var demoted = this.extract( demote, used );
// Auto
for ( var i = 0, len = included.length; i < len; i++ ) {
if ( !used[ included[ i ] ] ) {
auto.push( included[ i ] );
}
}
return promoted.concat( auto ).concat( demoted );
};
/**
* Get a flat list of names from a list of names or groups.
*
* Normally, `collection` is an array of tool specifications. Tools can be specified in the
* following ways:
*
* - To include an individual tool, use the symbolic name: `{ name: 'tool-name' }` or `'tool-name'`.
* - To include all tools in a group, use the group name: `{ group: 'group-name' }`. (To assign the
* tool to a group, use OO.ui.Tool.static.group.)
*
* Alternatively, to include all tools that are not yet assigned to any other toolgroup, use the
* catch-all selector `'*'`.
*
* If `used` is passed, tool names that appear as properties in this object will be considered
* already assigned, and will not be returned even if specified otherwise. The tool names extracted
* by this function call will be added as new properties in the object.
*
* @private
* @param {Array|string} collection List of tools, see above
* @param {Object.<string,boolean>} [used] Object containing information about used tools, see above
* @return {string[]} List of extracted tool names
*/
OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
var names = [];
collection = !Array.isArray( collection ) ? [ collection ] : collection;
for ( var i = 0, len = collection.length; i < len; i++ ) {
var item = collection[ i ];
var name, tool;
if ( item === '*' ) {
for ( name in this.registry ) {
tool = this.registry[ name ];
if (
// Only add tools by group name when auto-add is enabled
tool.static.autoAddToCatchall &&
// Exclude already used tools
( !used || !used[ name ] )
) {
names.push( name );
if ( used ) {
used[ name ] = true;
}
}
}
} else {
// Allow plain strings as shorthand for named tools
if ( typeof item === 'string' ) {
item = { name: item };
}
if ( OO.isPlainObject( item ) ) {
if ( item.group ) {
for ( name in this.registry ) {
tool = this.registry[ name ];
if (
// Include tools with matching group
tool.static.group === item.group &&
// Only add tools by group name when auto-add is enabled
tool.static.autoAddToGroup &&
// Exclude already used tools
( !used || !used[ name ] )
) {
names.push( name );
if ( used ) {
used[ name ] = true;
}
}
}
// Include tools with matching name and exclude already used tools
} else if ( item.name && ( !used || !used[ item.name ] ) ) {
names.push( item.name );
if ( used ) {
used[ item.name ] = true;
}
}
}
}
}
return names;
};
/**
* ToolGroupFactories create {@link OO.ui.ToolGroup toolgroups} on demand. The toolgroup classes
* must specify a symbolic name and be registered with the factory. The following classes are
* registered by default:
*
* - {@link OO.ui.BarToolGroup BarToolGroups} (bar)
* - {@link OO.ui.MenuToolGroup MenuToolGroups} (menu)
* - {@link OO.ui.ListToolGroup ListToolGroups} (list)
*
* See {@link OO.ui.Toolbar toolbars} for an example.
*
* For more information about toolbars in general, please see the
* [OOUI documentation on MediaWiki][1].
*
* [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars
*
* @class
* @extends OO.Factory
* @constructor
*/
OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() {
// Parent constructor
OO.Factory.call( this );
var defaultClasses = this.constructor.static.getDefaultClasses();
// Register default toolgroups
for ( var i = 0, l = defaultClasses.length; i < l; i++ ) {
this.register( defaultClasses[ i ] );
}
};
/* Setup */
OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory );
/* Static Methods */
/**
* Get a default set of classes to be registered on construction.
*
* @return {Function[]} Default classes
*/
OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
return [
OO.ui.BarToolGroup,
OO.ui.ListToolGroup,
OO.ui.MenuToolGroup
];
};
/**
* Popup tools open a popup window when they are selected from the {@link OO.ui.Toolbar toolbar}.
* Each popup tool is configured with a static name, title, and icon, as well with as any popup
* configurations. Unlike other tools, popup tools do not require that developers specify an
* #onSelect or #onUpdateState method, as these methods have been implemented already.
*
* // Example of a popup tool. When selected, a popup tool displays
* // a popup window.
* function HelpTool( toolGroup, config ) {
* OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
* padded: true,
* label: 'Help',
* head: true
* } }, config ) );
* this.popup.$body.append( '<p>I am helpful!</p>' );
* };
* OO.inheritClass( HelpTool, OO.ui.PopupTool );
* HelpTool.static.name = 'help';
* HelpTool.static.icon = 'help';
* HelpTool.static.title = 'Help';
* toolFactory.register( HelpTool );
*
* For an example of a toolbar that contains a popup tool, see {@link OO.ui.Toolbar toolbars}.
* For more information about toolbars in general, please see the
* [OOUI documentation on MediaWiki][1].
*
* [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars
*
* @abstract
* @class
* @extends OO.ui.Tool
* @mixins OO.ui.mixin.PopupElement
*
* @constructor
* @param {OO.ui.ToolGroup} toolGroup
* @param {Object} [config] Configuration options
*/
OO.ui.PopupTool = function OoUiPopupTool( toolGroup, config ) {
// Allow passing positional parameters inside the config object
if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
config = toolGroup;
toolGroup = config.toolGroup;
}
// Parent constructor
OO.ui.PopupTool.super.call( this, toolGroup, config );
// Mixin constructors
OO.ui.mixin.PopupElement.call( this, config );
// Events
this.popup.connect( this, {
toggle: 'onPopupToggle'
} );
// Initialization
this.popup.setAutoFlip( false );
this.popup.setPosition( toolGroup.getToolbar().position === 'bottom' ? 'above' : 'below' );
this.$element.addClass( 'oo-ui-popupTool' );
this.popup.$element.addClass( 'oo-ui-popupTool-popup' );
this.toolbar.$popups.append( this.popup.$element );
};
/* Setup */
OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool );
OO.mixinClass( OO.ui.PopupTool, OO.ui.mixin.PopupElement );
/* Methods */
/**
* Handle the tool being selected.
*
* @inheritdoc
*/
OO.ui.PopupTool.prototype.onSelect = function () {
if ( !this.isDisabled() ) {
this.popup.toggle();
}
return false;
};
/**
* Handle the toolbar state being updated.
*
* @inheritdoc
*/
OO.ui.PopupTool.prototype.onUpdateState = function () {
};
/**
* Handle popup visibility being toggled.
*
* @param {boolean} isVisible
*/
OO.ui.PopupTool.prototype.onPopupToggle = function ( isVisible ) {
this.setActive( isVisible );
this.toolGroup.emit( 'active', isVisible );
};
/**
* A ToolGroupTool is a special sort of tool that can contain other {@link OO.ui.Tool tools}
* and {@link OO.ui.ToolGroup toolgroups}. The ToolGroupTool was specifically designed to be used
* inside a {@link OO.ui.BarToolGroup bar} toolgroup to provide access to additional tools from
* the bar item. Included tools will be displayed in a dropdown {@link OO.ui.ListToolGroup list}
* when the ToolGroupTool is selected.
*
* // Example: ToolGroupTool with two nested tools, 'setting1' and 'setting2',
* // defined elsewhere.
*
* function SettingsTool() {
* SettingsTool.super.apply( this, arguments );
* };
* OO.inheritClass( SettingsTool, OO.ui.ToolGroupTool );
* SettingsTool.static.name = 'settings';
* SettingsTool.static.title = 'Change settings';
* SettingsTool.static.groupConfig = {
* icon: 'settings',
* label: 'ToolGroupTool',
* include: [ 'setting1', 'setting2' ]
* };
* toolFactory.register( SettingsTool );
*
* For more information, please see the [OOUI documentation on MediaWiki][1].
*
* Please note that this implementation is subject to change per [T74159] [2].
*
* [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars#ToolGroupTool
* [2]: https://phabricator.wikimedia.org/T74159
*
* @abstract
* @class
* @extends OO.ui.Tool
*
* @constructor
* @param {OO.ui.ToolGroup} toolGroup
* @param {Object} [config] Configuration options
*/
OO.ui.ToolGroupTool = function OoUiToolGroupTool( toolGroup, config ) {
// Allow passing positional parameters inside the config object
if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
config = toolGroup;
toolGroup = config.toolGroup;
}
// Parent constructor
OO.ui.ToolGroupTool.super.call( this, toolGroup, config );
// Properties
this.innerToolGroup = this.createGroup( this.constructor.static.groupConfig );
// Events
this.innerToolGroup.connect( this, {
disable: 'onToolGroupDisable',
// Re-emit active events from the innerToolGroup on the parent toolGroup
active: this.toolGroup.emit.bind( this.toolGroup, 'active' )
} );
// Initialization
this.$link.remove();
this.$element
.addClass( 'oo-ui-toolGroupTool' )
.append( this.innerToolGroup.$element );
};
/* Setup */
OO.inheritClass( OO.ui.ToolGroupTool, OO.ui.Tool );
/* Static Properties */
/**
* Toolgroup configuration.
*
* The toolgroup configuration consists of the tools to include, as well as an icon and label
* to use for the bar item. Tools can be included by symbolic name, group, or with the
* wildcard selector. Please see {@link OO.ui.ToolGroup toolgroup} for more information.
*
* @property {Object.<string,Array>}
*/
OO.ui.ToolGroupTool.static.groupConfig = {};
/* Methods */
/**
* Handle the tool being selected.
*
* @inheritdoc
*/
OO.ui.ToolGroupTool.prototype.onSelect = function () {
this.innerToolGroup.setActive( !this.innerToolGroup.active );
return false;
};
/**
* Synchronize disabledness state of the tool with the inner toolgroup.
*
* @private
* @param {boolean} disabled Element is disabled
*/
OO.ui.ToolGroupTool.prototype.onToolGroupDisable = function ( disabled ) {
this.setDisabled( disabled );
};
/**
* Handle the toolbar state being updated.
*
* @inheritdoc
*/
OO.ui.ToolGroupTool.prototype.onUpdateState = function () {
this.setActive( false );
};
/**
* Build a {@link OO.ui.ToolGroup toolgroup} from the specified configuration.
*
* @param {Object.<string,Array>} group Toolgroup configuration. Please see
* {@link OO.ui.ToolGroup toolgroup} for more information.
* @return {OO.ui.ListToolGroup}
*/
OO.ui.ToolGroupTool.prototype.createGroup = function ( group ) {
if ( group.include === '*' ) {
// Apply defaults to catch-all groups
if ( group.label === undefined ) {
group.label = OO.ui.msg( 'ooui-toolbar-more' );
}
}
return this.toolbar.getToolGroupFactory().create( 'list', this.toolbar, group );
};
/**
* BarToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
* create {@link OO.ui.Toolbar toolbars} (the other types of groups are
* {@link OO.ui.MenuToolGroup MenuToolGroup} and {@link OO.ui.ListToolGroup ListToolGroup}).
* The {@link OO.ui.Tool tools} in a BarToolGroup are displayed by icon in a single row. The
* title of the tool is displayed when users move the mouse over the tool.
*
* BarToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar
* is set up.
*
* @example
* // Example of a BarToolGroup with two tools
* var toolFactory = new OO.ui.ToolFactory();
* var toolGroupFactory = new OO.ui.ToolGroupFactory();
* var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
*
* // We will be placing status text in this element when tools are used
* var $area = $( '<p>' ).text( 'Example of a BarToolGroup with two tools.' );
*
* // Define the tools that we're going to place in our toolbar
*
* // Create a class inheriting from OO.ui.Tool
* function SearchTool() {
* SearchTool.super.apply( this, arguments );
* }
* OO.inheritClass( SearchTool, OO.ui.Tool );
* // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
* // of 'icon' and 'title' (displayed icon and text).
* SearchTool.static.name = 'search';
* SearchTool.static.icon = 'search';
* SearchTool.static.title = 'Search...';
* // Defines the action that will happen when this tool is selected (clicked).
* SearchTool.prototype.onSelect = function () {
* $area.text( 'Search tool clicked!' );
* // Never display this tool as "active" (selected).
* this.setActive( false );
* };
* SearchTool.prototype.onUpdateState = function () {};
* // Make this tool available in our toolFactory and thus our toolbar
* toolFactory.register( SearchTool );
*
* // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
* // little popup window (a PopupWidget).
* function HelpTool( toolGroup, config ) {
* OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
* padded: true,
* label: 'Help',
* head: true
* } }, config ) );
* this.popup.$body.append( '<p>I am helpful!</p>' );
* }
* OO.inheritClass( HelpTool, OO.ui.PopupTool );
* HelpTool.static.name = 'help';
* HelpTool.static.icon = 'help';
* HelpTool.static.title = 'Help';
* toolFactory.register( HelpTool );
*
* // Finally define which tools and in what order appear in the toolbar. Each tool may only be
* // used once (but not all defined tools must be used).
* toolbar.setup( [
* {
* // 'bar' tool groups display tools by icon only
* type: 'bar',
* include: [ 'search', 'help' ]
* }
* ] );
*
* // Create some UI around the toolbar and place it in the document
* var frame = new OO.ui.PanelLayout( {
* expanded: false,
* framed: true
* } );
* var contentFrame = new OO.ui.PanelLayout( {
* expanded: false,
* padded: true
* } );
* frame.$element.append(
* toolbar.$element,
* contentFrame.$element.append( $area )
* );
* $( document.body ).append( frame.$element );
*
* // Here is where the toolbar is actually built. This must be done after inserting it into the
* // document.
* toolbar.initialize();
*
* For more information about how to add tools to a bar tool group, please see
* {@link OO.ui.ToolGroup toolgroup}.
* For more information about toolbars in general, please see the
* [OOUI documentation on MediaWiki][1].
*
* [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars
*
* @class
* @extends OO.ui.ToolGroup
*
* @constructor
* @param {OO.ui.Toolbar} toolbar
* @param {Object} [config] Configuration options
*/
OO.ui.BarToolGroup = function OoUiBarToolGroup( toolbar, config ) {
// Allow passing positional parameters inside the config object
if ( OO.isPlainObject( toolbar ) && config === undefined ) {
config = toolbar;
toolbar = config.toolbar;
}
// Parent constructor
OO.ui.BarToolGroup.super.call( this, toolbar, config );
// Initialization
this.$element.addClass( 'oo-ui-barToolGroup' );
this.$group.addClass( 'oo-ui-barToolGroup-tools' );
};
/* Setup */
OO.inheritClass( OO.ui.BarToolGroup, OO.ui.ToolGroup );
/* Static Properties */
/**
* @static
* @inheritdoc
*/
OO.ui.BarToolGroup.static.titleTooltips = true;
/**
* @static
* @inheritdoc
*/
OO.ui.BarToolGroup.static.accelTooltips = true;
/**
* @static
* @inheritdoc
*/
OO.ui.BarToolGroup.static.name = 'bar';
/**
* PopupToolGroup is an abstract base class used by both {@link OO.ui.MenuToolGroup MenuToolGroup}
* and {@link OO.ui.ListToolGroup ListToolGroup} to provide a popup (an overlaid menu or list of
* tools with an optional icon and label). This class can be used for other base classes that
* also use this functionality.
*
* @abstract
* @class
* @extends OO.ui.ToolGroup
* @mixins OO.ui.mixin.IconElement
* @mixins OO.ui.mixin.IndicatorElement
* @mixins OO.ui.mixin.LabelElement
* @mixins OO.ui.mixin.TitledElement
* @mixins OO.ui.mixin.FlaggedElement
* @mixins OO.ui.mixin.ClippableElement
* @mixins OO.ui.mixin.FloatableElement
* @mixins OO.ui.mixin.TabIndexedElement
*
* @constructor
* @param {OO.ui.Toolbar} toolbar
* @param {Object} [config] Configuration options
* @cfg {string} [header] Text to display at the top of the popup
* @cfg {Object} [narrowConfig] See static.narrowConfig
*/
OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
// Allow passing positional parameters inside the config object
if ( OO.isPlainObject( toolbar ) && config === undefined ) {
config = toolbar;
toolbar = config.toolbar;
}
// Configuration initialization
config = $.extend( {
indicator: config.indicator === undefined ?
( toolbar.position === 'bottom' ? 'up' : 'down' ) : config.indicator
}, config );
// Parent constructor
OO.ui.PopupToolGroup.super.call( this, toolbar, config );
// Properties
this.active = false;
this.dragging = false;
// Don't conflict with parent method of the same name
this.onPopupDocumentMouseKeyUpHandler = this.onPopupDocumentMouseKeyUp.bind( this );
this.$handle = $( '<span>' );
this.narrowConfig = config.narrowConfig || this.constructor.static.narrowConfig;
// Mixin constructors
OO.ui.mixin.IconElement.call( this, config );
OO.ui.mixin.IndicatorElement.call( this, config );
OO.ui.mixin.LabelElement.call( this, config );
OO.ui.mixin.TitledElement.call( this, config );
OO.ui.mixin.FlaggedElement.call( this, config );
OO.ui.mixin.ClippableElement.call( this, $.extend( {
$clippable: this.$group
}, config ) );
OO.ui.mixin.FloatableElement.call( this, $.extend( {
$floatable: this.$group,
$floatableContainer: this.$handle,
hideWhenOutOfView: false,
verticalPosition: this.toolbar.position === 'bottom' ? 'above' : 'below'
// horizontalPosition is set in setActive
}, config ) );
OO.ui.mixin.TabIndexedElement.call( this, $.extend( {
$tabIndexed: this.$handle
}, config ) );
// Events
this.$handle.on( {
keydown: this.onHandleMouseKeyDown.bind( this ),
keyup: this.onHandleMouseKeyUp.bind( this ),
mousedown: this.onHandleMouseKeyDown.bind( this ),
mouseup: this.onHandleMouseKeyUp.bind( this )
} );
this.toolbar.connect( this, {
resize: 'onToolbarResize'
} );
// Initialization
this.$handle
.addClass( 'oo-ui-popupToolGroup-handle' )
.attr( { role: 'button', 'aria-expanded': 'false' } )
.append( this.$icon, this.$label, this.$indicator );
// If the pop-up should have a header, add it to the top of the toolGroup.
// Note: If this feature is useful for other widgets, we could abstract it into an
// OO.ui.HeaderedElement mixin constructor.
if ( config.header !== undefined ) {
this.$group
.prepend( $( '<span>' )
.addClass( 'oo-ui-popupToolGroup-header' )
.text( config.header )
);
}
this.$element
.addClass( 'oo-ui-popupToolGroup' )
.prepend( this.$handle );
this.$group.addClass( 'oo-ui-popupToolGroup-tools' );
this.toolbar.$popups.append( this.$group );
};
/* Setup */
OO.inheritClass( OO.ui.PopupToolGroup, OO.ui.ToolGroup );
OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.IconElement );
OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.IndicatorElement );
OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.LabelElement );
OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.TitledElement );
OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.FlaggedElement );
OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.ClippableElement );
OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.FloatableElement );
OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.TabIndexedElement );
/* Static properties */
/**
* Config options to change when toolbar is in narrow mode
*
* Supports `invisibleLabel`, label` and `icon` properties.
*
* @static
* @inheritable
* @property {Object|null}
*/
OO.ui.PopupToolGroup.static.narrowConfig = null;
/* Methods */
/**
* @inheritdoc
*/
OO.ui.PopupToolGroup.prototype.setDisabled = function () {
// Parent method
OO.ui.PopupToolGroup.super.prototype.setDisabled.apply( this, arguments );
if ( this.isDisabled() && this.isElementAttached() ) {
this.setActive( false );
}
};
/**
* Handle resize events from the toolbar
*/
OO.ui.PopupToolGroup.prototype.onToolbarResize = function () {
if ( !this.narrowConfig ) {
return;
}
if ( this.toolbar.isNarrow() ) {
if ( this.narrowConfig.invisibleLabel !== undefined ) {
this.wideInvisibleLabel = this.invisibleLabel;
this.setInvisibleLabel( this.narrowConfig.invisibleLabel );
}
if ( this.narrowConfig.label !== undefined ) {
this.wideLabel = this.label;
this.setLabel( this.narrowConfig.label );
}
if ( this.narrowConfig.icon !== undefined ) {
this.wideIcon = this.icon;
this.setIcon( this.narrowConfig.icon );
}
} else {
if ( this.wideInvisibleLabel !== undefined ) {
this.setInvisibleLabel( this.wideInvisibleLabel );
}
if ( this.wideLabel !== undefined ) {
this.setLabel( this.wideLabel );
}
if ( this.wideIcon !== undefined ) {
this.setIcon( this.wideIcon );
}
}
};
/**
* Handle document mouse up and key up events.
*
* @protected
* @param {MouseEvent|KeyboardEvent} e Mouse up or key up event
*/
OO.ui.PopupToolGroup.prototype.onPopupDocumentMouseKeyUp = function ( e ) {
var $target = $( e.target );
// Only deactivate when clicking outside the dropdown element
if ( $target.closest( '.oo-ui-popupToolGroup' )[ 0 ] === this.$element[ 0 ] ) {
return;
}
if ( $target.closest( '.oo-ui-popupToolGroup-tools' )[ 0 ] === this.$group[ 0 ] ) {
return;
}
this.setActive( false );
};
/**
* @inheritdoc
*/
OO.ui.PopupToolGroup.prototype.onMouseKeyUp = function ( e ) {
// Only close toolgroup when a tool was actually selected
if (
!this.isDisabled() && this.pressed && this.pressed === this.findTargetTool( e ) && (
e.which === OO.ui.MouseButtons.LEFT ||
e.which === OO.ui.Keys.SPACE ||
e.which === OO.ui.Keys.ENTER
)
) {
this.setActive( false );
}
return OO.ui.PopupToolGroup.super.prototype.onMouseKeyUp.call( this, e );
};
/**
* @inheritdoc
*/
OO.ui.PopupToolGroup.prototype.onMouseKeyDown = function ( e ) {
// Shift-Tab on the first tool in the group jumps to the handle.
// Tab on the last tool in the group jumps to the next group.
if ( !this.isDisabled() && e.which === OO.ui.Keys.TAB ) {
// We can't use this.items because ListToolGroup inserts the extra fake
// expand/collapse tool.
var $focused = $( document.activeElement );
var $firstFocusable = OO.ui.findFocusable( this.$group );
if ( $focused[ 0 ] === $firstFocusable[ 0 ] && e.shiftKey ) {
this.$handle.trigger( 'focus' );
return false;
}
var $lastFocusable = OO.ui.findFocusable( this.$group, true );
if ( $focused[ 0 ] === $lastFocusable[ 0 ] && !e.shiftKey ) {
// Focus this group's handle and let the browser's tab handling happen
// (no 'return false').
// This way we don't have to fiddle with other ToolGroups' business, or worry what to do
// if the next group is not a PopupToolGroup or doesn't exist at all.
this.$handle.trigger( 'focus' );
// Close the popup so that we don't move back inside it (if this is the last group).
this.setActive( false );
}
}
return OO.ui.PopupToolGroup.super.prototype.onMouseKeyDown.call( this, e );
};
/**
* Handle mouse up and key up events.
*
* @protected
* @param {jQuery.Event} e Mouse up or key up event
* @return {undefined|boolean} False to prevent default if event is handled
*/
OO.ui.PopupToolGroup.prototype.onHandleMouseKeyUp = function ( e ) {
if (
!this.isDisabled() && (
e.which === OO.ui.MouseButtons.LEFT ||
e.which === OO.ui.Keys.SPACE ||
e.which === OO.ui.Keys.ENTER
)
) {
return false;
}
};
/**
* Handle mouse down and key down events.
*
* @protected
* @param {jQuery.Event} e Mouse down or key down event
* @return {undefined|boolean} False to prevent default if event is handled
*/
OO.ui.PopupToolGroup.prototype.onHandleMouseKeyDown = function ( e ) {
var $focusable;
if ( !this.isDisabled() ) {
// Tab on the handle jumps to the first tool in the group (if the popup is open).
if ( e.which === OO.ui.Keys.TAB && !e.shiftKey ) {
$focusable = OO.ui.findFocusable( this.$group );
if ( $focusable.length ) {
$focusable.trigger( 'focus' );
return false;
}
}
if (
e.which === OO.ui.MouseButtons.LEFT ||
e.which === OO.ui.Keys.SPACE ||
e.which === OO.ui.Keys.ENTER
) {
this.setActive( !this.active );
return false;
}
}
};
/**
* Check if the tool group is active.
*
* @return {boolean} Tool group is active
*/
OO.ui.PopupToolGroup.prototype.isActive = function () {
return this.active;
};
/**
* Switch into 'active' mode.
*
* When active, the popup is visible. A mouseup event anywhere in the document will trigger
* deactivation.
*
* @param {boolean} [value=false] The active state to set
* @fires active
*/
OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
var containerWidth, containerLeft;
value = !!value;
if ( this.active !== value ) {
this.active = value;
if ( value ) {
this.getElementDocument().addEventListener(
'mouseup',
this.onPopupDocumentMouseKeyUpHandler,
true
);
this.getElementDocument().addEventListener(
'keyup',
this.onPopupDocumentMouseKeyUpHandler,
true
);
this.$clippable.css( 'left', '' );
this.$element.addClass( 'oo-ui-popupToolGroup-active' );
this.$group.addClass( 'oo-ui-popupToolGroup-active-tools' );
this.$handle.attr( 'aria-expanded', true );
this.togglePositioning( true );
this.toggleClipping( true );
// Tools on the left of the toolbar will try to align their
// popups with their left side if possible, and vice-versa.
var preferredSide = this.align === 'before' ? 'start' : 'end';
var otherSide = this.align === 'before' ? 'end' : 'start';
// Try anchoring the popup to the preferred side first
this.setHorizontalPosition( preferredSide );
if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
// Anchoring to the preferred side caused the popup to clip, so anchor it
// to the other side instead.
this.setHorizontalPosition( otherSide );
}
if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
// Anchoring to the right also caused the popup to clip, so just make it fill the
// container.
containerWidth = this.$clippableScrollableContainer.width();
containerLeft = this.$clippableScrollableContainer[ 0 ] ===
document.documentElement ?
0 :
this.$clippableScrollableContainer.offset().left;
this.toggleClipping( false );
this.setHorizontalPosition( preferredSide );
this.$clippable.css( {
'margin-left': -( this.$element.offset().left - containerLeft ),
width: containerWidth
} );
}
} else {
this.getElementDocument().removeEventListener(
'mouseup',
this.onPopupDocumentMouseKeyUpHandler,
true
);
this.getElementDocument().removeEventListener(
'keyup',
this.onPopupDocumentMouseKeyUpHandler,
true
);
this.$element.removeClass( 'oo-ui-popupToolGroup-active' );
this.$group.removeClass( 'oo-ui-popupToolGroup-active-tools' );
this.$handle.attr( 'aria-expanded', false );
this.togglePositioning( false );
this.toggleClipping( false );
}
this.emit( 'active', this.active );
this.updateThemeClasses();
}
};
/**
* ListToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
* create {@link OO.ui.Toolbar toolbars} (the other types of groups are
* {@link OO.ui.MenuToolGroup MenuToolGroup} and {@link OO.ui.BarToolGroup BarToolGroup}).
* The {@link OO.ui.Tool tools} in a ListToolGroup are displayed by label in a dropdown menu.
* The title of the tool is used as the label text. The menu itself can be configured with a label,
* icon, indicator, header, and title.
*
* ListToolGroups can be configured to be expanded and collapsed. Collapsed lists will have a
* More option that users can select to see the full list of tools. If a collapsed toolgroup is
* expanded, a Fewer option permits users to collapse the list again.
*
* ListToolGroups are created by a {@link OO.ui.ToolGroupFactory toolgroup factory} when the
* toolbar is set up. The factory requires the ListToolGroup's symbolic name, 'list', which is
* specified along with the other configurations. For more information about how to add tools to a
* ListToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
*
* @example
* // Example of a ListToolGroup
* var toolFactory = new OO.ui.ToolFactory();
* var toolGroupFactory = new OO.ui.ToolGroupFactory();
* var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
*
* // Configure and register two tools
* function SettingsTool() {
* SettingsTool.super.apply( this, arguments );
* }
* OO.inheritClass( SettingsTool, OO.ui.Tool );
* SettingsTool.static.name = 'settings';
* SettingsTool.static.icon = 'settings';
* SettingsTool.static.title = 'Change settings';
* SettingsTool.prototype.onSelect = function () {
* this.setActive( false );
* };
* SettingsTool.prototype.onUpdateState = function () {};
* toolFactory.register( SettingsTool );
* // Register two more tools, nothing interesting here
* function StuffTool() {
* StuffTool.super.apply( this, arguments );
* }
* OO.inheritClass( StuffTool, OO.ui.Tool );
* StuffTool.static.name = 'stuff';
* StuffTool.static.icon = 'search';
* StuffTool.static.title = 'Change the world';
* StuffTool.prototype.onSelect = function () {
* this.setActive( false );
* };
* StuffTool.prototype.onUpdateState = function () {};
* toolFactory.register( StuffTool );
* toolbar.setup( [
* {
* // Configurations for list toolgroup.
* type: 'list',
* label: 'ListToolGroup',
* icon: 'ellipsis',
* title: 'This is the title, displayed when user moves the mouse over the list ' +
* 'toolgroup',
* header: 'This is the header',
* include: [ 'settings', 'stuff' ],
* allowCollapse: ['stuff']
* }
* ] );
*
* // Create some UI around the toolbar and place it in the document
* var frame = new OO.ui.PanelLayout( {
* expanded: false,
* framed: true
* } );
* frame.$element.append(
* toolbar.$element
* );
* $( document.body ).append( frame.$element );
* // Build the toolbar. This must be done after the toolbar has been appended to the document.
* toolbar.initialize();
*
* For more information about toolbars in general, please see the
* [OOUI documentation on MediaWiki][1].
*
* [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars
*
* @class
* @extends OO.ui.PopupToolGroup
*
* @constructor
* @param {OO.ui.Toolbar} toolbar
* @param {Object} [config] Configuration options
* @cfg {Array} [allowCollapse] Allow the specified tools to be collapsed. By default, collapsible
* tools will only be displayed if users click the More option displayed at the bottom of the
* list. If the list is expanded, a Fewer option permits users to collapse the list again.
* Any tools that are included in the toolgroup, but are not designated as collapsible, will always
* be displayed.
* To open a collapsible list in its expanded state, set #expanded to 'true'.
* @cfg {Array} [forceExpand] Expand the specified tools. All other tools will be designated as
* collapsible. Unless #expanded is set to true, the collapsible tools will be collapsed when the
* list is first opened.
* @cfg {boolean} [expanded=false] Expand collapsible tools. This config is only relevant if tools
* have been designated as collapsible. When expanded is set to true, all tools in the group will
* be displayed when the list is first opened. Users can collapse the list with a Fewer option at
* the bottom.
*/
OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
// Allow passing positional parameters inside the config object
if ( OO.isPlainObject( toolbar ) && config === undefined ) {
config = toolbar;
toolbar = config.toolbar;
}
// Configuration initialization
config = config || {};
// Properties (must be set before parent constructor, which calls #populate)
this.allowCollapse = config.allowCollapse;
this.forceExpand = config.forceExpand;
this.expanded = config.expanded !== undefined ? config.expanded : false;
this.collapsibleTools = [];
// Parent constructor
OO.ui.ListToolGroup.super.call( this, toolbar, config );
// Initialization
this.$element.addClass( 'oo-ui-listToolGroup' );
this.$group.addClass( 'oo-ui-listToolGroup-tools' );
};
/* Setup */
OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup );
/* Static Properties */
/**
* @static
* @inheritdoc
*/
OO.ui.ListToolGroup.static.name = 'list';
/* Methods */
/**
* @inheritdoc
*/
OO.ui.ListToolGroup.prototype.populate = function () {
OO.ui.ListToolGroup.super.prototype.populate.call( this );
var allowCollapse = [];
// Update the list of collapsible tools
if ( this.allowCollapse !== undefined ) {
allowCollapse = this.allowCollapse;
} else if ( this.forceExpand !== undefined ) {
allowCollapse = OO.simpleArrayDifference( Object.keys( this.tools ), this.forceExpand );
}
this.collapsibleTools = [];
for ( var i = 0, len = allowCollapse.length; i < len; i++ ) {
if ( this.tools[ allowCollapse[ i ] ] !== undefined ) {
this.collapsibleTools.push( this.tools[ allowCollapse[ i ] ] );
}
}
// Keep at the end, even when tools are added
this.$group.append( this.getExpandCollapseTool().$element );
this.getExpandCollapseTool().toggle( this.collapsibleTools.length !== 0 );
this.updateCollapsibleState();
};
/**
* Get the expand/collapse tool for this group
*
* @return {OO.ui.Tool} Expand collapse tool
*/
OO.ui.ListToolGroup.prototype.getExpandCollapseTool = function () {
if ( this.expandCollapseTool === undefined ) {
var ExpandCollapseTool = function () {
ExpandCollapseTool.super.apply( this, arguments );
};
OO.inheritClass( ExpandCollapseTool, OO.ui.Tool );
ExpandCollapseTool.prototype.onSelect = function () {
this.toolGroup.expanded = !this.toolGroup.expanded;
this.toolGroup.updateCollapsibleState();
this.setActive( false );
};
ExpandCollapseTool.prototype.onUpdateState = function () {
// Do nothing. Tool interface requires an implementation of this function.
};
ExpandCollapseTool.static.name = 'more-fewer';
this.expandCollapseTool = new ExpandCollapseTool( this );
}
return this.expandCollapseTool;
};
/**
* @inheritdoc
*/
OO.ui.ListToolGroup.prototype.onMouseKeyUp = function ( e ) {
// Do not close the popup when the user wants to show more/fewer tools
if (
$( e.target ).closest( '.oo-ui-tool-name-more-fewer' ).length && (
e.which === OO.ui.MouseButtons.LEFT ||
e.which === OO.ui.Keys.SPACE ||
e.which === OO.ui.Keys.ENTER
)
) {
// HACK: Prevent the popup list from being hidden. Skip the PopupToolGroup implementation
// (which hides the popup list when a tool is selected) and call ToolGroup's implementation
// directly.
return OO.ui.ListToolGroup.super.super.prototype.onMouseKeyUp.call( this, e );
} else {
return OO.ui.ListToolGroup.super.prototype.onMouseKeyUp.call( this, e );
}
};
OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () {
var inverted = this.toolbar.position === 'bottom',
icon = this.expanded === inverted ? 'expand' : 'collapse';
this.getExpandCollapseTool()
.setIcon( icon )
.setTitle( OO.ui.msg( this.expanded ? 'ooui-toolgroup-collapse' : 'ooui-toolgroup-expand' ) );
for ( var i = 0; i < this.collapsibleTools.length; i++ ) {
this.collapsibleTools[ i ].toggle( this.expanded );
}
// Re-evaluate clipping, because our height has changed
this.clip();
};
/**
* MenuToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
* create {@link OO.ui.Toolbar toolbars} (the other types of groups are
* {@link OO.ui.BarToolGroup BarToolGroup} and {@link OO.ui.ListToolGroup ListToolGroup}).
* MenuToolGroups contain selectable {@link OO.ui.Tool tools}, which are displayed by label in a
* dropdown menu. The tool's title is used as the label text, and the menu label is updated to
* reflect which tool or tools are currently selected. If no tools are selected, the menu label
* is empty. The menu can be configured with an indicator, icon, title, and/or header.
*
* MenuToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the
* toolbar is set up.
*
* @example
* // Example of a MenuToolGroup
* var toolFactory = new OO.ui.ToolFactory();
* var toolGroupFactory = new OO.ui.ToolGroupFactory();
* var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
*
* // We will be placing status text in this element when tools are used
* var $area = $( '<p>' ).text( 'An example of a MenuToolGroup. Select a tool from the '
* + 'dropdown menu.' );
*
* // Define the tools that we're going to place in our toolbar
*
* function SettingsTool() {
* SettingsTool.super.apply( this, arguments );
* this.reallyActive = false;
* }
* OO.inheritClass( SettingsTool, OO.ui.Tool );
* SettingsTool.static.name = 'settings';
* SettingsTool.static.icon = 'settings';
* SettingsTool.static.title = 'Change settings';
* SettingsTool.prototype.onSelect = function () {
* $area.text( 'Settings tool clicked!' );
* // Toggle the active state on each click
* this.reallyActive = !this.reallyActive;
* this.setActive( this.reallyActive );
* // To update the menu label
* this.toolbar.emit( 'updateState' );
* };
* SettingsTool.prototype.onUpdateState = function () {};
* toolFactory.register( SettingsTool );
*
* function StuffTool() {
* StuffTool.super.apply( this, arguments );
* this.reallyActive = false;
* }
* OO.inheritClass( StuffTool, OO.ui.Tool );
* StuffTool.static.name = 'stuff';
* StuffTool.static.icon = 'ellipsis';
* StuffTool.static.title = 'More stuff';
* StuffTool.prototype.onSelect = function () {
* $area.text( 'More stuff tool clicked!' );
* // Toggle the active state on each click
* this.reallyActive = !this.reallyActive;
* this.setActive( this.reallyActive );
* // To update the menu label
* this.toolbar.emit( 'updateState' );
* };
* StuffTool.prototype.onUpdateState = function () {};
* toolFactory.register( StuffTool );
*
* // Finally define which tools and in what order appear in the toolbar. Each tool may only be
* // used once (but not all defined tools must be used).
* toolbar.setup( [
* {
* type: 'menu',
* header: 'This is the (optional) header',
* title: 'This is the (optional) title',
* include: [ 'settings', 'stuff' ]
* }
* ] );
*
* // Create some UI around the toolbar and place it in the document
* var frame = new OO.ui.PanelLayout( {
* expanded: false,
* framed: true
* } );
* var contentFrame = new OO.ui.PanelLayout( {
* expanded: false,
* padded: true
* } );
* frame.$element.append(
* toolbar.$element,
* contentFrame.$element.append( $area )
* );
* $( document.body ).append( frame.$element );
*
* // Here is where the toolbar is actually built. This must be done after inserting it into the
* // document.
* toolbar.initialize();
* toolbar.emit( 'updateState' );
*
* For more information about how to add tools to a MenuToolGroup, please see
* {@link OO.ui.ToolGroup toolgroup}.
* For more information about toolbars in general, please see the
* [OOUI documentation on MediaWiki] [1].
*
* [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars
*
* @class
* @extends OO.ui.PopupToolGroup
*
* @constructor
* @param {OO.ui.Toolbar} toolbar
* @param {Object} [config] Configuration options
*/
OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
// Allow passing positional parameters inside the config object
if ( OO.isPlainObject( toolbar ) && config === undefined ) {
config = toolbar;
toolbar = config.toolbar;
}
// Configuration initialization
config = config || {};
// Parent constructor
OO.ui.MenuToolGroup.super.call( this, toolbar, config );
// Events
this.toolbar.connect( this, {
updateState: 'onUpdateState'
} );
// Initialization
this.$element.addClass( 'oo-ui-menuToolGroup' );
this.$group.addClass( 'oo-ui-menuToolGroup-tools' );
};
/* Setup */
OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup );
/* Static Properties */
/**
* @static
* @inheritdoc
*/
OO.ui.MenuToolGroup.static.name = 'menu';
/* Methods */
/**
* Handle the toolbar state being updated.
*
* When the state changes, the title of each active item in the menu will be joined together and
* used as a label for the group. The label will be empty if none of the items are active.
*
* @private
*/
OO.ui.MenuToolGroup.prototype.onUpdateState = function () {
var labelTexts = [];
for ( var name in this.tools ) {
if ( this.tools[ name ].isActive() ) {
labelTexts.push( this.tools[ name ].getTitle() );
}
}
this.setLabel( labelTexts.join( ', ' ) || ' ' );
};
}( OO ) );
//# sourceMappingURL=oojs-ui-toolbars.js.map.json