3 * https://www.mediawiki.org/wiki/OOUI
5 * Copyright 2011–2024 OOUI Team and other contributors.
6 * Released under the MIT license
7 * http://oojs.mit-license.org
9 * Date: 2024-12-05T17:34:41Z
16 * Toolbars are complex interface components that permit users to easily access a variety
17 * of {@link OO.ui.Tool tools} (e.g., formatting commands) and actions, which are additional
18 * commands that are part of the toolbar, but not configured as tools.
20 * Individual tools are customized and then registered with a
21 * {@link OO.ui.ToolFactory tool factory}, which creates the tools on demand. Each tool has a
22 * symbolic name (used when registering the tool), a title (e.g., ‘Insert image’), and an icon.
24 * Individual tools are organized in {@link OO.ui.ToolGroup toolgroups}, which can be
25 * {@link OO.ui.MenuToolGroup menus} of tools, {@link OO.ui.ListToolGroup lists} of tools, or a
26 * single {@link OO.ui.BarToolGroup bar} of tools. The arrangement and order of the toolgroups is
27 * customized when the toolbar is set up. Tools can be presented in any order, but each can only
28 * appear once in the toolbar.
30 * The toolbar can be synchronized with the state of the external "application", like a text
31 * editor's editing area, marking tools as active/inactive (e.g. a 'bold' tool would be shown as
32 * active when the text cursor was inside bolded text) or enabled/disabled (e.g. a table caption
33 * tool would be disabled while the user is not editing a table). A state change is signalled by
34 * emitting the {@link OO.ui.Toolbar#event:updateState 'updateState' event}, which calls Tools'
35 * {@link OO.ui.Tool#onUpdateState onUpdateState method}.
37 * @example <caption>The following is an example of a basic toolbar.</caption>
38 * // Example of a toolbar
39 * // Create the toolbar
40 * const toolFactory = new OO.ui.ToolFactory();
41 * const toolGroupFactory = new OO.ui.ToolGroupFactory();
42 * const toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
44 * // We will be placing status text in this element when tools are used
45 * const $area = $( '<p>' ).text( 'Toolbar example' );
47 * // Define the tools that we're going to place in our toolbar
49 * // Create a class inheriting from OO.ui.Tool
50 * function SearchTool() {
51 * SearchTool.super.apply( this, arguments );
53 * OO.inheritClass( SearchTool, OO.ui.Tool );
54 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
55 * // of 'icon' and 'title' (displayed icon and text).
56 * SearchTool.static.name = 'search';
57 * SearchTool.static.icon = 'search';
58 * SearchTool.static.title = 'Search...';
59 * // Defines the action that will happen when this tool is selected (clicked).
60 * SearchTool.prototype.onSelect = function () {
61 * $area.text( 'Search tool clicked!' );
62 * // Never display this tool as "active" (selected).
63 * this.setActive( false );
65 * SearchTool.prototype.onUpdateState = function () {};
66 * // Make this tool available in our toolFactory and thus our toolbar
67 * toolFactory.register( SearchTool );
69 * // Register two more tools, nothing interesting here
70 * function SettingsTool() {
71 * SettingsTool.super.apply( this, arguments );
73 * OO.inheritClass( SettingsTool, OO.ui.Tool );
74 * SettingsTool.static.name = 'settings';
75 * SettingsTool.static.icon = 'settings';
76 * SettingsTool.static.title = 'Change settings';
77 * SettingsTool.prototype.onSelect = function () {
78 * $area.text( 'Settings tool clicked!' );
79 * this.setActive( false );
81 * SettingsTool.prototype.onUpdateState = function () {};
82 * toolFactory.register( SettingsTool );
84 * // Register two more tools, nothing interesting here
85 * function StuffTool() {
86 * StuffTool.super.apply( this, arguments );
88 * OO.inheritClass( StuffTool, OO.ui.Tool );
89 * StuffTool.static.name = 'stuff';
90 * StuffTool.static.icon = 'ellipsis';
91 * StuffTool.static.title = 'More stuff';
92 * StuffTool.prototype.onSelect = function () {
93 * $area.text( 'More stuff tool clicked!' );
94 * this.setActive( false );
96 * StuffTool.prototype.onUpdateState = function () {};
97 * toolFactory.register( StuffTool );
99 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
100 * // little popup window (a PopupWidget).
101 * function HelpTool( toolGroup, config ) {
102 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
107 * this.popup.$body.append( '<p>I am helpful!</p>' );
109 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
110 * HelpTool.static.name = 'help';
111 * HelpTool.static.icon = 'help';
112 * HelpTool.static.title = 'Help';
113 * toolFactory.register( HelpTool );
115 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
116 * // used once (but not all defined tools must be used).
119 * // 'bar' tool groups display tools' icons only, side-by-side.
121 * include: [ 'search', 'help' ]
124 * // 'list' tool groups display both the titles and icons, in a dropdown list.
128 * include: [ 'settings', 'stuff' ]
130 * // Note how the tools themselves are toolgroup-agnostic - the same tool can be displayed
131 * // either in a 'list' or a 'bar'. There is a 'menu' tool group too, not showcased here,
132 * // since it's more complicated to use. (See the next example snippet on this page.)
135 * // Create some UI around the toolbar and place it in the document
136 * const frame = new OO.ui.PanelLayout( {
140 * const contentFrame = new OO.ui.PanelLayout( {
144 * frame.$element.append(
146 * contentFrame.$element.append( $area )
148 * $( document.body ).append( frame.$element );
150 * // Here is where the toolbar is actually built. This must be done after inserting it into the
152 * toolbar.initialize();
153 * toolbar.emit( 'updateState' );
155 * @example <caption>The following example extends the previous one to illustrate 'menu' toolgroups and the usage of
156 * {@link OO.ui.Toolbar#event:updateState 'updateState' event}.</caption>
157 * // Create the toolbar
158 * const toolFactory = new OO.ui.ToolFactory();
159 * const toolGroupFactory = new OO.ui.ToolGroupFactory();
160 * const toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
162 * // We will be placing status text in this element when tools are used
163 * const $area = $( '<p>' ).text( 'Toolbar example' );
165 * // Define the tools that we're going to place in our toolbar
167 * // Create a class inheriting from OO.ui.Tool
168 * function SearchTool() {
169 * SearchTool.super.apply( this, arguments );
171 * OO.inheritClass( SearchTool, OO.ui.Tool );
172 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
173 * // of 'icon' and 'title' (displayed icon and text).
174 * SearchTool.static.name = 'search';
175 * SearchTool.static.icon = 'search';
176 * SearchTool.static.title = 'Search...';
177 * // Defines the action that will happen when this tool is selected (clicked).
178 * SearchTool.prototype.onSelect = function () {
179 * $area.text( 'Search tool clicked!' );
180 * // Never display this tool as "active" (selected).
181 * this.setActive( false );
183 * SearchTool.prototype.onUpdateState = function () {};
184 * // Make this tool available in our toolFactory and thus our toolbar
185 * toolFactory.register( SearchTool );
187 * // Register two more tools, nothing interesting here
188 * function SettingsTool() {
189 * SettingsTool.super.apply( this, arguments );
190 * this.reallyActive = false;
192 * OO.inheritClass( SettingsTool, OO.ui.Tool );
193 * SettingsTool.static.name = 'settings';
194 * SettingsTool.static.icon = 'settings';
195 * SettingsTool.static.title = 'Change settings';
196 * SettingsTool.prototype.onSelect = function () {
197 * $area.text( 'Settings tool clicked!' );
198 * // Toggle the active state on each click
199 * this.reallyActive = !this.reallyActive;
200 * this.setActive( this.reallyActive );
201 * // To update the menu label
202 * this.toolbar.emit( 'updateState' );
204 * SettingsTool.prototype.onUpdateState = function () {};
205 * toolFactory.register( SettingsTool );
207 * // Register two more tools, nothing interesting here
208 * function StuffTool() {
209 * StuffTool.super.apply( this, arguments );
210 * this.reallyActive = false;
212 * OO.inheritClass( StuffTool, OO.ui.Tool );
213 * StuffTool.static.name = 'stuff';
214 * StuffTool.static.icon = 'ellipsis';
215 * StuffTool.static.title = 'More stuff';
216 * StuffTool.prototype.onSelect = function () {
217 * $area.text( 'More stuff tool clicked!' );
218 * // Toggle the active state on each click
219 * this.reallyActive = !this.reallyActive;
220 * this.setActive( this.reallyActive );
221 * // To update the menu label
222 * this.toolbar.emit( 'updateState' );
224 * StuffTool.prototype.onUpdateState = function () {};
225 * toolFactory.register( StuffTool );
227 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
228 * // little popup window (a PopupWidget). 'onUpdateState' is also already implemented.
229 * function HelpTool( toolGroup, config ) {
230 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
235 * this.popup.$body.append( '<p>I am helpful!</p>' );
237 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
238 * HelpTool.static.name = 'help';
239 * HelpTool.static.icon = 'help';
240 * HelpTool.static.title = 'Help';
241 * toolFactory.register( HelpTool );
243 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
244 * // used once (but not all defined tools must be used).
247 * // 'bar' tool groups display tools' icons only, side-by-side.
249 * include: [ 'search', 'help' ]
252 * // 'menu' tool groups display both the titles and icons, in a dropdown menu.
253 * // Menu label indicates which items are selected.
256 * include: [ 'settings', 'stuff' ]
260 * // Create some UI around the toolbar and place it in the document
261 * const frame = new OO.ui.PanelLayout( {
265 * const contentFrame = new OO.ui.PanelLayout( {
269 * frame.$element.append(
271 * contentFrame.$element.append( $area )
273 * $( document.body ).append( frame.$element );
275 * // Here is where the toolbar is actually built. This must be done after inserting it into the
277 * toolbar.initialize();
278 * toolbar.emit( 'updateState' );
281 * @extends OO.ui.Element
282 * @mixes OO.EventEmitter
283 * @mixes OO.ui.mixin.GroupElement
286 * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
287 * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating toolgroups
288 * @param {Object} [config] Configuration options
289 * @param {boolean} [config.actions] Add an actions section to the toolbar. Actions are commands that are
290 * included in the toolbar, but are not configured as tools. By default, actions are displayed on
291 * the right side of the toolbar.
292 * This feature is deprecated. It is suggested to use the ToolGroup 'align' property instead.
293 * @param {string} [config.position='top'] Whether the toolbar is positioned above ('top') or below
294 * ('bottom') content.
295 * @param {jQuery} [config.$overlay] An overlay for the popup.
296 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
298 OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
299 // Allow passing positional parameters inside the config object
300 if ( OO.isPlainObject( toolFactory ) && config === undefined ) {
301 config = toolFactory;
302 toolFactory = config.toolFactory;
303 toolGroupFactory = config.toolGroupFactory;
306 // Configuration initialization
307 config = config || {};
309 // Parent constructor
310 OO.ui.Toolbar.super.call( this, config );
312 // Mixin constructors
313 OO.EventEmitter.call( this );
314 OO.ui.mixin.GroupElement.call( this, config );
317 this.toolFactory = toolFactory;
318 this.toolGroupFactory = toolGroupFactory;
319 this.groupsByName = {};
320 this.activeToolGroups = 0;
322 this.position = config.position || 'top';
323 this.$bar = $( '<div>' );
324 this.$after = $( '<div>' );
325 this.$actions = $( '<div>' );
326 this.$popups = $( '<div>' );
327 this.initialized = false;
329 this.narrowThreshold = null;
330 this.onWindowResizeHandler = this.onWindowResize.bind( this );
331 this.$overlay = ( config.$overlay === true ? OO.ui.getDefaultOverlay() : config.$overlay ) ||
336 .add( this.$bar ).add( this.$group ).add( this.$after ).add( this.$actions )
337 .on( 'mousedown keydown', this.onPointerDown.bind( this ) );
340 this.$bar.addClass( 'oo-ui-toolbar-bar' );
341 this.$group.addClass( 'oo-ui-toolbar-tools' );
342 this.$after.addClass( 'oo-ui-toolbar-tools oo-ui-toolbar-after' );
343 this.$popups.addClass( 'oo-ui-toolbar-popups' );
345 this.$bar.append( this.$group, this.$after );
346 if ( config.actions ) {
347 this.$bar.append( this.$actions.addClass( 'oo-ui-toolbar-actions' ) );
349 this.$bar.append( $( '<div>' ).css( 'clear', 'both' ) );
351 // Possible classes: oo-ui-toolbar-position-top, oo-ui-toolbar-position-bottom
353 .addClass( 'oo-ui-toolbar oo-ui-toolbar-position-' + this.position )
354 .append( this.$bar );
355 this.$overlay.append( this.$popups );
360 OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
361 OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
362 OO.mixinClass( OO.ui.Toolbar, OO.ui.mixin.GroupElement );
367 * An 'updateState' event must be emitted on the Toolbar (by calling
368 * `toolbar.emit( 'updateState' )`) every time the state of the application using the toolbar
369 * changes, and an update to the state of tools is required.
371 * @event OO.ui.Toolbar#updateState
372 * @param {...any} data Application-defined parameters
376 * An 'active' event is emitted when the number of active toolgroups increases from 0, or
379 * @event OO.ui.Toolbar#active
380 * @param {boolean} There are active toolgroups in this toolbar
384 * Toolbar has resized to a point where narrow mode has changed
386 * @event OO.ui.Toolbar#resize
392 * Get the tool factory.
394 * @return {OO.ui.ToolFactory} Tool factory
396 OO.ui.Toolbar.prototype.getToolFactory = function () {
397 return this.toolFactory;
401 * Get the toolgroup factory.
403 * @return {OO.Factory} Toolgroup factory
405 OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
406 return this.toolGroupFactory;
410 * @inheritdoc {OO.ui.mixin.GroupElement}
412 OO.ui.Toolbar.prototype.insertItemElements = function ( item ) {
414 OO.ui.mixin.GroupElement.prototype.insertItemElements.apply( this, arguments );
416 if ( item.align === 'after' ) {
417 // Toolbar only ever appends ToolGroups to the end, so we can ignore 'index'
418 this.$after.append( item.$element );
423 * Handles mouse down events.
426 * @param {jQuery.Event} e Mouse down event
427 * @return {undefined|boolean} False to prevent default if event is handled
429 OO.ui.Toolbar.prototype.onPointerDown = function ( e ) {
430 const $closestWidgetToEvent = $( e.target ).closest( '.oo-ui-widget' ),
431 $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
433 !$closestWidgetToEvent.length ||
434 $closestWidgetToEvent[ 0 ] ===
435 $closestWidgetToToolbar[ 0 ]
442 * Handle window resize event.
445 * @param {jQuery.Event} e Window resize event
447 OO.ui.Toolbar.prototype.onWindowResize = function () {
448 this.setNarrow( this.$bar[ 0 ].clientWidth <= this.getNarrowThreshold() );
452 * Check if the toolbar is in narrow mode
454 * @return {boolean} Toolbar is in narrow mode
456 OO.ui.Toolbar.prototype.isNarrow = function () {
461 * Set the narrow mode flag
463 * @param {boolean} narrow Toolbar is in narrow mode
465 OO.ui.Toolbar.prototype.setNarrow = function ( narrow ) {
466 if ( narrow !== this.narrow ) {
467 this.narrow = narrow;
468 this.$element.add( this.$popups ).toggleClass(
469 'oo-ui-toolbar-narrow',
472 this.emit( 'resize' );
477 * Get the (lazily-computed) width threshold for applying the oo-ui-toolbar-narrow
481 * @return {number} Width threshold in pixels
483 OO.ui.Toolbar.prototype.getNarrowThreshold = function () {
484 if ( this.narrowThreshold === null ) {
485 this.narrowThreshold = this.$group[ 0 ].offsetWidth + this.$after[ 0 ].offsetWidth +
486 this.$actions[ 0 ].offsetWidth;
488 return this.narrowThreshold;
492 * Sets up handles and preloads required information for the toolbar to work.
493 * This must be called after it is attached to a visible document and before doing anything else.
495 OO.ui.Toolbar.prototype.initialize = function () {
496 if ( !this.initialized ) {
497 this.initialized = true;
498 $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
499 this.onWindowResize();
504 * Set up the toolbar.
506 * The toolbar is set up with a list of toolgroup configurations that specify the type of
507 * toolgroup ({@link OO.ui.BarToolGroup bar}, {@link OO.ui.MenuToolGroup menu}, or
508 * {@link OO.ui.ListToolGroup list}) to add and which tools to include, exclude, promote, or demote
509 * within that toolgroup. Please see {@link OO.ui.ToolGroup toolgroups} for more information about
510 * including tools in toolgroups.
512 * @param {Object[]} groups List of toolgroup configurations
513 * @param {string} groups.name Symbolic name for this toolgroup
514 * @param {string} [groups.type] Toolgroup type, e.g. "bar", "list", or "menu". Should exist in the
515 * {@link OO.ui.ToolGroupFactory} provided via the constructor. Defaults to "list" for catch-all
516 * groups where `include='*'`, otherwise "bar".
517 * @param {Array|string} [groups.include] Tools to include in the toolgroup, or "*" for catch-all,
518 * see {@link OO.ui.ToolFactory#extract}
519 * @param {Array|string} [groups.exclude] Tools to exclude from the toolgroup
520 * @param {Array|string} [groups.promote] Tools to promote to the beginning of the toolgroup
521 * @param {Array|string} [groups.demote] Tools to demote to the end of the toolgroup
523 OO.ui.Toolbar.prototype.setup = function ( groups ) {
524 const defaultType = 'bar';
526 // Cleanup previous groups
530 // Build out new groups
531 for ( let i = 0, len = groups.length; i < len; i++ ) {
532 const groupConfig = groups[ i ];
533 if ( groupConfig.include === '*' ) {
534 // Apply defaults to catch-all groups
535 if ( groupConfig.type === undefined ) {
536 groupConfig.type = 'list';
538 if ( groupConfig.label === undefined ) {
539 groupConfig.label = OO.ui.msg( 'ooui-toolbar-more' );
542 // Check type has been registered
543 const type = this.getToolGroupFactory().lookup( groupConfig.type ) ?
544 groupConfig.type : defaultType;
545 const toolGroup = this.getToolGroupFactory().create( type, this, groupConfig );
546 items.push( toolGroup );
547 this.groupsByName[ groupConfig.name ] = toolGroup;
548 toolGroup.connect( this, {
549 active: 'onToolGroupActive'
552 this.addItems( items );
556 * Handle active events from tool groups
558 * @param {boolean} active Tool group has become active, inactive if false
559 * @fires OO.ui.Toolbar#active
561 OO.ui.Toolbar.prototype.onToolGroupActive = function ( active ) {
563 this.activeToolGroups++;
564 if ( this.activeToolGroups === 1 ) {
565 this.emit( 'active', true );
568 this.activeToolGroups--;
569 if ( this.activeToolGroups === 0 ) {
570 this.emit( 'active', false );
576 * Get a toolgroup by name
578 * @param {string} name Group name
579 * @return {OO.ui.ToolGroup|null} Tool group, or null if none found by that name
581 OO.ui.Toolbar.prototype.getToolGroupByName = function ( name ) {
582 return this.groupsByName[ name ] || null;
586 * Remove all tools and toolgroups from the toolbar.
588 OO.ui.Toolbar.prototype.reset = function () {
589 this.groupsByName = {};
591 for ( let i = 0, len = this.items.length; i < len; i++ ) {
592 this.items[ i ].destroy();
598 * Destroy the toolbar.
600 * Destroying the toolbar removes all event handlers and DOM elements that constitute the toolbar.
601 * Call this method whenever you are done using a toolbar.
603 OO.ui.Toolbar.prototype.destroy = function () {
604 $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
606 this.$element.remove();
610 * Check if the tool is available.
612 * Available tools are ones that have not yet been added to the toolbar.
614 * @param {string} name Symbolic name of tool
615 * @return {boolean} Tool is available
617 OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
618 return !this.tools[ name ];
622 * Prevent tool from being used again.
624 * @param {OO.ui.Tool} tool Tool to reserve
626 OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
627 this.tools[ tool.getName() ] = tool;
631 * Allow tool to be used again.
633 * @param {OO.ui.Tool} tool Tool to release
635 OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
636 delete this.tools[ tool.getName() ];
640 * Get accelerator label for tool.
642 * The OOUI library does not contain an accelerator system, but this is the hook for one. To
643 * use an accelerator system, subclass the toolbar and override this method, which is meant to
644 * return a label that describes the accelerator keys for the tool passed (by symbolic name) to
647 * @param {string} name Symbolic name of tool
648 * @return {string|undefined} Tool accelerator label if available
650 OO.ui.Toolbar.prototype.getToolAccelerator = function () {
655 * Tools, together with {@link OO.ui.ToolGroup toolgroups}, constitute
656 * {@link OO.ui.Toolbar toolbars}.
657 * Each tool is configured with a static name, title, and icon and is customized with the command
658 * to carry out when the tool is selected. Tools must also be registered with a
659 * {@link OO.ui.ToolFactory tool factory}, which creates the tools on demand.
661 * Every Tool subclass must implement two methods:
663 * - {@link OO.ui.Tool#onUpdateState onUpdateState}
664 * - {@link OO.ui.Tool#onSelect onSelect}
666 * Tools are added to toolgroups ({@link OO.ui.ListToolGroup ListToolGroup},
667 * {@link OO.ui.BarToolGroup BarToolGroup}, or {@link OO.ui.MenuToolGroup MenuToolGroup}), which
668 * determine how the tool is displayed in the toolbar. See {@link OO.ui.Toolbar toolbars} for an
671 * For more information, please see the [OOUI documentation on MediaWiki][1].
672 * [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars
676 * @extends OO.ui.Widget
677 * @mixes OO.ui.mixin.IconElement
678 * @mixes OO.ui.mixin.FlaggedElement
679 * @mixes OO.ui.mixin.TabIndexedElement
682 * @param {OO.ui.ToolGroup} toolGroup
683 * @param {Object} [config] Configuration options
684 * @param {string|Function} [config.title] Title text or a function that returns text. If this config is
685 * omitted, the value of the {@link OO.ui.Tool.static.title static title} property is used.
686 * @param {boolean} [config.displayBothIconAndLabel] See static.displayBothIconAndLabel
687 * @param {Object} [config.narrowConfig] See static.narrowConfig
689 * The title is used in different ways depending on the type of toolgroup that contains the tool.
690 * The title is used as a tooltip if the tool is part of a {@link OO.ui.BarToolGroup bar}
691 * toolgroup, or as the label text if the tool is part of a {@link OO.ui.ListToolGroup list} or
692 * {@link OO.ui.MenuToolGroup menu} toolgroup.
694 * For bar toolgroups, a description of the accelerator key is appended to the title if an
695 * accelerator key is associated with an action by the same name as the tool and accelerator
696 * functionality has been added to the application.
697 * To add accelerator key functionality, you must subclass OO.ui.Toolbar and override the
698 * {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method.
700 OO.ui.Tool = function OoUiTool( toolGroup, config ) {
701 // Allow passing positional parameters inside the config object
702 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
704 toolGroup = config.toolGroup;
707 // Configuration initialization
708 config = config || {};
710 // Parent constructor
711 OO.ui.Tool.super.call( this, config );
714 this.toolGroup = toolGroup;
715 this.toolbar = this.toolGroup.getToolbar();
717 this.$title = $( '<span>' );
718 this.$accel = $( '<span>' );
719 this.$link = $( '<a>' );
721 this.checkIcon = new OO.ui.IconWidget( {
723 classes: [ 'oo-ui-tool-checkIcon' ]
725 this.displayBothIconAndLabel = config.displayBothIconAndLabel !== undefined ?
726 config.displayBothIconAndLabel : this.constructor.static.displayBothIconAndLabel;
727 this.narrowConfig = config.narrowConfig || this.constructor.static.narrowConfig;
729 // Mixin constructors
730 OO.ui.mixin.IconElement.call( this, config );
731 OO.ui.mixin.FlaggedElement.call( this, config );
732 OO.ui.mixin.TabIndexedElement.call( this, Object.assign( {
733 $tabIndexed: this.$link
737 this.toolbar.connect( this, {
738 updateState: 'onUpdateState',
739 resize: 'onToolbarResize'
743 this.$title.addClass( 'oo-ui-tool-title' );
745 .addClass( 'oo-ui-tool-accel' )
747 // This may need to be changed if the key names are ever localized,
748 // but for now they are essentially written in English
753 .addClass( 'oo-ui-tool-link' )
754 .append( this.checkIcon.$element, this.$icon, this.$title, this.$accel )
755 .attr( 'role', 'button' );
757 // Don't show keyboard shortcuts on mobile as users are unlikely to have
758 // a physical keyboard, and likely to have limited screen space.
759 if ( !OO.ui.isMobile() ) {
760 this.$link.append( this.$accel );
764 .data( 'oo-ui-tool', this )
765 .addClass( 'oo-ui-tool' )
766 .addClass( 'oo-ui-tool-name-' +
767 this.constructor.static.name.replace( /^([^/]+)\/([^/]+).*$/, '$1-$2' ) )
768 .append( this.$link );
769 this.setTitle( config.title || this.constructor.static.title );
774 OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
775 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.IconElement );
776 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.FlaggedElement );
777 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.TabIndexedElement );
779 /* Static Properties */
785 OO.ui.Tool.static.tagName = 'span';
788 * Symbolic name of tool.
790 * The symbolic name is used internally to register the tool with a
791 * {@link OO.ui.ToolFactory ToolFactory}. It can also be used when adding tools to toolgroups.
797 OO.ui.Tool.static.name = '';
800 * Symbolic name of the group.
802 * The group name is used to associate tools with each other so that they can be selected later by
803 * a {@link OO.ui.ToolGroup toolgroup}.
809 OO.ui.Tool.static.group = '';
812 * Tool title text or a function that returns title text. The value of the static property is
813 * overridden if the #title config option is used.
817 * @property {string|Function}
819 OO.ui.Tool.static.title = '';
822 * Display both icon and label when the tool is used in a {@link OO.ui.BarToolGroup bar} toolgroup.
823 * Normally only the icon is displayed, or only the label if no icon is given.
826 * @property {boolean}
828 OO.ui.Tool.static.displayBothIconAndLabel = false;
831 * Add tool to catch-all groups automatically.
833 * A catch-all group, which contains all tools that do not currently belong to a toolgroup,
834 * can be included in a toolgroup using the wildcard selector, an asterisk (*).
837 * @property {boolean}
839 OO.ui.Tool.static.autoAddToCatchall = true;
842 * Add tool to named groups automatically.
844 * By default, tools that are configured with a static ‘group’ property are added
845 * to that group and will be selected when the symbolic name of the group is specified (e.g., when
846 * toolgroups include tools by group name).
849 * @property {boolean}
851 OO.ui.Tool.static.autoAddToGroup = true;
854 * Check if this tool is compatible with given data.
856 * This is a stub that can be overridden to provide support for filtering tools based on an
857 * arbitrary piece of information (e.g., where the cursor is in a document). The implementation
858 * must also call this method so that the compatibility check can be performed.
861 * @param {any} data Data to check
862 * @return {boolean} Tool can be used with data
864 OO.ui.Tool.static.isCompatibleWith = function () {
869 * Config options to change when toolbar is in narrow mode
871 * Supports `displayBothIconAndLabel`, `title` and `icon` properties.
874 * @property {Object|null}
876 OO.ui.Tool.static.narrowConfig = null;
881 * Handle the toolbar state being updated. This method is called when the
882 * {@link OO.ui.Toolbar#event-updateState 'updateState' event} is emitted on the
883 * {@link OO.ui.Toolbar Toolbar} that uses this tool, and should set the state of this tool
884 * depending on application state (usually by calling #setDisabled to enable or disable the tool,
885 * or #setActive to mark is as currently in-use or not).
887 * This is an abstract method that must be overridden in a concrete subclass.
893 OO.ui.Tool.prototype.onUpdateState = null;
896 * Handle the tool being selected. This method is called when the user triggers this tool,
897 * usually by clicking on its label/icon.
899 * This is an abstract method that must be overridden in a concrete subclass.
905 OO.ui.Tool.prototype.onSelect = null;
908 * Check if the tool is active.
910 * Tools become active when their #onSelect or #onUpdateState handlers change them to appear pressed
911 * with the #setActive method. Additional CSS is applied to the tool to reflect the active state.
913 * @return {boolean} Tool is active
915 OO.ui.Tool.prototype.isActive = function () {
920 * Make the tool appear active or inactive.
922 * This method should be called within #onSelect or #onUpdateState event handlers to make the tool
923 * appear pressed or not.
925 * @param {boolean} [state=false] Make tool appear active
927 OO.ui.Tool.prototype.setActive = function ( state ) {
928 this.active = !!state;
929 this.$element.toggleClass( 'oo-ui-tool-active', this.active );
930 this.updateThemeClasses();
934 * Set the tool #title.
936 * @param {string|Function} title Title text or a function that returns text
938 * @return {OO.ui.Tool} The tool, for chaining
940 OO.ui.Tool.prototype.setTitle = function ( title ) {
941 this.title = OO.ui.resolveMsg( title );
944 this.setDisplayBothIconAndLabel( this.displayBothIconAndLabel );
949 * Set the tool's displayBothIconAndLabel state.
951 * Update title classes if necessary
953 * @param {boolean} displayBothIconAndLabel
955 * @return {OO.ui.Tool} The tool, for chaining
957 OO.ui.Tool.prototype.setDisplayBothIconAndLabel = function ( displayBothIconAndLabel ) {
958 this.displayBothIconAndLabel = displayBothIconAndLabel;
959 this.$element.toggleClass( 'oo-ui-tool-with-label', !!this.title && this.displayBothIconAndLabel );
964 * Get the tool #title.
966 * @return {string} Title text
968 OO.ui.Tool.prototype.getTitle = function () {
973 * Get the tool's symbolic name.
975 * @return {string} Symbolic name of tool
977 OO.ui.Tool.prototype.getName = function () {
978 return this.constructor.static.name;
982 * Handle resize events from the toolbar
984 OO.ui.Tool.prototype.onToolbarResize = function () {
985 if ( !this.narrowConfig ) {
988 if ( this.toolbar.isNarrow() ) {
989 if ( this.narrowConfig.displayBothIconAndLabel !== undefined ) {
990 this.wideDisplayBothIconAndLabel = this.displayBothIconAndLabel;
991 this.setDisplayBothIconAndLabel( this.narrowConfig.displayBothIconAndLabel );
993 if ( this.narrowConfig.title !== undefined ) {
994 this.wideTitle = this.title;
995 this.setTitle( this.narrowConfig.title );
997 if ( this.narrowConfig.icon !== undefined ) {
998 this.wideIcon = this.icon;
999 this.setIcon( this.narrowConfig.icon );
1002 if ( this.wideDisplayBothIconAndLabel !== undefined ) {
1003 this.setDisplayBothIconAndLabel( this.wideDisplayBothIconAndLabel );
1005 if ( this.wideTitle !== undefined ) {
1006 this.setTitle( this.wideTitle );
1008 if ( this.wideIcon !== undefined ) {
1009 this.setIcon( this.wideIcon );
1017 OO.ui.Tool.prototype.updateTitle = function () {
1018 const titleTooltips = this.toolGroup.constructor.static.titleTooltips,
1019 accelTooltips = this.toolGroup.constructor.static.accelTooltips,
1020 accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
1023 this.$title.text( this.title );
1024 this.$accel.text( accel );
1026 if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
1027 tooltipParts.push( this.title );
1029 if ( accelTooltips && typeof accel === 'string' && accel.length ) {
1030 tooltipParts.push( accel );
1032 if ( tooltipParts.length ) {
1033 this.$link.attr( 'title', tooltipParts.join( ' ' ) );
1035 this.$link.removeAttr( 'title' );
1040 * @inheritdoc OO.ui.mixin.IconElement
1042 OO.ui.Tool.prototype.setIcon = function ( icon ) {
1044 OO.ui.mixin.IconElement.prototype.setIcon.call( this, icon );
1046 this.$element.toggleClass( 'oo-ui-tool-with-icon', !!this.icon );
1054 * Destroying the tool removes all event handlers and the tool’s DOM elements.
1055 * Call this method whenever you are done using a tool.
1057 OO.ui.Tool.prototype.destroy = function () {
1058 this.toolbar.disconnect( this );
1059 this.$element.remove();
1063 * ToolGroups are collections of {@link OO.ui.Tool tools} that are used in a
1064 * {@link OO.ui.Toolbar toolbar}.
1065 * The type of toolgroup ({@link OO.ui.ListToolGroup list}, {@link OO.ui.BarToolGroup bar}, or
1066 * {@link OO.ui.MenuToolGroup menu}) to which a tool belongs determines how the tool is arranged
1067 * and displayed in the toolbar. Toolgroups themselves are created on demand with a
1068 * {@link OO.ui.ToolGroupFactory toolgroup factory}.
1070 * Toolgroups can contain individual tools, groups of tools, or all available tools, as specified
1071 * using the `include` config option. See OO.ui.ToolFactory#extract on documentation of the format.
1072 * The options `exclude`, `promote`, and `demote` support the same formats.
1074 * See {@link OO.ui.Toolbar toolbars} for a full example. For more information about toolbars in
1075 * general, please see the [OOUI documentation on MediaWiki][1].
1077 * [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars
1081 * @extends OO.ui.Widget
1082 * @mixes OO.ui.mixin.GroupElement
1085 * @param {OO.ui.Toolbar} toolbar
1086 * @param {Object} [config] Configuration options
1087 * @param {Array|string} [config.include=[]] List of tools to include in the toolgroup, see above.
1088 * @param {Array|string} [config.exclude=[]] List of tools to exclude from the toolgroup, see above.
1089 * @param {Array|string} [config.promote=[]] List of tools to promote to the beginning of the toolgroup,
1091 * @param {Array|string} [config.demote=[]] List of tools to demote to the end of the toolgroup, see above.
1092 * This setting is particularly useful when tools have been added to the toolgroup
1093 * en masse (e.g., via the catch-all selector).
1094 * @param {string} [config.align='before'] Alignment within the toolbar, either 'before' or 'after'.
1096 OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
1097 // Allow passing positional parameters inside the config object
1098 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
1100 toolbar = config.toolbar;
1103 // Configuration initialization
1104 config = config || {};
1106 // Parent constructor
1107 OO.ui.ToolGroup.super.call( this, config );
1109 // Mixin constructors
1110 OO.ui.mixin.GroupElement.call( this, config );
1113 this.toolbar = toolbar;
1115 this.pressed = null;
1116 this.autoDisabled = false;
1117 this.include = config.include || [];
1118 this.exclude = config.exclude || [];
1119 this.promote = config.promote || [];
1120 this.demote = config.demote || [];
1121 this.align = config.align || 'before';
1122 this.onDocumentMouseKeyUpHandler = this.onDocumentMouseKeyUp.bind( this );
1126 mousedown: this.onMouseKeyDown.bind( this ),
1127 mouseup: this.onMouseKeyUp.bind( this ),
1128 keydown: this.onMouseKeyDown.bind( this ),
1129 keyup: this.onMouseKeyUp.bind( this ),
1130 focus: this.onMouseOverFocus.bind( this ),
1131 blur: this.onMouseOutBlur.bind( this ),
1132 mouseover: this.onMouseOverFocus.bind( this ),
1133 mouseout: this.onMouseOutBlur.bind( this )
1135 this.toolbar.getToolFactory().connect( this, {
1136 register: 'onToolFactoryRegister'
1139 disable: 'itemDisable'
1141 this.connect( this, {
1142 itemDisable: 'updateDisabled',
1143 disable: 'onDisable'
1147 this.$group.addClass( 'oo-ui-toolGroup-tools' );
1149 .addClass( 'oo-ui-toolGroup' )
1150 .append( this.$group );
1151 this.onDisable( this.isDisabled() );
1157 OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget );
1158 OO.mixinClass( OO.ui.ToolGroup, OO.ui.mixin.GroupElement );
1163 * @event OO.ui.ToolGroup#update
1167 * An 'active' event is emitted when any popup is shown/hidden.
1169 * @event OO.ui.ToolGroup#active
1170 * @param {boolean} The popup is visible
1173 /* Static Properties */
1176 * Show labels in tooltips.
1179 * @property {boolean}
1181 OO.ui.ToolGroup.static.titleTooltips = false;
1184 * Show acceleration labels in tooltips.
1186 * Note: The OOUI library does not include an accelerator system, but does contain
1187 * a hook for one. To use an accelerator system, subclass the {@link OO.ui.Toolbar toolbar} and
1188 * override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method, which is
1189 * meant to return a label that describes the accelerator keys for a given tool (e.g., Control+M
1193 * @property {boolean}
1195 OO.ui.ToolGroup.static.accelTooltips = false;
1198 * Automatically disable the toolgroup when all tools are disabled
1201 * @property {boolean}
1203 OO.ui.ToolGroup.static.autoDisable = true;
1208 * @property {string}
1210 OO.ui.ToolGroup.static.name = null;
1217 OO.ui.ToolGroup.prototype.isDisabled = function () {
1218 return this.autoDisabled ||
1219 OO.ui.ToolGroup.super.prototype.isDisabled.apply( this, arguments );
1225 OO.ui.ToolGroup.prototype.updateDisabled = function () {
1226 let allDisabled = true;
1228 if ( this.constructor.static.autoDisable ) {
1229 for ( let i = this.items.length - 1; i >= 0; i-- ) {
1230 const item = this.items[ i ];
1231 if ( !item.isDisabled() ) {
1232 allDisabled = false;
1236 this.autoDisabled = allDisabled;
1238 OO.ui.ToolGroup.super.prototype.updateDisabled.apply( this, arguments );
1242 * Handle disable events.
1245 * @param {boolean} isDisabled
1247 OO.ui.ToolGroup.prototype.onDisable = function ( isDisabled ) {
1248 this.$group.toggleClass( 'oo-ui-toolGroup-disabled-tools', isDisabled );
1249 this.$group.toggleClass( 'oo-ui-toolGroup-enabled-tools', !isDisabled );
1253 * Handle mouse down and key down events.
1256 * @param {jQuery.Event} e Mouse down or key down event
1257 * @return {undefined|boolean} False to prevent default if event is handled
1259 OO.ui.ToolGroup.prototype.onMouseKeyDown = function ( e ) {
1261 !this.isDisabled() && (
1262 e.which === OO.ui.MouseButtons.LEFT ||
1263 e.which === OO.ui.Keys.SPACE ||
1264 e.which === OO.ui.Keys.ENTER
1267 this.pressed = this.findTargetTool( e );
1268 if ( this.pressed ) {
1269 this.pressed.setActive( true );
1270 this.getElementDocument().addEventListener(
1272 this.onDocumentMouseKeyUpHandler,
1275 this.getElementDocument().addEventListener(
1277 this.onDocumentMouseKeyUpHandler,
1286 * Handle document mouse up and key up events.
1289 * @param {MouseEvent|KeyboardEvent} e Mouse up or key up event
1291 OO.ui.ToolGroup.prototype.onDocumentMouseKeyUp = function ( e ) {
1292 this.getElementDocument().removeEventListener(
1294 this.onDocumentMouseKeyUpHandler,
1297 this.getElementDocument().removeEventListener(
1299 this.onDocumentMouseKeyUpHandler,
1302 // onMouseKeyUp may be called a second time, depending on where the mouse is when the button is
1303 // released, but since `this.pressed` will no longer be true, the second call will be ignored.
1304 this.onMouseKeyUp( e );
1308 * Handle mouse up and key up events.
1311 * @param {MouseEvent|KeyboardEvent} e Mouse up or key up event
1313 OO.ui.ToolGroup.prototype.onMouseKeyUp = function ( e ) {
1314 const tool = this.findTargetTool( e );
1317 !this.isDisabled() && this.pressed && this.pressed === tool && (
1318 e.which === OO.ui.MouseButtons.LEFT ||
1319 e.which === OO.ui.Keys.SPACE ||
1320 e.which === OO.ui.Keys.ENTER
1323 this.pressed.onSelect();
1324 this.pressed = null;
1326 e.stopPropagation();
1329 this.pressed = null;
1333 * Handle mouse over and focus events.
1336 * @param {jQuery.Event} e Mouse over or focus event
1338 OO.ui.ToolGroup.prototype.onMouseOverFocus = function ( e ) {
1339 const tool = this.findTargetTool( e );
1341 if ( this.pressed && this.pressed === tool ) {
1342 this.pressed.setActive( true );
1347 * Handle mouse out and blur events.
1350 * @param {jQuery.Event} e Mouse out or blur event
1352 OO.ui.ToolGroup.prototype.onMouseOutBlur = function ( e ) {
1353 const tool = this.findTargetTool( e );
1355 if ( this.pressed && this.pressed === tool ) {
1356 this.pressed.setActive( false );
1361 * Get the closest tool to a jQuery.Event.
1363 * Only tool links are considered, which prevents other elements in the tool such as popups from
1364 * triggering tool group interactions.
1367 * @param {jQuery.Event} e
1368 * @return {OO.ui.Tool|null} Tool, `null` if none was found
1370 OO.ui.ToolGroup.prototype.findTargetTool = function ( e ) {
1371 const $item = $( e.target ).closest( '.oo-ui-tool-link' );
1374 if ( $item.length ) {
1375 tool = $item.parent().data( 'oo-ui-tool' );
1378 return tool && !tool.isDisabled() ? tool : null;
1382 * Handle tool registry register events.
1384 * If a tool is registered after the group is created, we must repopulate the list to account for:
1386 * - a tool being added that may be included
1387 * - a tool already included being overridden
1390 * @param {string} name Symbolic name of tool
1392 OO.ui.ToolGroup.prototype.onToolFactoryRegister = function () {
1397 * Get the toolbar that contains the toolgroup.
1399 * @return {OO.ui.Toolbar} Toolbar that contains the toolgroup
1401 OO.ui.ToolGroup.prototype.getToolbar = function () {
1402 return this.toolbar;
1406 * Add and remove tools based on configuration.
1408 OO.ui.ToolGroup.prototype.populate = function () {
1409 const toolFactory = this.toolbar.getToolFactory(),
1413 list = this.toolbar.getToolFactory().getTools(
1414 this.include, this.exclude, this.promote, this.demote
1418 // Build a list of needed tools
1419 for ( let i = 0, len = list.length; i < len; i++ ) {
1423 toolFactory.lookup( name ) &&
1424 // Tool is available or is already in this group
1425 ( this.toolbar.isToolAvailable( name ) || this.tools[ name ] )
1427 // Hack to prevent infinite recursion via ToolGroupTool. We need to reserve the tool
1428 // before creating it, but we can't call reserveTool() yet because we haven't created
1430 this.toolbar.tools[ name ] = true;
1431 let tool = this.tools[ name ];
1433 // Auto-initialize tools on first use
1434 this.tools[ name ] = tool = toolFactory.create( name, this );
1437 this.toolbar.reserveTool( tool );
1439 names[ name ] = true;
1442 // Remove tools that are no longer needed
1443 for ( name in this.tools ) {
1444 if ( !names[ name ] ) {
1445 this.tools[ name ].destroy();
1446 this.toolbar.releaseTool( this.tools[ name ] );
1447 remove.push( this.tools[ name ] );
1448 delete this.tools[ name ];
1451 if ( remove.length ) {
1452 this.removeItems( remove );
1454 // Update emptiness state
1455 this.$element.toggleClass( 'oo-ui-toolGroup-empty', !add.length );
1456 // Re-add tools (moving existing ones to new locations)
1457 this.addItems( add );
1458 // Disabled state may depend on items
1459 this.updateDisabled();
1463 * Destroy toolgroup.
1465 OO.ui.ToolGroup.prototype.destroy = function () {
1467 this.toolbar.getToolFactory().disconnect( this );
1468 for ( const name in this.tools ) {
1469 this.toolbar.releaseTool( this.tools[ name ] );
1470 this.tools[ name ].disconnect( this ).destroy();
1471 delete this.tools[ name ];
1473 this.$element.remove();
1477 * A ToolFactory creates tools on demand. All tools ({@link OO.ui.Tool Tools},
1478 * {@link OO.ui.PopupTool PopupTools}, and {@link OO.ui.ToolGroupTool ToolGroupTools}) must be
1479 * registered with a tool factory. Tools are registered by their symbolic name. See
1480 * {@link OO.ui.Toolbar toolbars} for an example.
1482 * For more information about toolbars in general, please see the
1483 * [OOUI documentation on MediaWiki][1].
1485 * [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars
1488 * @extends OO.Factory
1491 OO.ui.ToolFactory = function OoUiToolFactory() {
1492 // Parent constructor
1493 OO.ui.ToolFactory.super.call( this );
1498 OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
1503 * Get tools from the factory.
1505 * @param {Array|string} include Included tools, see #extract for format
1506 * @param {Array|string} exclude Excluded tools, see #extract for format
1507 * @param {Array|string} promote Promoted tools, see #extract for format
1508 * @param {Array|string} demote Demoted tools, see #extract for format
1509 * @return {string[]} List of tools
1511 OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
1515 // Collect included and not excluded tools
1516 const included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
1519 const promoted = this.extract( promote, used );
1520 const demoted = this.extract( demote, used );
1523 for ( let i = 0, len = included.length; i < len; i++ ) {
1524 if ( !used[ included[ i ] ] ) {
1525 auto.push( included[ i ] );
1529 return promoted.concat( auto ).concat( demoted );
1533 * Get a flat list of names from a list of names or groups.
1535 * Normally, `collection` is an array of tool specifications. Tools can be specified in the
1538 * - To include an individual tool, use the symbolic name: `{ name: 'tool-name' }` or `'tool-name'`.
1539 * - To include all tools in a group, use the group name: `{ group: 'group-name' }`. (To assign the
1540 * tool to a group, use OO.ui.Tool.static.group.)
1542 * Alternatively, to include all tools that are not yet assigned to any other toolgroup, use the
1543 * catch-all selector `'*'`.
1545 * If `used` is passed, tool names that appear as properties in this object will be considered
1546 * already assigned, and will not be returned even if specified otherwise. The tool names extracted
1547 * by this function call will be added as new properties in the object.
1550 * @param {Array|string} collection List of tools, see above
1551 * @param {Object.<string,boolean>} [used] Object containing information about used tools, see above
1552 * @return {string[]} List of extracted tool names
1554 OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
1557 collection = !Array.isArray( collection ) ? [ collection ] : collection;
1559 for ( let i = 0, len = collection.length; i < len; i++ ) {
1560 let item = collection[ i ],
1562 if ( item === '*' ) {
1563 for ( name in this.registry ) {
1564 tool = this.registry[ name ];
1566 // Only add tools by group name when auto-add is enabled
1567 tool.static.autoAddToCatchall &&
1568 // Exclude already used tools
1569 ( !used || !used[ name ] )
1573 used[ name ] = true;
1578 // Allow plain strings as shorthand for named tools
1579 if ( typeof item === 'string' ) {
1580 item = { name: item };
1582 if ( OO.isPlainObject( item ) ) {
1584 for ( name in this.registry ) {
1585 tool = this.registry[ name ];
1587 // Include tools with matching group
1588 tool.static.group === item.group &&
1589 // Only add tools by group name when auto-add is enabled
1590 tool.static.autoAddToGroup &&
1591 // Exclude already used tools
1592 ( !used || !used[ name ] )
1596 used[ name ] = true;
1600 // Include tools with matching name and exclude already used tools
1601 } else if ( item.name && ( !used || !used[ item.name ] ) ) {
1602 names.push( item.name );
1604 used[ item.name ] = true;
1614 * ToolGroupFactories create {@link OO.ui.ToolGroup toolgroups} on demand. The toolgroup classes
1615 * must specify a symbolic name and be registered with the factory. The following classes are
1616 * registered by default:
1618 * - {@link OO.ui.BarToolGroup BarToolGroups} (‘bar’)
1619 * - {@link OO.ui.MenuToolGroup MenuToolGroups} (‘menu’)
1620 * - {@link OO.ui.ListToolGroup ListToolGroups} (‘list’)
1622 * See {@link OO.ui.Toolbar toolbars} for an example.
1624 * For more information about toolbars in general, please see the
1625 * [OOUI documentation on MediaWiki][1].
1627 * [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars
1630 * @extends OO.Factory
1633 OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() {
1634 // Parent constructor
1635 OO.Factory.call( this );
1637 const defaultClasses = this.constructor.static.getDefaultClasses();
1639 // Register default toolgroups
1640 for ( let i = 0, l = defaultClasses.length; i < l; i++ ) {
1641 this.register( defaultClasses[ i ] );
1647 OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory );
1649 /* Static Methods */
1652 * Get a default set of classes to be registered on construction.
1654 * @return {Function[]} Default classes
1656 OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
1659 OO.ui.ListToolGroup,
1665 * Popup tools open a popup window when they are selected from the {@link OO.ui.Toolbar toolbar}.
1666 * Each popup tool is configured with a static name, title, and icon, as well with as any popup
1667 * configurations. Unlike other tools, popup tools do not require that developers specify an
1668 * #onSelect or #onUpdateState method, as these methods have been implemented already.
1670 * // Example of a popup tool. When selected, a popup tool displays
1671 * // a popup window.
1672 * function HelpTool( toolGroup, config ) {
1673 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
1678 * this.popup.$body.append( '<p>I am helpful!</p>' );
1680 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
1681 * HelpTool.static.name = 'help';
1682 * HelpTool.static.icon = 'help';
1683 * HelpTool.static.title = 'Help';
1684 * toolFactory.register( HelpTool );
1686 * For an example of a toolbar that contains a popup tool, see {@link OO.ui.Toolbar toolbars}.
1687 * For more information about toolbars in general, please see the
1688 * [OOUI documentation on MediaWiki][1].
1690 * [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars
1694 * @extends OO.ui.Tool
1695 * @mixes OO.ui.mixin.PopupElement
1698 * @param {OO.ui.ToolGroup} toolGroup
1699 * @param {Object} [config] Configuration options
1701 OO.ui.PopupTool = function OoUiPopupTool( toolGroup, config ) {
1702 // Allow passing positional parameters inside the config object
1703 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
1705 toolGroup = config.toolGroup;
1708 // Parent constructor
1709 OO.ui.PopupTool.super.call( this, toolGroup, config );
1711 // Mixin constructors
1712 OO.ui.mixin.PopupElement.call( this, config );
1715 this.popup.connect( this, {
1716 toggle: 'onPopupToggle'
1720 this.popup.setAutoFlip( false );
1721 this.popup.setPosition( toolGroup.getToolbar().position === 'bottom' ? 'above' : 'below' );
1722 this.$element.addClass( 'oo-ui-popupTool' );
1723 this.popup.$element.addClass( 'oo-ui-popupTool-popup' );
1724 this.toolbar.$popups.append( this.popup.$element );
1729 OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool );
1730 OO.mixinClass( OO.ui.PopupTool, OO.ui.mixin.PopupElement );
1735 * Handle the tool being selected.
1739 OO.ui.PopupTool.prototype.onSelect = function () {
1740 if ( !this.isDisabled() ) {
1741 this.popup.toggle();
1747 * Handle the toolbar state being updated.
1751 OO.ui.PopupTool.prototype.onUpdateState = function () {
1755 * Handle popup visibility being toggled.
1757 * @param {boolean} isVisible
1759 OO.ui.PopupTool.prototype.onPopupToggle = function ( isVisible ) {
1760 this.setActive( isVisible );
1761 this.toolGroup.emit( 'active', isVisible );
1765 * A ToolGroupTool is a special sort of tool that can contain other {@link OO.ui.Tool tools}
1766 * and {@link OO.ui.ToolGroup toolgroups}. The ToolGroupTool was specifically designed to be used
1767 * inside a {@link OO.ui.BarToolGroup bar} toolgroup to provide access to additional tools from
1768 * the bar item. Included tools will be displayed in a dropdown {@link OO.ui.ListToolGroup list}
1769 * when the ToolGroupTool is selected.
1771 * // Example: ToolGroupTool with two nested tools, 'setting1' and 'setting2',
1772 * // defined elsewhere.
1774 * function SettingsTool() {
1775 * SettingsTool.super.apply( this, arguments );
1777 * OO.inheritClass( SettingsTool, OO.ui.ToolGroupTool );
1778 * SettingsTool.static.name = 'settings';
1779 * SettingsTool.static.title = 'Change settings';
1780 * SettingsTool.static.groupConfig = {
1782 * label: 'ToolGroupTool',
1783 * include: [ 'setting1', 'setting2' ]
1785 * toolFactory.register( SettingsTool );
1787 * For more information, please see the [OOUI documentation on MediaWiki][1].
1789 * Please note that this implementation is subject to change per [T74159][2].
1791 * [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars#ToolGroupTool
1792 * [2]: https://phabricator.wikimedia.org/T74159
1796 * @extends OO.ui.Tool
1799 * @param {OO.ui.ToolGroup} toolGroup
1800 * @param {Object} [config] Configuration options
1802 OO.ui.ToolGroupTool = function OoUiToolGroupTool( toolGroup, config ) {
1803 // Allow passing positional parameters inside the config object
1804 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
1806 toolGroup = config.toolGroup;
1809 // Parent constructor
1810 OO.ui.ToolGroupTool.super.call( this, toolGroup, config );
1813 this.innerToolGroup = this.createGroup( this.constructor.static.groupConfig );
1816 this.innerToolGroup.connect( this, {
1817 disable: 'onToolGroupDisable',
1818 // Re-emit active events from the innerToolGroup on the parent toolGroup
1819 active: this.toolGroup.emit.bind( this.toolGroup, 'active' )
1823 this.$link.remove();
1825 .addClass( 'oo-ui-toolGroupTool' )
1826 .append( this.innerToolGroup.$element );
1831 OO.inheritClass( OO.ui.ToolGroupTool, OO.ui.Tool );
1833 /* Static Properties */
1836 * Toolgroup configuration.
1838 * The toolgroup configuration consists of the tools to include, as well as an icon and label
1839 * to use for the bar item. Tools can be included by symbolic name, group, or with the
1840 * wildcard selector. Please see {@link OO.ui.ToolGroup toolgroup} for more information.
1842 * @property {Object.<string,Array>}
1844 OO.ui.ToolGroupTool.static.groupConfig = {};
1849 * Handle the tool being selected.
1853 OO.ui.ToolGroupTool.prototype.onSelect = function () {
1854 this.innerToolGroup.setActive( !this.innerToolGroup.active );
1859 * Synchronize disabledness state of the tool with the inner toolgroup.
1862 * @param {boolean} disabled Element is disabled
1864 OO.ui.ToolGroupTool.prototype.onToolGroupDisable = function ( disabled ) {
1865 this.setDisabled( disabled );
1869 * Handle the toolbar state being updated.
1873 OO.ui.ToolGroupTool.prototype.onUpdateState = function () {
1874 this.setActive( false );
1878 * Build a {@link OO.ui.ToolGroup toolgroup} from the specified configuration.
1880 * @param {Object.<string,Array>} group Toolgroup configuration. Please see
1881 * {@link OO.ui.ToolGroup toolgroup} for more information.
1882 * @return {OO.ui.ListToolGroup}
1884 OO.ui.ToolGroupTool.prototype.createGroup = function ( group ) {
1885 if ( group.include === '*' ) {
1886 // Apply defaults to catch-all groups
1887 if ( group.label === undefined ) {
1888 group.label = OO.ui.msg( 'ooui-toolbar-more' );
1892 return this.toolbar.getToolGroupFactory().create( 'list', this.toolbar, group );
1896 * BarToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
1897 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are
1898 * {@link OO.ui.MenuToolGroup MenuToolGroup} and {@link OO.ui.ListToolGroup ListToolGroup}).
1899 * The {@link OO.ui.Tool tools} in a BarToolGroup are displayed by icon in a single row. The
1900 * title of the tool is displayed when users move the mouse over the tool.
1902 * BarToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar
1905 * For more information about how to add tools to a bar tool group, please see
1906 * {@link OO.ui.ToolGroup toolgroup}.
1907 * For more information about toolbars in general, please see the
1908 * [OOUI documentation on MediaWiki][1].
1910 * [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars
1913 * // Example of a BarToolGroup with two tools
1914 * const toolFactory = new OO.ui.ToolFactory();
1915 * const toolGroupFactory = new OO.ui.ToolGroupFactory();
1916 * const toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
1918 * // We will be placing status text in this element when tools are used
1919 * const $area = $( '<p>' ).text( 'Example of a BarToolGroup with two tools.' );
1921 * // Define the tools that we're going to place in our toolbar
1923 * // Create a class inheriting from OO.ui.Tool
1924 * function SearchTool() {
1925 * SearchTool.super.apply( this, arguments );
1927 * OO.inheritClass( SearchTool, OO.ui.Tool );
1928 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
1929 * // of 'icon' and 'title' (displayed icon and text).
1930 * SearchTool.static.name = 'search';
1931 * SearchTool.static.icon = 'search';
1932 * SearchTool.static.title = 'Search...';
1933 * // Defines the action that will happen when this tool is selected (clicked).
1934 * SearchTool.prototype.onSelect = function () {
1935 * $area.text( 'Search tool clicked!' );
1936 * // Never display this tool as "active" (selected).
1937 * this.setActive( false );
1939 * SearchTool.prototype.onUpdateState = function () {};
1940 * // Make this tool available in our toolFactory and thus our toolbar
1941 * toolFactory.register( SearchTool );
1943 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
1944 * // little popup window (a PopupWidget).
1945 * function HelpTool( toolGroup, config ) {
1946 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
1951 * this.popup.$body.append( '<p>I am helpful!</p>' );
1953 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
1954 * HelpTool.static.name = 'help';
1955 * HelpTool.static.icon = 'help';
1956 * HelpTool.static.title = 'Help';
1957 * toolFactory.register( HelpTool );
1959 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
1960 * // used once (but not all defined tools must be used).
1963 * // 'bar' tool groups display tools by icon only
1965 * include: [ 'search', 'help' ]
1969 * // Create some UI around the toolbar and place it in the document
1970 * const frame = new OO.ui.PanelLayout( {
1974 * const contentFrame = new OO.ui.PanelLayout( {
1978 * frame.$element.append(
1980 * contentFrame.$element.append( $area )
1982 * $( document.body ).append( frame.$element );
1984 * // Here is where the toolbar is actually built. This must be done after inserting it into the
1986 * toolbar.initialize();
1989 * @extends OO.ui.ToolGroup
1992 * @param {OO.ui.Toolbar} toolbar
1993 * @param {Object} [config] Configuration options
1995 OO.ui.BarToolGroup = function OoUiBarToolGroup( toolbar, config ) {
1996 // Allow passing positional parameters inside the config object
1997 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
1999 toolbar = config.toolbar;
2002 // Parent constructor
2003 OO.ui.BarToolGroup.super.call( this, toolbar, config );
2006 this.$element.addClass( 'oo-ui-barToolGroup' );
2007 this.$group.addClass( 'oo-ui-barToolGroup-tools' );
2012 OO.inheritClass( OO.ui.BarToolGroup, OO.ui.ToolGroup );
2014 /* Static Properties */
2020 OO.ui.BarToolGroup.static.titleTooltips = true;
2026 OO.ui.BarToolGroup.static.accelTooltips = true;
2032 OO.ui.BarToolGroup.static.name = 'bar';
2035 * PopupToolGroup is an abstract base class used by both {@link OO.ui.MenuToolGroup MenuToolGroup}
2036 * and {@link OO.ui.ListToolGroup ListToolGroup} to provide a popup (an overlaid menu or list of
2037 * tools with an optional icon and label). This class can be used for other base classes that
2038 * also use this functionality.
2042 * @extends OO.ui.ToolGroup
2043 * @mixes OO.ui.mixin.IconElement
2044 * @mixes OO.ui.mixin.IndicatorElement
2045 * @mixes OO.ui.mixin.LabelElement
2046 * @mixes OO.ui.mixin.TitledElement
2047 * @mixes OO.ui.mixin.FlaggedElement
2048 * @mixes OO.ui.mixin.ClippableElement
2049 * @mixes OO.ui.mixin.FloatableElement
2050 * @mixes OO.ui.mixin.TabIndexedElement
2053 * @param {OO.ui.Toolbar} toolbar
2054 * @param {Object} [config] Configuration options
2055 * @param {string} [config.header] Text to display at the top of the popup
2056 * @param {Object} [config.narrowConfig] See static.narrowConfig
2058 OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
2059 // Allow passing positional parameters inside the config object
2060 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
2062 toolbar = config.toolbar;
2065 // Configuration initialization
2066 config = Object.assign( {
2067 indicator: config.indicator === undefined ?
2068 ( toolbar.position === 'bottom' ? 'up' : 'down' ) : config.indicator
2071 // Parent constructor
2072 OO.ui.PopupToolGroup.super.call( this, toolbar, config );
2075 this.active = false;
2076 this.dragging = false;
2077 // Don't conflict with parent method of the same name
2078 this.onPopupDocumentMouseKeyUpHandler = this.onPopupDocumentMouseKeyUp.bind( this );
2079 this.$handle = $( '<span>' );
2080 this.narrowConfig = config.narrowConfig || this.constructor.static.narrowConfig;
2082 // Mixin constructors
2083 OO.ui.mixin.IconElement.call( this, config );
2084 OO.ui.mixin.IndicatorElement.call( this, config );
2085 OO.ui.mixin.LabelElement.call( this, config );
2086 OO.ui.mixin.TitledElement.call( this, config );
2087 OO.ui.mixin.FlaggedElement.call( this, config );
2088 OO.ui.mixin.ClippableElement.call( this, Object.assign( {
2089 $clippable: this.$group
2091 OO.ui.mixin.FloatableElement.call( this, Object.assign( {
2092 $floatable: this.$group,
2093 $floatableContainer: this.$handle,
2094 hideWhenOutOfView: false,
2095 verticalPosition: this.toolbar.position === 'bottom' ? 'above' : 'below'
2096 // horizontalPosition is set in setActive
2098 OO.ui.mixin.TabIndexedElement.call( this, Object.assign( {
2099 $tabIndexed: this.$handle
2104 keydown: this.onHandleMouseKeyDown.bind( this ),
2105 keyup: this.onHandleMouseKeyUp.bind( this ),
2106 mousedown: this.onHandleMouseKeyDown.bind( this ),
2107 mouseup: this.onHandleMouseKeyUp.bind( this )
2109 this.toolbar.connect( this, {
2110 resize: 'onToolbarResize'
2115 .addClass( 'oo-ui-popupToolGroup-handle' )
2116 .attr( { role: 'button', 'aria-expanded': 'false' } )
2117 .append( this.$icon, this.$label, this.$indicator );
2118 // If the pop-up should have a header, add it to the top of the toolGroup.
2119 // Note: If this feature is useful for other widgets, we could abstract it into an
2120 // OO.ui.HeaderedElement mixin constructor.
2121 if ( config.header !== undefined ) {
2123 .prepend( $( '<span>' )
2124 .addClass( 'oo-ui-popupToolGroup-header' )
2125 .text( config.header )
2129 .addClass( 'oo-ui-popupToolGroup' )
2130 .prepend( this.$handle );
2131 this.$group.addClass( 'oo-ui-popupToolGroup-tools' );
2132 this.toolbar.$popups.append( this.$group );
2137 OO.inheritClass( OO.ui.PopupToolGroup, OO.ui.ToolGroup );
2138 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.IconElement );
2139 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.IndicatorElement );
2140 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.LabelElement );
2141 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.TitledElement );
2142 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.FlaggedElement );
2143 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.ClippableElement );
2144 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.FloatableElement );
2145 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.TabIndexedElement );
2147 /* Static properties */
2150 * Config options to change when toolbar is in narrow mode
2152 * Supports `invisibleLabel`, label` and `icon` properties.
2155 * @property {Object|null}
2157 OO.ui.PopupToolGroup.static.narrowConfig = null;
2164 OO.ui.PopupToolGroup.prototype.setDisabled = function () {
2166 OO.ui.PopupToolGroup.super.prototype.setDisabled.apply( this, arguments );
2168 if ( this.isDisabled() && this.isElementAttached() ) {
2169 this.setActive( false );
2174 * Handle resize events from the toolbar
2176 OO.ui.PopupToolGroup.prototype.onToolbarResize = function () {
2177 if ( !this.narrowConfig ) {
2180 if ( this.toolbar.isNarrow() ) {
2181 if ( this.narrowConfig.invisibleLabel !== undefined ) {
2182 this.wideInvisibleLabel = this.invisibleLabel;
2183 this.setInvisibleLabel( this.narrowConfig.invisibleLabel );
2185 if ( this.narrowConfig.label !== undefined ) {
2186 this.wideLabel = this.label;
2187 this.setLabel( this.narrowConfig.label );
2189 if ( this.narrowConfig.icon !== undefined ) {
2190 this.wideIcon = this.icon;
2191 this.setIcon( this.narrowConfig.icon );
2194 if ( this.wideInvisibleLabel !== undefined ) {
2195 this.setInvisibleLabel( this.wideInvisibleLabel );
2197 if ( this.wideLabel !== undefined ) {
2198 this.setLabel( this.wideLabel );
2200 if ( this.wideIcon !== undefined ) {
2201 this.setIcon( this.wideIcon );
2207 * Handle document mouse up and key up events.
2210 * @param {MouseEvent|KeyboardEvent} e Mouse up or key up event
2212 OO.ui.PopupToolGroup.prototype.onPopupDocumentMouseKeyUp = function ( e ) {
2213 const $target = $( e.target );
2214 // Only deactivate when clicking outside the dropdown element
2215 if ( $target.closest( '.oo-ui-popupToolGroup' )[ 0 ] === this.$element[ 0 ] ) {
2218 if ( $target.closest( '.oo-ui-popupToolGroup-tools' )[ 0 ] === this.$group[ 0 ] ) {
2221 this.setActive( false );
2227 OO.ui.PopupToolGroup.prototype.onMouseKeyUp = function ( e ) {
2228 // Only close toolgroup when a tool was actually selected
2230 !this.isDisabled() && this.pressed && this.pressed === this.findTargetTool( e ) && (
2231 e.which === OO.ui.MouseButtons.LEFT ||
2232 e.which === OO.ui.Keys.SPACE ||
2233 e.which === OO.ui.Keys.ENTER
2236 this.setActive( false );
2238 return OO.ui.PopupToolGroup.super.prototype.onMouseKeyUp.call( this, e );
2244 OO.ui.PopupToolGroup.prototype.onMouseKeyDown = function ( e ) {
2245 // Shift-Tab on the first tool in the group jumps to the handle.
2246 // Tab on the last tool in the group jumps to the next group.
2247 if ( !this.isDisabled() && e.which === OO.ui.Keys.TAB ) {
2248 // We can't use this.items because ListToolGroup inserts the extra fake
2249 // expand/collapse tool.
2250 const $focused = $( document.activeElement );
2251 const $firstFocusable = OO.ui.findFocusable( this.$group );
2252 if ( $focused[ 0 ] === $firstFocusable[ 0 ] && e.shiftKey ) {
2253 this.$handle.trigger( 'focus' );
2256 const $lastFocusable = OO.ui.findFocusable( this.$group, true );
2257 if ( $focused[ 0 ] === $lastFocusable[ 0 ] && !e.shiftKey ) {
2258 // Focus this group's handle and let the browser's tab handling happen
2259 // (no 'return false').
2260 // This way we don't have to fiddle with other ToolGroups' business, or worry what to do
2261 // if the next group is not a PopupToolGroup or doesn't exist at all.
2262 this.$handle.trigger( 'focus' );
2263 // Close the popup so that we don't move back inside it (if this is the last group).
2264 this.setActive( false );
2267 return OO.ui.PopupToolGroup.super.prototype.onMouseKeyDown.call( this, e );
2271 * Handle mouse up and key up events.
2274 * @param {jQuery.Event} e Mouse up or key up event
2275 * @return {undefined|boolean} False to prevent default if event is handled
2277 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyUp = function ( e ) {
2279 !this.isDisabled() && (
2280 e.which === OO.ui.MouseButtons.LEFT ||
2281 e.which === OO.ui.Keys.SPACE ||
2282 e.which === OO.ui.Keys.ENTER
2290 * Handle mouse down and key down events.
2293 * @param {jQuery.Event} e Mouse down or key down event
2294 * @return {undefined|boolean} False to prevent default if event is handled
2296 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyDown = function ( e ) {
2298 if ( !this.isDisabled() ) {
2299 // Tab on the handle jumps to the first tool in the group (if the popup is open).
2300 if ( e.which === OO.ui.Keys.TAB && !e.shiftKey ) {
2301 $focusable = OO.ui.findFocusable( this.$group );
2302 if ( $focusable.length ) {
2303 $focusable.trigger( 'focus' );
2308 e.which === OO.ui.MouseButtons.LEFT ||
2309 e.which === OO.ui.Keys.SPACE ||
2310 e.which === OO.ui.Keys.ENTER
2312 this.setActive( !this.active );
2319 * Check if the tool group is active.
2321 * @return {boolean} Tool group is active
2323 OO.ui.PopupToolGroup.prototype.isActive = function () {
2328 * Switch into 'active' mode.
2330 * When active, the popup is visible. A mouseup event anywhere in the document will trigger
2333 * @param {boolean} [value=false] The active state to set
2334 * @fires OO.ui.PopupToolGroup#active
2336 OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
2337 let containerWidth, containerLeft;
2339 if ( this.active !== value ) {
2340 this.active = value;
2342 this.getElementDocument().addEventListener(
2344 this.onPopupDocumentMouseKeyUpHandler,
2347 this.getElementDocument().addEventListener(
2349 this.onPopupDocumentMouseKeyUpHandler,
2353 this.$clippable.css( 'left', '' );
2354 this.$element.addClass( 'oo-ui-popupToolGroup-active' );
2355 this.$group.addClass( 'oo-ui-popupToolGroup-active-tools' );
2356 this.$handle.attr( 'aria-expanded', true );
2357 this.togglePositioning( true );
2358 this.toggleClipping( true );
2360 // Tools on the left of the toolbar will try to align their
2361 // popups with their left side if possible, and vice-versa.
2362 const preferredSide = this.align === 'before' ? 'start' : 'end';
2363 const otherSide = this.align === 'before' ? 'end' : 'start';
2365 // Try anchoring the popup to the preferred side first
2366 this.setHorizontalPosition( preferredSide );
2368 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
2369 // Anchoring to the preferred side caused the popup to clip, so anchor it
2370 // to the other side instead.
2371 this.setHorizontalPosition( otherSide );
2373 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
2374 // Anchoring to the right also caused the popup to clip, so just make it fill the
2376 containerWidth = this.$clippableScrollableContainer.width();
2377 containerLeft = this.$clippableScrollableContainer[ 0 ] ===
2378 document.documentElement ?
2380 this.$clippableScrollableContainer.offset().left;
2382 this.toggleClipping( false );
2383 this.setHorizontalPosition( preferredSide );
2385 this.$clippable.css( {
2386 'margin-left': -( this.$element.offset().left - containerLeft ),
2387 width: containerWidth
2391 this.getElementDocument().removeEventListener(
2393 this.onPopupDocumentMouseKeyUpHandler,
2396 this.getElementDocument().removeEventListener(
2398 this.onPopupDocumentMouseKeyUpHandler,
2401 this.$element.removeClass( 'oo-ui-popupToolGroup-active' );
2402 this.$group.removeClass( 'oo-ui-popupToolGroup-active-tools' );
2403 this.$handle.attr( 'aria-expanded', false );
2404 this.togglePositioning( false );
2405 this.toggleClipping( false );
2407 this.emit( 'active', this.active );
2408 this.updateThemeClasses();
2413 * ListToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
2414 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are
2415 * {@link OO.ui.MenuToolGroup MenuToolGroup} and {@link OO.ui.BarToolGroup BarToolGroup}).
2416 * The {@link OO.ui.Tool tools} in a ListToolGroup are displayed by label in a dropdown menu.
2417 * The title of the tool is used as the label text. The menu itself can be configured with a label,
2418 * icon, indicator, header, and title.
2420 * ListToolGroups can be configured to be expanded and collapsed. Collapsed lists will have a
2421 * ‘More’ option that users can select to see the full list of tools. If a collapsed toolgroup is
2422 * expanded, a ‘Fewer’ option permits users to collapse the list again.
2424 * ListToolGroups are created by a {@link OO.ui.ToolGroupFactory toolgroup factory} when the
2425 * toolbar is set up. The factory requires the ListToolGroup's symbolic name, 'list', which is
2426 * specified along with the other configurations. For more information about how to add tools to a
2427 * ListToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
2429 * For more information about toolbars in general, please see the
2430 * [OOUI documentation on MediaWiki][1].
2432 * [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars
2435 * // Example of a ListToolGroup
2436 * const toolFactory = new OO.ui.ToolFactory();
2437 * const toolGroupFactory = new OO.ui.ToolGroupFactory();
2438 * const toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
2440 * // Configure and register two tools
2441 * function SettingsTool() {
2442 * SettingsTool.super.apply( this, arguments );
2444 * OO.inheritClass( SettingsTool, OO.ui.Tool );
2445 * SettingsTool.static.name = 'settings';
2446 * SettingsTool.static.icon = 'settings';
2447 * SettingsTool.static.title = 'Change settings';
2448 * SettingsTool.prototype.onSelect = function () {
2449 * this.setActive( false );
2451 * SettingsTool.prototype.onUpdateState = function () {};
2452 * toolFactory.register( SettingsTool );
2453 * // Register two more tools, nothing interesting here
2454 * function StuffTool() {
2455 * StuffTool.super.apply( this, arguments );
2457 * OO.inheritClass( StuffTool, OO.ui.Tool );
2458 * StuffTool.static.name = 'stuff';
2459 * StuffTool.static.icon = 'search';
2460 * StuffTool.static.title = 'Change the world';
2461 * StuffTool.prototype.onSelect = function () {
2462 * this.setActive( false );
2464 * StuffTool.prototype.onUpdateState = function () {};
2465 * toolFactory.register( StuffTool );
2468 * // Configurations for list toolgroup.
2470 * label: 'ListToolGroup',
2472 * title: 'This is the title, displayed when user moves the mouse over the list ' +
2474 * header: 'This is the header',
2475 * include: [ 'settings', 'stuff' ],
2476 * allowCollapse: ['stuff']
2480 * // Create some UI around the toolbar and place it in the document
2481 * const frame = new OO.ui.PanelLayout( {
2485 * frame.$element.append(
2488 * $( document.body ).append( frame.$element );
2489 * // Build the toolbar. This must be done after the toolbar has been appended to the document.
2490 * toolbar.initialize();
2493 * @extends OO.ui.PopupToolGroup
2496 * @param {OO.ui.Toolbar} toolbar
2497 * @param {Object} [config] Configuration options
2498 * @param {Array} [config.allowCollapse] Allow the specified tools to be collapsed. By default, collapsible
2499 * tools will only be displayed if users click the ‘More’ option displayed at the bottom of the
2500 * list. If the list is expanded, a ‘Fewer’ option permits users to collapse the list again.
2501 * Any tools that are included in the toolgroup, but are not designated as collapsible, will always
2503 * To open a collapsible list in its expanded state, set #expanded to 'true'.
2504 * @param {Array} [config.forceExpand] Expand the specified tools. All other tools will be designated as
2505 * collapsible. Unless #expanded is set to true, the collapsible tools will be collapsed when the
2506 * list is first opened.
2507 * @param {boolean} [config.expanded=false] Expand collapsible tools. This config is only relevant if tools
2508 * have been designated as collapsible. When expanded is set to true, all tools in the group will
2509 * be displayed when the list is first opened. Users can collapse the list with a ‘Fewer’ option at
2512 OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
2513 // Allow passing positional parameters inside the config object
2514 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
2516 toolbar = config.toolbar;
2519 // Configuration initialization
2520 config = config || {};
2522 // Properties (must be set before parent constructor, which calls #populate)
2523 this.allowCollapse = config.allowCollapse;
2524 this.forceExpand = config.forceExpand;
2525 this.expanded = !!config.expanded;
2526 this.collapsibleTools = [];
2528 // Parent constructor
2529 OO.ui.ListToolGroup.super.call( this, toolbar, config );
2532 this.$element.addClass( 'oo-ui-listToolGroup' );
2533 this.$group.addClass( 'oo-ui-listToolGroup-tools' );
2538 OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup );
2540 /* Static Properties */
2546 OO.ui.ListToolGroup.static.name = 'list';
2553 OO.ui.ListToolGroup.prototype.populate = function () {
2554 OO.ui.ListToolGroup.super.prototype.populate.call( this );
2556 let allowCollapse = [];
2557 // Update the list of collapsible tools
2558 if ( this.allowCollapse !== undefined ) {
2559 allowCollapse = this.allowCollapse;
2560 } else if ( this.forceExpand !== undefined ) {
2561 allowCollapse = OO.simpleArrayDifference( Object.keys( this.tools ), this.forceExpand );
2564 this.collapsibleTools = [];
2565 for ( let i = 0, len = allowCollapse.length; i < len; i++ ) {
2566 if ( this.tools[ allowCollapse[ i ] ] !== undefined ) {
2567 this.collapsibleTools.push( this.tools[ allowCollapse[ i ] ] );
2571 // Keep at the end, even when tools are added
2572 this.$group.append( this.getExpandCollapseTool().$element );
2574 this.getExpandCollapseTool().toggle( this.collapsibleTools.length !== 0 );
2575 this.updateCollapsibleState();
2579 * Get the expand/collapse tool for this group
2581 * @return {OO.ui.Tool} Expand collapse tool
2583 OO.ui.ListToolGroup.prototype.getExpandCollapseTool = function () {
2584 if ( this.expandCollapseTool === undefined ) {
2585 const ExpandCollapseTool = function () {
2586 ExpandCollapseTool.super.apply( this, arguments );
2589 OO.inheritClass( ExpandCollapseTool, OO.ui.Tool );
2591 ExpandCollapseTool.prototype.onSelect = function () {
2592 this.toolGroup.expanded = !this.toolGroup.expanded;
2593 this.toolGroup.updateCollapsibleState();
2594 this.setActive( false );
2596 ExpandCollapseTool.prototype.onUpdateState = function () {
2597 // Do nothing. Tool interface requires an implementation of this function.
2600 ExpandCollapseTool.static.name = 'more-fewer';
2602 this.expandCollapseTool = new ExpandCollapseTool( this );
2604 return this.expandCollapseTool;
2610 OO.ui.ListToolGroup.prototype.onMouseKeyUp = function ( e ) {
2611 // Do not close the popup when the user wants to show more/fewer tools
2613 $( e.target ).closest( '.oo-ui-tool-name-more-fewer' ).length && (
2614 e.which === OO.ui.MouseButtons.LEFT ||
2615 e.which === OO.ui.Keys.SPACE ||
2616 e.which === OO.ui.Keys.ENTER
2619 // HACK: Prevent the popup list from being hidden. Skip the PopupToolGroup implementation
2620 // (which hides the popup list when a tool is selected) and call ToolGroup's implementation
2622 return OO.ui.ListToolGroup.super.super.prototype.onMouseKeyUp.call( this, e );
2624 return OO.ui.ListToolGroup.super.prototype.onMouseKeyUp.call( this, e );
2628 OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () {
2629 const inverted = this.toolbar.position === 'bottom',
2630 icon = this.expanded === inverted ? 'expand' : 'collapse';
2632 this.getExpandCollapseTool()
2634 .setTitle( OO.ui.msg( this.expanded ? 'ooui-toolgroup-collapse' : 'ooui-toolgroup-expand' ) );
2636 for ( let i = 0; i < this.collapsibleTools.length; i++ ) {
2637 this.collapsibleTools[ i ].toggle( this.expanded );
2640 // Re-evaluate clipping, because our height has changed
2645 * MenuToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
2646 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are
2647 * {@link OO.ui.BarToolGroup BarToolGroup} and {@link OO.ui.ListToolGroup ListToolGroup}).
2648 * MenuToolGroups contain selectable {@link OO.ui.Tool tools}, which are displayed by label in a
2649 * dropdown menu. The tool's title is used as the label text, and the menu label is updated to
2650 * reflect which tool or tools are currently selected. If no tools are selected, the menu label
2651 * is empty. The menu can be configured with an indicator, icon, title, and/or header.
2653 * MenuToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the
2654 * toolbar is set up.
2656 * For more information about how to add tools to a MenuToolGroup, please see
2657 * {@link OO.ui.ToolGroup toolgroup}.
2658 * For more information about toolbars in general, please see the
2659 * [OOUI documentation on MediaWiki][1].
2661 * [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars
2664 * // Example of a MenuToolGroup
2665 * const toolFactory = new OO.ui.ToolFactory();
2666 * const toolGroupFactory = new OO.ui.ToolGroupFactory();
2667 * const toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
2669 * // We will be placing status text in this element when tools are used
2670 * const $area = $( '<p>' ).text( 'An example of a MenuToolGroup. Select a tool from the '
2671 * + 'dropdown menu.' );
2673 * // Define the tools that we're going to place in our toolbar
2675 * function SettingsTool() {
2676 * SettingsTool.super.apply( this, arguments );
2677 * this.reallyActive = false;
2679 * OO.inheritClass( SettingsTool, OO.ui.Tool );
2680 * SettingsTool.static.name = 'settings';
2681 * SettingsTool.static.icon = 'settings';
2682 * SettingsTool.static.title = 'Change settings';
2683 * SettingsTool.prototype.onSelect = function () {
2684 * $area.text( 'Settings tool clicked!' );
2685 * // Toggle the active state on each click
2686 * this.reallyActive = !this.reallyActive;
2687 * this.setActive( this.reallyActive );
2688 * // To update the menu label
2689 * this.toolbar.emit( 'updateState' );
2691 * SettingsTool.prototype.onUpdateState = function () {};
2692 * toolFactory.register( SettingsTool );
2694 * function StuffTool() {
2695 * StuffTool.super.apply( this, arguments );
2696 * this.reallyActive = false;
2698 * OO.inheritClass( StuffTool, OO.ui.Tool );
2699 * StuffTool.static.name = 'stuff';
2700 * StuffTool.static.icon = 'ellipsis';
2701 * StuffTool.static.title = 'More stuff';
2702 * StuffTool.prototype.onSelect = function () {
2703 * $area.text( 'More stuff tool clicked!' );
2704 * // Toggle the active state on each click
2705 * this.reallyActive = !this.reallyActive;
2706 * this.setActive( this.reallyActive );
2707 * // To update the menu label
2708 * this.toolbar.emit( 'updateState' );
2710 * StuffTool.prototype.onUpdateState = function () {};
2711 * toolFactory.register( StuffTool );
2713 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
2714 * // used once (but not all defined tools must be used).
2718 * header: 'This is the (optional) header',
2719 * title: 'This is the (optional) title',
2720 * include: [ 'settings', 'stuff' ]
2724 * // Create some UI around the toolbar and place it in the document
2725 * const frame = new OO.ui.PanelLayout( {
2729 * const contentFrame = new OO.ui.PanelLayout( {
2733 * frame.$element.append(
2735 * contentFrame.$element.append( $area )
2737 * $( document.body ).append( frame.$element );
2739 * // Here is where the toolbar is actually built. This must be done after inserting it into the
2741 * toolbar.initialize();
2742 * toolbar.emit( 'updateState' );
2745 * @extends OO.ui.PopupToolGroup
2748 * @param {OO.ui.Toolbar} toolbar
2749 * @param {Object} [config] Configuration options
2751 OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
2752 // Allow passing positional parameters inside the config object
2753 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
2755 toolbar = config.toolbar;
2758 // Configuration initialization
2759 config = config || {};
2761 // Parent constructor
2762 OO.ui.MenuToolGroup.super.call( this, toolbar, config );
2765 this.toolbar.connect( this, {
2766 updateState: 'onUpdateState'
2770 this.$element.addClass( 'oo-ui-menuToolGroup' );
2771 this.$group.addClass( 'oo-ui-menuToolGroup-tools' );
2776 OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup );
2778 /* Static Properties */
2784 OO.ui.MenuToolGroup.static.name = 'menu';
2789 * Handle the toolbar state being updated.
2791 * When the state changes, the title of each active item in the menu will be joined together and
2792 * used as a label for the group. The label will be empty if none of the items are active.
2796 OO.ui.MenuToolGroup.prototype.onUpdateState = function () {
2797 const labelTexts = [];
2799 for ( const name in this.tools ) {
2800 if ( this.tools[ name ].isActive() ) {
2801 labelTexts.push( this.tools[ name ].getTitle() );
2805 this.setLabel( labelTexts.join( ', ' ) || ' ' );
2810 //# sourceMappingURL=oojs-ui-toolbars.js.map.json