Merge "Bump wikimedia/parsoid to 0.21.0-a11"
[mediawiki.git] / resources / lib / ooui / oojs-ui-toolbars.js
blob125e4b17a1aa1c056440a7cdf9e736ba2ba81c56
1 /*!
2  * OOUI v0.51.4
3  * https://www.mediawiki.org/wiki/OOUI
4  *
5  * Copyright 2011–2024 OOUI Team and other contributors.
6  * Released under the MIT license
7  * http://oojs.mit-license.org
8  *
9  * Date: 2024-12-05T17:34:41Z
10  */
11 ( function ( OO ) {
13 'use strict';
15 /**
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.
19  *
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.
23  *
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.
29  *
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}.
36  *
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 );
43  *
44  *     // We will be placing status text in this element when tools are used
45  *     const $area = $( '<p>' ).text( 'Toolbar example' );
46  *
47  *     // Define the tools that we're going to place in our toolbar
48  *
49  *     // Create a class inheriting from OO.ui.Tool
50  *     function SearchTool() {
51  *         SearchTool.super.apply( this, arguments );
52  *     }
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 );
64  *     };
65  *     SearchTool.prototype.onUpdateState = function () {};
66  *     // Make this tool available in our toolFactory and thus our toolbar
67  *     toolFactory.register( SearchTool );
68  *
69  *     // Register two more tools, nothing interesting here
70  *     function SettingsTool() {
71  *         SettingsTool.super.apply( this, arguments );
72  *     }
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 );
80  *     };
81  *     SettingsTool.prototype.onUpdateState = function () {};
82  *     toolFactory.register( SettingsTool );
83  *
84  *     // Register two more tools, nothing interesting here
85  *     function StuffTool() {
86  *         StuffTool.super.apply( this, arguments );
87  *     }
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 );
95  *     };
96  *     StuffTool.prototype.onUpdateState = function () {};
97  *     toolFactory.register( StuffTool );
98  *
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: {
103  *             padded: true,
104  *             label: 'Help',
105  *             head: true
106  *         } }, config ) );
107  *         this.popup.$body.append( '<p>I am helpful!</p>' );
108  *     }
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).
117  *     toolbar.setup( [
118  *         {
119  *             // 'bar' tool groups display tools' icons only, side-by-side.
120  *             type: 'bar',
121  *             include: [ 'search', 'help' ]
122  *         },
123  *         {
124  *             // 'list' tool groups display both the titles and icons, in a dropdown list.
125  *             type: 'list',
126  *             indicator: 'down',
127  *             label: 'More',
128  *             include: [ 'settings', 'stuff' ]
129  *         }
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.)
133  *     ] );
135  *     // Create some UI around the toolbar and place it in the document
136  *     const frame = new OO.ui.PanelLayout( {
137  *         expanded: false,
138  *         framed: true
139  *     } );
140  *     const contentFrame = new OO.ui.PanelLayout( {
141  *         expanded: false,
142  *         padded: true
143  *     } );
144  *     frame.$element.append(
145  *         toolbar.$element,
146  *         contentFrame.$element.append( $area )
147  *     );
148  *     $( document.body ).append( frame.$element );
150  *     // Here is where the toolbar is actually built. This must be done after inserting it into the
151  *     // document.
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 );
170  *     }
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 );
182  *     };
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;
191  *     }
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' );
203  *     };
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;
211  *     }
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' );
223  *     };
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: {
231  *             padded: true,
232  *             label: 'Help',
233  *             head: true
234  *         } }, config ) );
235  *         this.popup.$body.append( '<p>I am helpful!</p>' );
236  *     }
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).
245  *     toolbar.setup( [
246  *         {
247  *             // 'bar' tool groups display tools' icons only, side-by-side.
248  *             type: 'bar',
249  *             include: [ 'search', 'help' ]
250  *         },
251  *         {
252  *             // 'menu' tool groups display both the titles and icons, in a dropdown menu.
253  *             // Menu label indicates which items are selected.
254  *             type: 'menu',
255  *             indicator: 'down',
256  *             include: [ 'settings', 'stuff' ]
257  *         }
258  *     ] );
260  *     // Create some UI around the toolbar and place it in the document
261  *     const frame = new OO.ui.PanelLayout( {
262  *         expanded: false,
263  *         framed: true
264  *     } );
265  *     const contentFrame = new OO.ui.PanelLayout( {
266  *         expanded: false,
267  *         padded: true
268  *     } );
269  *     frame.$element.append(
270  *         toolbar.$element,
271  *         contentFrame.$element.append( $area )
272  *     );
273  *     $( document.body ).append( frame.$element );
275  *     // Here is where the toolbar is actually built. This must be done after inserting it into the
276  *     // document.
277  *     toolbar.initialize();
278  *     toolbar.emit( 'updateState' );
280  * @class
281  * @extends OO.ui.Element
282  * @mixes OO.EventEmitter
283  * @mixes OO.ui.mixin.GroupElement
285  * @constructor
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>.
297  */
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;
304         }
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 );
316         // Properties
317         this.toolFactory = toolFactory;
318         this.toolGroupFactory = toolGroupFactory;
319         this.groupsByName = {};
320         this.activeToolGroups = 0;
321         this.tools = {};
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;
328         this.narrow = false;
329         this.narrowThreshold = null;
330         this.onWindowResizeHandler = this.onWindowResize.bind( this );
331         this.$overlay = ( config.$overlay === true ? OO.ui.getDefaultOverlay() : config.$overlay ) ||
332                 this.$element;
334         // Events
335         this.$element
336                 .add( this.$bar ).add( this.$group ).add( this.$after ).add( this.$actions )
337                 .on( 'mousedown keydown', this.onPointerDown.bind( this ) );
339         // Initialization
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' ) );
348         }
349         this.$bar.append( $( '<div>' ).css( 'clear', 'both' ) );
351         // Possible classes: oo-ui-toolbar-position-top, oo-ui-toolbar-position-bottom
352         this.$element
353                 .addClass( 'oo-ui-toolbar oo-ui-toolbar-position-' + this.position )
354                 .append( this.$bar );
355         this.$overlay.append( this.$popups );
358 /* Setup */
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 );
364 /* Events */
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
373  */
376  * An 'active' event is emitted when the number of active toolgroups increases from 0, or
377  * returns to 0.
379  * @event OO.ui.Toolbar#active
380  * @param {boolean} There are active toolgroups in this toolbar
381  */
384  * Toolbar has resized to a point where narrow mode has changed
386  * @event OO.ui.Toolbar#resize
387  */
389 /* Methods */
392  * Get the tool factory.
394  * @return {OO.ui.ToolFactory} Tool factory
395  */
396 OO.ui.Toolbar.prototype.getToolFactory = function () {
397         return this.toolFactory;
401  * Get the toolgroup factory.
403  * @return {OO.Factory} Toolgroup factory
404  */
405 OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
406         return this.toolGroupFactory;
410  * @inheritdoc {OO.ui.mixin.GroupElement}
411  */
412 OO.ui.Toolbar.prototype.insertItemElements = function ( item ) {
413         // Mixin method
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 );
419         }
423  * Handles mouse down events.
425  * @private
426  * @param {jQuery.Event} e Mouse down event
427  * @return {undefined|boolean} False to prevent default if event is handled
428  */
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' );
432         if (
433                 !$closestWidgetToEvent.length ||
434                 $closestWidgetToEvent[ 0 ] ===
435                 $closestWidgetToToolbar[ 0 ]
436         ) {
437                 return false;
438         }
442  * Handle window resize event.
444  * @private
445  * @param {jQuery.Event} e Window resize event
446  */
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
455  */
456 OO.ui.Toolbar.prototype.isNarrow = function () {
457         return this.narrow;
461  * Set the narrow mode flag
463  * @param {boolean} narrow Toolbar is in narrow mode
464  */
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',
470                         this.narrow
471                 );
472                 this.emit( 'resize' );
473         }
477  * Get the (lazily-computed) width threshold for applying the oo-ui-toolbar-narrow
478  * class.
480  * @private
481  * @return {number} Width threshold in pixels
482  */
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;
487         }
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.
494  */
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();
500         }
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
522  */
523 OO.ui.Toolbar.prototype.setup = function ( groups ) {
524         const defaultType = 'bar';
526         // Cleanup previous groups
527         this.reset();
529         const items = [];
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';
537                         }
538                         if ( groupConfig.label === undefined ) {
539                                 groupConfig.label = OO.ui.msg( 'ooui-toolbar-more' );
540                         }
541                 }
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'
550                 } );
551         }
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
560  */
561 OO.ui.Toolbar.prototype.onToolGroupActive = function ( active ) {
562         if ( active ) {
563                 this.activeToolGroups++;
564                 if ( this.activeToolGroups === 1 ) {
565                         this.emit( 'active', true );
566                 }
567         } else {
568                 this.activeToolGroups--;
569                 if ( this.activeToolGroups === 0 ) {
570                         this.emit( 'active', false );
571                 }
572         }
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
580  */
581 OO.ui.Toolbar.prototype.getToolGroupByName = function ( name ) {
582         return this.groupsByName[ name ] || null;
586  * Remove all tools and toolgroups from the toolbar.
587  */
588 OO.ui.Toolbar.prototype.reset = function () {
589         this.groupsByName = {};
590         this.tools = {};
591         for ( let i = 0, len = this.items.length; i < len; i++ ) {
592                 this.items[ i ].destroy();
593         }
594         this.clearItems();
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.
602  */
603 OO.ui.Toolbar.prototype.destroy = function () {
604         $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
605         this.reset();
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
616  */
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
625  */
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
634  */
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
645  * the method.
647  * @param {string} name Symbolic name of tool
648  * @return {string|undefined} Tool accelerator label if available
649  */
650 OO.ui.Toolbar.prototype.getToolAccelerator = function () {
651         return undefined;
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
669  * example.
671  * For more information, please see the [OOUI documentation on MediaWiki][1].
672  * [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars
674  * @abstract
675  * @class
676  * @extends OO.ui.Widget
677  * @mixes OO.ui.mixin.IconElement
678  * @mixes OO.ui.mixin.FlaggedElement
679  * @mixes OO.ui.mixin.TabIndexedElement
681  * @constructor
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.
699  */
700 OO.ui.Tool = function OoUiTool( toolGroup, config ) {
701         // Allow passing positional parameters inside the config object
702         if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
703                 config = toolGroup;
704                 toolGroup = config.toolGroup;
705         }
707         // Configuration initialization
708         config = config || {};
710         // Parent constructor
711         OO.ui.Tool.super.call( this, config );
713         // Properties
714         this.toolGroup = toolGroup;
715         this.toolbar = this.toolGroup.getToolbar();
716         this.active = false;
717         this.$title = $( '<span>' );
718         this.$accel = $( '<span>' );
719         this.$link = $( '<a>' );
720         this.title = null;
721         this.checkIcon = new OO.ui.IconWidget( {
722                 icon: 'check',
723                 classes: [ 'oo-ui-tool-checkIcon' ]
724         } );
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
734         }, config ) );
736         // Events
737         this.toolbar.connect( this, {
738                 updateState: 'onUpdateState',
739                 resize: 'onToolbarResize'
740         } );
742         // Initialization
743         this.$title.addClass( 'oo-ui-tool-title' );
744         this.$accel
745                 .addClass( 'oo-ui-tool-accel' )
746                 .prop( {
747                         // This may need to be changed if the key names are ever localized,
748                         // but for now they are essentially written in English
749                         dir: 'ltr',
750                         lang: 'en'
751                 } );
752         this.$link
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 );
761         }
763         this.$element
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 );
772 /* Setup */
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 */
782  * @static
783  * @inheritdoc
784  */
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.
793  * @abstract
794  * @static
795  * @property {string}
796  */
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}.
805  * @abstract
806  * @static
807  * @property {string}
808  */
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.
815  * @abstract
816  * @static
817  * @property {string|Function}
818  */
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.
825  * @static
826  * @property {boolean}
827  */
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 (*).
836  * @static
837  * @property {boolean}
838  */
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).
848  * @static
849  * @property {boolean}
850  */
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.
860  * @static
861  * @param {any} data Data to check
862  * @return {boolean} Tool can be used with data
863  */
864 OO.ui.Tool.static.isCompatibleWith = function () {
865         return false;
869  * Config options to change when toolbar is in narrow mode
871  * Supports `displayBothIconAndLabel`, `title` and `icon` properties.
873  * @static
874  * @property {Object|null}
875  */
876 OO.ui.Tool.static.narrowConfig = null;
878 /* Methods */
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.
889  * @method
890  * @protected
891  * @abstract
892  */
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.
901  * @method
902  * @protected
903  * @abstract
904  */
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
914  */
915 OO.ui.Tool.prototype.isActive = function () {
916         return this.active;
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
926  */
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
937  * @chainable
938  * @return {OO.ui.Tool} The tool, for chaining
939  */
940 OO.ui.Tool.prototype.setTitle = function ( title ) {
941         this.title = OO.ui.resolveMsg( title );
942         this.updateTitle();
943         // Update classes
944         this.setDisplayBothIconAndLabel( this.displayBothIconAndLabel );
945         return this;
949  * Set the tool's displayBothIconAndLabel state.
951  * Update title classes if necessary
953  * @param {boolean} displayBothIconAndLabel
954  * @chainable
955  * @return {OO.ui.Tool} The tool, for chaining
956  */
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 );
960         return this;
964  * Get the tool #title.
966  * @return {string} Title text
967  */
968 OO.ui.Tool.prototype.getTitle = function () {
969         return this.title;
973  * Get the tool's symbolic name.
975  * @return {string} Symbolic name of tool
976  */
977 OO.ui.Tool.prototype.getName = function () {
978         return this.constructor.static.name;
982  * Handle resize events from the toolbar
983  */
984 OO.ui.Tool.prototype.onToolbarResize = function () {
985         if ( !this.narrowConfig ) {
986                 return;
987         }
988         if ( this.toolbar.isNarrow() ) {
989                 if ( this.narrowConfig.displayBothIconAndLabel !== undefined ) {
990                         this.wideDisplayBothIconAndLabel = this.displayBothIconAndLabel;
991                         this.setDisplayBothIconAndLabel( this.narrowConfig.displayBothIconAndLabel );
992                 }
993                 if ( this.narrowConfig.title !== undefined ) {
994                         this.wideTitle = this.title;
995                         this.setTitle( this.narrowConfig.title );
996                 }
997                 if ( this.narrowConfig.icon !== undefined ) {
998                         this.wideIcon = this.icon;
999                         this.setIcon( this.narrowConfig.icon );
1000                 }
1001         } else {
1002                 if ( this.wideDisplayBothIconAndLabel !== undefined ) {
1003                         this.setDisplayBothIconAndLabel( this.wideDisplayBothIconAndLabel );
1004                 }
1005                 if ( this.wideTitle !== undefined ) {
1006                         this.setTitle( this.wideTitle );
1007                 }
1008                 if ( this.wideIcon !== undefined ) {
1009                         this.setIcon( this.wideIcon );
1010                 }
1011         }
1015  * Update the title.
1016  */
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 ),
1021                 tooltipParts = [];
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 );
1028         }
1029         if ( accelTooltips && typeof accel === 'string' && accel.length ) {
1030                 tooltipParts.push( accel );
1031         }
1032         if ( tooltipParts.length ) {
1033                 this.$link.attr( 'title', tooltipParts.join( ' ' ) );
1034         } else {
1035                 this.$link.removeAttr( 'title' );
1036         }
1040  * @inheritdoc OO.ui.mixin.IconElement
1041  */
1042 OO.ui.Tool.prototype.setIcon = function ( icon ) {
1043         // Mixin method
1044         OO.ui.mixin.IconElement.prototype.setIcon.call( this, icon );
1046         this.$element.toggleClass( 'oo-ui-tool-with-icon', !!this.icon );
1048         return this;
1052  * Destroy tool.
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.
1056  */
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
1079  * @abstract
1080  * @class
1081  * @extends OO.ui.Widget
1082  * @mixes OO.ui.mixin.GroupElement
1084  * @constructor
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,
1090  *  see above.
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'.
1095  */
1096 OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
1097         // Allow passing positional parameters inside the config object
1098         if ( OO.isPlainObject( toolbar ) && config === undefined ) {
1099                 config = toolbar;
1100                 toolbar = config.toolbar;
1101         }
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 );
1112         // Properties
1113         this.toolbar = toolbar;
1114         this.tools = {};
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 );
1124         // Events
1125         this.$group.on( {
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 )
1134         } );
1135         this.toolbar.getToolFactory().connect( this, {
1136                 register: 'onToolFactoryRegister'
1137         } );
1138         this.aggregate( {
1139                 disable: 'itemDisable'
1140         } );
1141         this.connect( this, {
1142                 itemDisable: 'updateDisabled',
1143                 disable: 'onDisable'
1144         } );
1146         // Initialization
1147         this.$group.addClass( 'oo-ui-toolGroup-tools' );
1148         this.$element
1149                 .addClass( 'oo-ui-toolGroup' )
1150                 .append( this.$group );
1151         this.onDisable( this.isDisabled() );
1152         this.populate();
1155 /* Setup */
1157 OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget );
1158 OO.mixinClass( OO.ui.ToolGroup, OO.ui.mixin.GroupElement );
1160 /* Events */
1163  * @event OO.ui.ToolGroup#update
1164  */
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
1171  */
1173 /* Static Properties */
1176  * Show labels in tooltips.
1178  * @static
1179  * @property {boolean}
1180  */
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
1190  * key combination).
1192  * @static
1193  * @property {boolean}
1194  */
1195 OO.ui.ToolGroup.static.accelTooltips = false;
1198  * Automatically disable the toolgroup when all tools are disabled
1200  * @static
1201  * @property {boolean}
1202  */
1203 OO.ui.ToolGroup.static.autoDisable = true;
1206  * @abstract
1207  * @static
1208  * @property {string}
1209  */
1210 OO.ui.ToolGroup.static.name = null;
1212 /* Methods */
1215  * @inheritdoc
1216  */
1217 OO.ui.ToolGroup.prototype.isDisabled = function () {
1218         return this.autoDisabled ||
1219                 OO.ui.ToolGroup.super.prototype.isDisabled.apply( this, arguments );
1223  * @inheritdoc
1224  */
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;
1233                                 break;
1234                         }
1235                 }
1236                 this.autoDisabled = allDisabled;
1237         }
1238         OO.ui.ToolGroup.super.prototype.updateDisabled.apply( this, arguments );
1242  * Handle disable events.
1244  * @protected
1245  * @param {boolean} isDisabled
1246  */
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.
1255  * @protected
1256  * @param {jQuery.Event} e Mouse down or key down event
1257  * @return {undefined|boolean} False to prevent default if event is handled
1258  */
1259 OO.ui.ToolGroup.prototype.onMouseKeyDown = function ( e ) {
1260         if (
1261                 !this.isDisabled() && (
1262                         e.which === OO.ui.MouseButtons.LEFT ||
1263                         e.which === OO.ui.Keys.SPACE ||
1264                         e.which === OO.ui.Keys.ENTER
1265                 )
1266         ) {
1267                 this.pressed = this.findTargetTool( e );
1268                 if ( this.pressed ) {
1269                         this.pressed.setActive( true );
1270                         this.getElementDocument().addEventListener(
1271                                 'mouseup',
1272                                 this.onDocumentMouseKeyUpHandler,
1273                                 true
1274                         );
1275                         this.getElementDocument().addEventListener(
1276                                 'keyup',
1277                                 this.onDocumentMouseKeyUpHandler,
1278                                 true
1279                         );
1280                         return false;
1281                 }
1282         }
1286  * Handle document mouse up and key up events.
1288  * @protected
1289  * @param {MouseEvent|KeyboardEvent} e Mouse up or key up event
1290  */
1291 OO.ui.ToolGroup.prototype.onDocumentMouseKeyUp = function ( e ) {
1292         this.getElementDocument().removeEventListener(
1293                 'mouseup',
1294                 this.onDocumentMouseKeyUpHandler,
1295                 true
1296         );
1297         this.getElementDocument().removeEventListener(
1298                 'keyup',
1299                 this.onDocumentMouseKeyUpHandler,
1300                 true
1301         );
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.
1310  * @protected
1311  * @param {MouseEvent|KeyboardEvent} e Mouse up or key up event
1312  */
1313 OO.ui.ToolGroup.prototype.onMouseKeyUp = function ( e ) {
1314         const tool = this.findTargetTool( e );
1316         if (
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
1321                 )
1322         ) {
1323                 this.pressed.onSelect();
1324                 this.pressed = null;
1325                 e.preventDefault();
1326                 e.stopPropagation();
1327         }
1329         this.pressed = null;
1333  * Handle mouse over and focus events.
1335  * @protected
1336  * @param {jQuery.Event} e Mouse over or focus event
1337  */
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 );
1343         }
1347  * Handle mouse out and blur events.
1349  * @protected
1350  * @param {jQuery.Event} e Mouse out or blur event
1351  */
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 );
1357         }
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.
1366  * @private
1367  * @param {jQuery.Event} e
1368  * @return {OO.ui.Tool|null} Tool, `null` if none was found
1369  */
1370 OO.ui.ToolGroup.prototype.findTargetTool = function ( e ) {
1371         const $item = $( e.target ).closest( '.oo-ui-tool-link' );
1373         let tool;
1374         if ( $item.length ) {
1375                 tool = $item.parent().data( 'oo-ui-tool' );
1376         }
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
1389  * @protected
1390  * @param {string} name Symbolic name of tool
1391  */
1392 OO.ui.ToolGroup.prototype.onToolFactoryRegister = function () {
1393         this.populate();
1397  * Get the toolbar that contains the toolgroup.
1399  * @return {OO.ui.Toolbar} Toolbar that contains the toolgroup
1400  */
1401 OO.ui.ToolGroup.prototype.getToolbar = function () {
1402         return this.toolbar;
1406  * Add and remove tools based on configuration.
1407  */
1408 OO.ui.ToolGroup.prototype.populate = function () {
1409         const toolFactory = this.toolbar.getToolFactory(),
1410                 names = {},
1411                 add = [],
1412                 remove = [],
1413                 list = this.toolbar.getToolFactory().getTools(
1414                         this.include, this.exclude, this.promote, this.demote
1415                 );
1417         let name;
1418         // Build a list of needed tools
1419         for ( let i = 0, len = list.length; i < len; i++ ) {
1420                 name = list[ i ];
1421                 if (
1422                         // Tool exists
1423                         toolFactory.lookup( name ) &&
1424                         // Tool is available or is already in this group
1425                         ( this.toolbar.isToolAvailable( name ) || this.tools[ name ] )
1426                 ) {
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
1429                         // the tool.
1430                         this.toolbar.tools[ name ] = true;
1431                         let tool = this.tools[ name ];
1432                         if ( !tool ) {
1433                                 // Auto-initialize tools on first use
1434                                 this.tools[ name ] = tool = toolFactory.create( name, this );
1435                                 tool.updateTitle();
1436                         }
1437                         this.toolbar.reserveTool( tool );
1438                         add.push( tool );
1439                         names[ name ] = true;
1440                 }
1441         }
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 ];
1449                 }
1450         }
1451         if ( remove.length ) {
1452                 this.removeItems( remove );
1453         }
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.
1464  */
1465 OO.ui.ToolGroup.prototype.destroy = function () {
1466         this.clearItems();
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 ];
1472         }
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
1487  * @class
1488  * @extends OO.Factory
1489  * @constructor
1490  */
1491 OO.ui.ToolFactory = function OoUiToolFactory() {
1492         // Parent constructor
1493         OO.ui.ToolFactory.super.call( this );
1496 /* Setup */
1498 OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
1500 /* Methods */
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
1510  */
1511 OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
1512         const auto = [],
1513                 used = {};
1515         // Collect included and not excluded tools
1516         const included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
1518         // Promotion
1519         const promoted = this.extract( promote, used );
1520         const demoted = this.extract( demote, used );
1522         // Auto
1523         for ( let i = 0, len = included.length; i < len; i++ ) {
1524                 if ( !used[ included[ i ] ] ) {
1525                         auto.push( included[ i ] );
1526                 }
1527         }
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
1536  * following ways:
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.
1549  * @private
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
1553  */
1554 OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
1555         const names = [];
1557         collection = !Array.isArray( collection ) ? [ collection ] : collection;
1559         for ( let i = 0, len = collection.length; i < len; i++ ) {
1560                 let item = collection[ i ],
1561                         name, tool;
1562                 if ( item === '*' ) {
1563                         for ( name in this.registry ) {
1564                                 tool = this.registry[ name ];
1565                                 if (
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 ] )
1570                                 ) {
1571                                         names.push( name );
1572                                         if ( used ) {
1573                                                 used[ name ] = true;
1574                                         }
1575                                 }
1576                         }
1577                 } else {
1578                         // Allow plain strings as shorthand for named tools
1579                         if ( typeof item === 'string' ) {
1580                                 item = { name: item };
1581                         }
1582                         if ( OO.isPlainObject( item ) ) {
1583                                 if ( item.group ) {
1584                                         for ( name in this.registry ) {
1585                                                 tool = this.registry[ name ];
1586                                                 if (
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 ] )
1593                                                 ) {
1594                                                         names.push( name );
1595                                                         if ( used ) {
1596                                                                 used[ name ] = true;
1597                                                         }
1598                                                 }
1599                                         }
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 );
1603                                         if ( used ) {
1604                                                 used[ item.name ] = true;
1605                                         }
1606                                 }
1607                         }
1608                 }
1609         }
1610         return names;
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
1629  * @class
1630  * @extends OO.Factory
1631  * @constructor
1632  */
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 ] );
1642         }
1645 /* Setup */
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
1655  */
1656 OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
1657         return [
1658                 OO.ui.BarToolGroup,
1659                 OO.ui.ListToolGroup,
1660                 OO.ui.MenuToolGroup
1661         ];
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: {
1674  *            padded: true,
1675  *            label: 'Help',
1676  *            head: true
1677  *        } }, config ) );
1678  *        this.popup.$body.append( '<p>I am helpful!</p>' );
1679  *     };
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
1692  * @abstract
1693  * @class
1694  * @extends OO.ui.Tool
1695  * @mixes OO.ui.mixin.PopupElement
1697  * @constructor
1698  * @param {OO.ui.ToolGroup} toolGroup
1699  * @param {Object} [config] Configuration options
1700  */
1701 OO.ui.PopupTool = function OoUiPopupTool( toolGroup, config ) {
1702         // Allow passing positional parameters inside the config object
1703         if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
1704                 config = toolGroup;
1705                 toolGroup = config.toolGroup;
1706         }
1708         // Parent constructor
1709         OO.ui.PopupTool.super.call( this, toolGroup, config );
1711         // Mixin constructors
1712         OO.ui.mixin.PopupElement.call( this, config );
1714         // Events
1715         this.popup.connect( this, {
1716                 toggle: 'onPopupToggle'
1717         } );
1719         // Initialization
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 );
1727 /* Setup */
1729 OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool );
1730 OO.mixinClass( OO.ui.PopupTool, OO.ui.mixin.PopupElement );
1732 /* Methods */
1735  * Handle the tool being selected.
1737  * @inheritdoc
1738  */
1739 OO.ui.PopupTool.prototype.onSelect = function () {
1740         if ( !this.isDisabled() ) {
1741                 this.popup.toggle();
1742         }
1743         return false;
1747  * Handle the toolbar state being updated.
1749  * @inheritdoc
1750  */
1751 OO.ui.PopupTool.prototype.onUpdateState = function () {
1755  * Handle popup visibility being toggled.
1757  * @param {boolean} isVisible
1758  */
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 );
1776  *     };
1777  *     OO.inheritClass( SettingsTool, OO.ui.ToolGroupTool );
1778  *     SettingsTool.static.name = 'settings';
1779  *     SettingsTool.static.title = 'Change settings';
1780  *     SettingsTool.static.groupConfig = {
1781  *         icon: 'settings',
1782  *         label: 'ToolGroupTool',
1783  *         include: [  'setting1', 'setting2'  ]
1784  *     };
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
1794  * @abstract
1795  * @class
1796  * @extends OO.ui.Tool
1798  * @constructor
1799  * @param {OO.ui.ToolGroup} toolGroup
1800  * @param {Object} [config] Configuration options
1801  */
1802 OO.ui.ToolGroupTool = function OoUiToolGroupTool( toolGroup, config ) {
1803         // Allow passing positional parameters inside the config object
1804         if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
1805                 config = toolGroup;
1806                 toolGroup = config.toolGroup;
1807         }
1809         // Parent constructor
1810         OO.ui.ToolGroupTool.super.call( this, toolGroup, config );
1812         // Properties
1813         this.innerToolGroup = this.createGroup( this.constructor.static.groupConfig );
1815         // Events
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' )
1820         } );
1822         // Initialization
1823         this.$link.remove();
1824         this.$element
1825                 .addClass( 'oo-ui-toolGroupTool' )
1826                 .append( this.innerToolGroup.$element );
1829 /* Setup */
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>}
1843  */
1844 OO.ui.ToolGroupTool.static.groupConfig = {};
1846 /* Methods */
1849  * Handle the tool being selected.
1851  * @inheritdoc
1852  */
1853 OO.ui.ToolGroupTool.prototype.onSelect = function () {
1854         this.innerToolGroup.setActive( !this.innerToolGroup.active );
1855         return false;
1859  * Synchronize disabledness state of the tool with the inner toolgroup.
1861  * @private
1862  * @param {boolean} disabled Element is disabled
1863  */
1864 OO.ui.ToolGroupTool.prototype.onToolGroupDisable = function ( disabled ) {
1865         this.setDisabled( disabled );
1869  * Handle the toolbar state being updated.
1871  * @inheritdoc
1872  */
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}
1883  */
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' );
1889                 }
1890         }
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
1903  * is set up.
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
1912  *     @example
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 );
1926  *     }
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 );
1938  *     };
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: {
1947  *             padded: true,
1948  *             label: 'Help',
1949  *             head: true
1950  *         } }, config ) );
1951  *         this.popup.$body.append( '<p>I am helpful!</p>' );
1952  *     }
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).
1961  *     toolbar.setup( [
1962  *         {
1963  *             // 'bar' tool groups display tools by icon only
1964  *             type: 'bar',
1965  *             include: [ 'search', 'help' ]
1966  *         }
1967  *     ] );
1969  *     // Create some UI around the toolbar and place it in the document
1970  *     const frame = new OO.ui.PanelLayout( {
1971  *         expanded: false,
1972  *         framed: true
1973  *     } );
1974  *     const contentFrame = new OO.ui.PanelLayout( {
1975  *         expanded: false,
1976  *         padded: true
1977  *     } );
1978  *     frame.$element.append(
1979  *         toolbar.$element,
1980  *         contentFrame.$element.append( $area )
1981  *     );
1982  *     $( document.body ).append( frame.$element );
1984  *     // Here is where the toolbar is actually built. This must be done after inserting it into the
1985  *     // document.
1986  *     toolbar.initialize();
1988  * @class
1989  * @extends OO.ui.ToolGroup
1991  * @constructor
1992  * @param {OO.ui.Toolbar} toolbar
1993  * @param {Object} [config] Configuration options
1994  */
1995 OO.ui.BarToolGroup = function OoUiBarToolGroup( toolbar, config ) {
1996         // Allow passing positional parameters inside the config object
1997         if ( OO.isPlainObject( toolbar ) && config === undefined ) {
1998                 config = toolbar;
1999                 toolbar = config.toolbar;
2000         }
2002         // Parent constructor
2003         OO.ui.BarToolGroup.super.call( this, toolbar, config );
2005         // Initialization
2006         this.$element.addClass( 'oo-ui-barToolGroup' );
2007         this.$group.addClass( 'oo-ui-barToolGroup-tools' );
2010 /* Setup */
2012 OO.inheritClass( OO.ui.BarToolGroup, OO.ui.ToolGroup );
2014 /* Static Properties */
2017  * @static
2018  * @inheritdoc
2019  */
2020 OO.ui.BarToolGroup.static.titleTooltips = true;
2023  * @static
2024  * @inheritdoc
2025  */
2026 OO.ui.BarToolGroup.static.accelTooltips = true;
2029  * @static
2030  * @inheritdoc
2031  */
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.
2040  * @abstract
2041  * @class
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
2052  * @constructor
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
2057  */
2058 OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
2059         // Allow passing positional parameters inside the config object
2060         if ( OO.isPlainObject( toolbar ) && config === undefined ) {
2061                 config = toolbar;
2062                 toolbar = config.toolbar;
2063         }
2065         // Configuration initialization
2066         config = Object.assign( {
2067                 indicator: config.indicator === undefined ?
2068                         ( toolbar.position === 'bottom' ? 'up' : 'down' ) : config.indicator
2069         }, config );
2071         // Parent constructor
2072         OO.ui.PopupToolGroup.super.call( this, toolbar, config );
2074         // Properties
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
2090         }, config ) );
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
2097         }, config ) );
2098         OO.ui.mixin.TabIndexedElement.call( this, Object.assign( {
2099                 $tabIndexed: this.$handle
2100         }, config ) );
2102         // Events
2103         this.$handle.on( {
2104                 keydown: this.onHandleMouseKeyDown.bind( this ),
2105                 keyup: this.onHandleMouseKeyUp.bind( this ),
2106                 mousedown: this.onHandleMouseKeyDown.bind( this ),
2107                 mouseup: this.onHandleMouseKeyUp.bind( this )
2108         } );
2109         this.toolbar.connect( this, {
2110                 resize: 'onToolbarResize'
2111         } );
2113         // Initialization
2114         this.$handle
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 ) {
2122                 this.$group
2123                         .prepend( $( '<span>' )
2124                                 .addClass( 'oo-ui-popupToolGroup-header' )
2125                                 .text( config.header )
2126                         );
2127         }
2128         this.$element
2129                 .addClass( 'oo-ui-popupToolGroup' )
2130                 .prepend( this.$handle );
2131         this.$group.addClass( 'oo-ui-popupToolGroup-tools' );
2132         this.toolbar.$popups.append( this.$group );
2135 /* Setup */
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.
2154  * @static
2155  * @property {Object|null}
2156  */
2157 OO.ui.PopupToolGroup.static.narrowConfig = null;
2159 /* Methods */
2162  * @inheritdoc
2163  */
2164 OO.ui.PopupToolGroup.prototype.setDisabled = function () {
2165         // Parent method
2166         OO.ui.PopupToolGroup.super.prototype.setDisabled.apply( this, arguments );
2168         if ( this.isDisabled() && this.isElementAttached() ) {
2169                 this.setActive( false );
2170         }
2174  * Handle resize events from the toolbar
2175  */
2176 OO.ui.PopupToolGroup.prototype.onToolbarResize = function () {
2177         if ( !this.narrowConfig ) {
2178                 return;
2179         }
2180         if ( this.toolbar.isNarrow() ) {
2181                 if ( this.narrowConfig.invisibleLabel !== undefined ) {
2182                         this.wideInvisibleLabel = this.invisibleLabel;
2183                         this.setInvisibleLabel( this.narrowConfig.invisibleLabel );
2184                 }
2185                 if ( this.narrowConfig.label !== undefined ) {
2186                         this.wideLabel = this.label;
2187                         this.setLabel( this.narrowConfig.label );
2188                 }
2189                 if ( this.narrowConfig.icon !== undefined ) {
2190                         this.wideIcon = this.icon;
2191                         this.setIcon( this.narrowConfig.icon );
2192                 }
2193         } else {
2194                 if ( this.wideInvisibleLabel !== undefined ) {
2195                         this.setInvisibleLabel( this.wideInvisibleLabel );
2196                 }
2197                 if ( this.wideLabel !== undefined ) {
2198                         this.setLabel( this.wideLabel );
2199                 }
2200                 if ( this.wideIcon !== undefined ) {
2201                         this.setIcon( this.wideIcon );
2202                 }
2203         }
2207  * Handle document mouse up and key up events.
2209  * @protected
2210  * @param {MouseEvent|KeyboardEvent} e Mouse up or key up event
2211  */
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 ] ) {
2216                 return;
2217         }
2218         if ( $target.closest( '.oo-ui-popupToolGroup-tools' )[ 0 ] === this.$group[ 0 ] ) {
2219                 return;
2220         }
2221         this.setActive( false );
2225  * @inheritdoc
2226  */
2227 OO.ui.PopupToolGroup.prototype.onMouseKeyUp = function ( e ) {
2228         // Only close toolgroup when a tool was actually selected
2229         if (
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
2234                 )
2235         ) {
2236                 this.setActive( false );
2237         }
2238         return OO.ui.PopupToolGroup.super.prototype.onMouseKeyUp.call( this, e );
2242  * @inheritdoc
2243  */
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' );
2254                         return false;
2255                 }
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 );
2265                 }
2266         }
2267         return OO.ui.PopupToolGroup.super.prototype.onMouseKeyDown.call( this, e );
2271  * Handle mouse up and key up events.
2273  * @protected
2274  * @param {jQuery.Event} e Mouse up or key up event
2275  * @return {undefined|boolean} False to prevent default if event is handled
2276  */
2277 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyUp = function ( e ) {
2278         if (
2279                 !this.isDisabled() && (
2280                         e.which === OO.ui.MouseButtons.LEFT ||
2281                         e.which === OO.ui.Keys.SPACE ||
2282                         e.which === OO.ui.Keys.ENTER
2283                 )
2284         ) {
2285                 return false;
2286         }
2290  * Handle mouse down and key down events.
2292  * @protected
2293  * @param {jQuery.Event} e Mouse down or key down event
2294  * @return {undefined|boolean} False to prevent default if event is handled
2295  */
2296 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyDown = function ( e ) {
2297         let $focusable;
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' );
2304                                 return false;
2305                         }
2306                 }
2307                 if (
2308                         e.which === OO.ui.MouseButtons.LEFT ||
2309                         e.which === OO.ui.Keys.SPACE ||
2310                         e.which === OO.ui.Keys.ENTER
2311                 ) {
2312                         this.setActive( !this.active );
2313                         return false;
2314                 }
2315         }
2319  * Check if the tool group is active.
2321  * @return {boolean} Tool group is active
2322  */
2323 OO.ui.PopupToolGroup.prototype.isActive = function () {
2324         return this.active;
2328  * Switch into 'active' mode.
2330  * When active, the popup is visible. A mouseup event anywhere in the document will trigger
2331  * deactivation.
2333  * @param {boolean} [value=false] The active state to set
2334  * @fires OO.ui.PopupToolGroup#active
2335  */
2336 OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
2337         let containerWidth, containerLeft;
2338         value = !!value;
2339         if ( this.active !== value ) {
2340                 this.active = value;
2341                 if ( value ) {
2342                         this.getElementDocument().addEventListener(
2343                                 'mouseup',
2344                                 this.onPopupDocumentMouseKeyUpHandler,
2345                                 true
2346                         );
2347                         this.getElementDocument().addEventListener(
2348                                 'keyup',
2349                                 this.onPopupDocumentMouseKeyUpHandler,
2350                                 true
2351                         );
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 );
2372                         }
2373                         if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
2374                                 // Anchoring to the right also caused the popup to clip, so just make it fill the
2375                                 // container.
2376                                 containerWidth = this.$clippableScrollableContainer.width();
2377                                 containerLeft = this.$clippableScrollableContainer[ 0 ] ===
2378                                         document.documentElement ?
2379                                         0 :
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
2388                                 } );
2389                         }
2390                 } else {
2391                         this.getElementDocument().removeEventListener(
2392                                 'mouseup',
2393                                 this.onPopupDocumentMouseKeyUpHandler,
2394                                 true
2395                         );
2396                         this.getElementDocument().removeEventListener(
2397                                 'keyup',
2398                                 this.onPopupDocumentMouseKeyUpHandler,
2399                                 true
2400                         );
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 );
2406                 }
2407                 this.emit( 'active', this.active );
2408                 this.updateThemeClasses();
2409         }
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
2434  *     @example
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 );
2443  *     }
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 );
2450  *     };
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 );
2456  *     }
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 );
2463  *     };
2464  *     StuffTool.prototype.onUpdateState = function () {};
2465  *     toolFactory.register( StuffTool );
2466  *     toolbar.setup( [
2467  *         {
2468  *             // Configurations for list toolgroup.
2469  *             type: 'list',
2470  *             label: 'ListToolGroup',
2471  *             icon: 'ellipsis',
2472  *             title: 'This is the title, displayed when user moves the mouse over the list ' +
2473  *                 'toolgroup',
2474  *             header: 'This is the header',
2475  *             include: [ 'settings', 'stuff' ],
2476  *             allowCollapse: ['stuff']
2477  *         }
2478  *     ] );
2480  *     // Create some UI around the toolbar and place it in the document
2481  *     const frame = new OO.ui.PanelLayout( {
2482  *         expanded: false,
2483  *         framed: true
2484  *     } );
2485  *     frame.$element.append(
2486  *         toolbar.$element
2487  *     );
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();
2492  * @class
2493  * @extends OO.ui.PopupToolGroup
2495  * @constructor
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
2502  *  be displayed.
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
2510  *  the bottom.
2511  */
2512 OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
2513         // Allow passing positional parameters inside the config object
2514         if ( OO.isPlainObject( toolbar ) && config === undefined ) {
2515                 config = toolbar;
2516                 toolbar = config.toolbar;
2517         }
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 );
2531         // Initialization
2532         this.$element.addClass( 'oo-ui-listToolGroup' );
2533         this.$group.addClass( 'oo-ui-listToolGroup-tools' );
2536 /* Setup */
2538 OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup );
2540 /* Static Properties */
2543  * @static
2544  * @inheritdoc
2545  */
2546 OO.ui.ListToolGroup.static.name = 'list';
2548 /* Methods */
2551  * @inheritdoc
2552  */
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 );
2562         }
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 ] ] );
2568                 }
2569         }
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
2582  */
2583 OO.ui.ListToolGroup.prototype.getExpandCollapseTool = function () {
2584         if ( this.expandCollapseTool === undefined ) {
2585                 const ExpandCollapseTool = function () {
2586                         ExpandCollapseTool.super.apply( this, arguments );
2587                 };
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 );
2595                 };
2596                 ExpandCollapseTool.prototype.onUpdateState = function () {
2597                         // Do nothing. Tool interface requires an implementation of this function.
2598                 };
2600                 ExpandCollapseTool.static.name = 'more-fewer';
2602                 this.expandCollapseTool = new ExpandCollapseTool( this );
2603         }
2604         return this.expandCollapseTool;
2608  * @inheritdoc
2609  */
2610 OO.ui.ListToolGroup.prototype.onMouseKeyUp = function ( e ) {
2611         // Do not close the popup when the user wants to show more/fewer tools
2612         if (
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
2617                 )
2618         ) {
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
2621                 // directly.
2622                 return OO.ui.ListToolGroup.super.super.prototype.onMouseKeyUp.call( this, e );
2623         } else {
2624                 return OO.ui.ListToolGroup.super.prototype.onMouseKeyUp.call( this, e );
2625         }
2628 OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () {
2629         const inverted = this.toolbar.position === 'bottom',
2630                 icon = this.expanded === inverted ? 'expand' : 'collapse';
2632         this.getExpandCollapseTool()
2633                 .setIcon( icon )
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 );
2638         }
2640         // Re-evaluate clipping, because our height has changed
2641         this.clip();
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
2663  *     @example
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;
2678  *     }
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' );
2690  *     };
2691  *     SettingsTool.prototype.onUpdateState = function () {};
2692  *     toolFactory.register( SettingsTool );
2694  *     function StuffTool() {
2695  *         StuffTool.super.apply( this, arguments );
2696  *         this.reallyActive = false;
2697  *     }
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' );
2709  *     };
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).
2715  *     toolbar.setup( [
2716  *         {
2717  *             type: 'menu',
2718  *             header: 'This is the (optional) header',
2719  *             title: 'This is the (optional) title',
2720  *             include: [ 'settings', 'stuff' ]
2721  *         }
2722  *     ] );
2724  *     // Create some UI around the toolbar and place it in the document
2725  *     const frame = new OO.ui.PanelLayout( {
2726  *         expanded: false,
2727  *         framed: true
2728  *     } );
2729  *     const contentFrame = new OO.ui.PanelLayout( {
2730  *         expanded: false,
2731  *         padded: true
2732  *     } );
2733  *     frame.$element.append(
2734  *         toolbar.$element,
2735  *         contentFrame.$element.append( $area )
2736  *     );
2737  *     $( document.body ).append( frame.$element );
2739  *     // Here is where the toolbar is actually built. This must be done after inserting it into the
2740  *     // document.
2741  *     toolbar.initialize();
2742  *     toolbar.emit( 'updateState' );
2744  * @class
2745  * @extends OO.ui.PopupToolGroup
2747  * @constructor
2748  * @param {OO.ui.Toolbar} toolbar
2749  * @param {Object} [config] Configuration options
2750  */
2751 OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
2752         // Allow passing positional parameters inside the config object
2753         if ( OO.isPlainObject( toolbar ) && config === undefined ) {
2754                 config = toolbar;
2755                 toolbar = config.toolbar;
2756         }
2758         // Configuration initialization
2759         config = config || {};
2761         // Parent constructor
2762         OO.ui.MenuToolGroup.super.call( this, toolbar, config );
2764         // Events
2765         this.toolbar.connect( this, {
2766                 updateState: 'onUpdateState'
2767         } );
2769         // Initialization
2770         this.$element.addClass( 'oo-ui-menuToolGroup' );
2771         this.$group.addClass( 'oo-ui-menuToolGroup-tools' );
2774 /* Setup */
2776 OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup );
2778 /* Static Properties */
2781  * @static
2782  * @inheritdoc
2783  */
2784 OO.ui.MenuToolGroup.static.name = 'menu';
2786 /* Methods */
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.
2794  * @private
2795  */
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() );
2802                 }
2803         }
2805         this.setLabel( labelTexts.join( ', ' ) || ' ' );
2808 }( OO ) );
2810 //# sourceMappingURL=oojs-ui-toolbars.js.map.json