Merge "Special:Upload should not crash on failing previews"
[mediawiki.git] / resources / lib / oojs-ui / oojs-ui-toolbars.js
blob0b8a7f653017628485297ae05d603b130d180311
1 /*!
2  * OOjs UI v0.18.4-fix (d4045dee45)
3  * https://www.mediawiki.org/wiki/OOjs_UI
4  *
5  * Copyright 2011–2017 OOjs UI Team and other contributors.
6  * Released under the MIT license
7  * http://oojs.mit-license.org
8  *
9  * Date: 2017-01-19T20:22:26Z
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 commands that are
18  * part of the toolbar, but not configured as tools.
19  *
20  * Individual tools are customized and then registered with a {@link OO.ui.ToolFactory tool factory}, which creates
21  * the tools on demand. Each tool has a symbolic name (used when registering the tool), a title (e.g., ‘Insert
22  * image’), and an icon.
23  *
24  * Individual tools are organized in {@link OO.ui.ToolGroup toolgroups}, which can be {@link OO.ui.MenuToolGroup menus}
25  * of tools, {@link OO.ui.ListToolGroup lists} of tools, or a single {@link OO.ui.BarToolGroup bar} of tools.
26  * The arrangement and order of the toolgroups is customized when the toolbar is set up. Tools can be presented in
27  * any order, but each can only appear once in the toolbar.
28  *
29  * The toolbar can be synchronized with the state of the external "application", like a text
30  * editor's editing area, marking tools as active/inactive (e.g. a 'bold' tool would be shown as
31  * active when the text cursor was inside bolded text) or enabled/disabled (e.g. a table caption
32  * tool would be disabled while the user is not editing a table). A state change is signalled by
33  * emitting the {@link #event-updateState 'updateState' event}, which calls Tools'
34  * {@link OO.ui.Tool#onUpdateState onUpdateState method}.
35  *
36  * The following is an example of a basic toolbar.
37  *
38  *     @example
39  *     // Example of a toolbar
40  *     // Create the toolbar
41  *     var toolFactory = new OO.ui.ToolFactory();
42  *     var toolGroupFactory = new OO.ui.ToolGroupFactory();
43  *     var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
44  *
45  *     // We will be placing status text in this element when tools are used
46  *     var $area = $( '<p>' ).text( 'Toolbar example' );
47  *
48  *     // Define the tools that we're going to place in our toolbar
49  *
50  *     // Create a class inheriting from OO.ui.Tool
51  *     function SearchTool() {
52  *         SearchTool.parent.apply( this, arguments );
53  *     }
54  *     OO.inheritClass( SearchTool, OO.ui.Tool );
55  *     // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
56  *     // of 'icon' and 'title' (displayed icon and text).
57  *     SearchTool.static.name = 'search';
58  *     SearchTool.static.icon = 'search';
59  *     SearchTool.static.title = 'Search...';
60  *     // Defines the action that will happen when this tool is selected (clicked).
61  *     SearchTool.prototype.onSelect = function () {
62  *         $area.text( 'Search tool clicked!' );
63  *         // Never display this tool as "active" (selected).
64  *         this.setActive( false );
65  *     };
66  *     SearchTool.prototype.onUpdateState = function () {};
67  *     // Make this tool available in our toolFactory and thus our toolbar
68  *     toolFactory.register( SearchTool );
69  *
70  *     // Register two more tools, nothing interesting here
71  *     function SettingsTool() {
72  *         SettingsTool.parent.apply( this, arguments );
73  *     }
74  *     OO.inheritClass( SettingsTool, OO.ui.Tool );
75  *     SettingsTool.static.name = 'settings';
76  *     SettingsTool.static.icon = 'settings';
77  *     SettingsTool.static.title = 'Change settings';
78  *     SettingsTool.prototype.onSelect = function () {
79  *         $area.text( 'Settings tool clicked!' );
80  *         this.setActive( false );
81  *     };
82  *     SettingsTool.prototype.onUpdateState = function () {};
83  *     toolFactory.register( SettingsTool );
84  *
85  *     // Register two more tools, nothing interesting here
86  *     function StuffTool() {
87  *         StuffTool.parent.apply( this, arguments );
88  *     }
89  *     OO.inheritClass( StuffTool, OO.ui.Tool );
90  *     StuffTool.static.name = 'stuff';
91  *     StuffTool.static.icon = 'ellipsis';
92  *     StuffTool.static.title = 'More stuff';
93  *     StuffTool.prototype.onSelect = function () {
94  *         $area.text( 'More stuff tool clicked!' );
95  *         this.setActive( false );
96  *     };
97  *     StuffTool.prototype.onUpdateState = function () {};
98  *     toolFactory.register( StuffTool );
99  *
100  *     // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
101  *     // little popup window (a PopupWidget).
102  *     function HelpTool( toolGroup, config ) {
103  *         OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
104  *             padded: true,
105  *             label: 'Help',
106  *             head: true
107  *         } }, config ) );
108  *         this.popup.$body.append( '<p>I am helpful!</p>' );
109  *     }
110  *     OO.inheritClass( HelpTool, OO.ui.PopupTool );
111  *     HelpTool.static.name = 'help';
112  *     HelpTool.static.icon = 'help';
113  *     HelpTool.static.title = 'Help';
114  *     toolFactory.register( HelpTool );
116  *     // Finally define which tools and in what order appear in the toolbar. Each tool may only be
117  *     // used once (but not all defined tools must be used).
118  *     toolbar.setup( [
119  *         {
120  *             // 'bar' tool groups display tools' icons only, side-by-side.
121  *             type: 'bar',
122  *             include: [ 'search', 'help' ]
123  *         },
124  *         {
125  *             // 'list' tool groups display both the titles and icons, in a dropdown list.
126  *             type: 'list',
127  *             indicator: 'down',
128  *             label: 'More',
129  *             include: [ 'settings', 'stuff' ]
130  *         }
131  *         // Note how the tools themselves are toolgroup-agnostic - the same tool can be displayed
132  *         // either in a 'list' or a 'bar'. There is a 'menu' tool group too, not showcased here,
133  *         // since it's more complicated to use. (See the next example snippet on this page.)
134  *     ] );
136  *     // Create some UI around the toolbar and place it in the document
137  *     var frame = new OO.ui.PanelLayout( {
138  *         expanded: false,
139  *         framed: true
140  *     } );
141  *     var contentFrame = new OO.ui.PanelLayout( {
142  *         expanded: false,
143  *         padded: true
144  *     } );
145  *     frame.$element.append(
146  *         toolbar.$element,
147  *         contentFrame.$element.append( $area )
148  *     );
149  *     $( 'body' ).append( frame.$element );
151  *     // Here is where the toolbar is actually built. This must be done after inserting it into the
152  *     // document.
153  *     toolbar.initialize();
154  *     toolbar.emit( 'updateState' );
156  * The following example extends the previous one to illustrate 'menu' toolgroups and the usage of
157  * {@link #event-updateState 'updateState' event}.
159  *     @example
160  *     // Create the toolbar
161  *     var toolFactory = new OO.ui.ToolFactory();
162  *     var toolGroupFactory = new OO.ui.ToolGroupFactory();
163  *     var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
165  *     // We will be placing status text in this element when tools are used
166  *     var $area = $( '<p>' ).text( 'Toolbar example' );
168  *     // Define the tools that we're going to place in our toolbar
170  *     // Create a class inheriting from OO.ui.Tool
171  *     function SearchTool() {
172  *         SearchTool.parent.apply( this, arguments );
173  *     }
174  *     OO.inheritClass( SearchTool, OO.ui.Tool );
175  *     // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
176  *     // of 'icon' and 'title' (displayed icon and text).
177  *     SearchTool.static.name = 'search';
178  *     SearchTool.static.icon = 'search';
179  *     SearchTool.static.title = 'Search...';
180  *     // Defines the action that will happen when this tool is selected (clicked).
181  *     SearchTool.prototype.onSelect = function () {
182  *         $area.text( 'Search tool clicked!' );
183  *         // Never display this tool as "active" (selected).
184  *         this.setActive( false );
185  *     };
186  *     SearchTool.prototype.onUpdateState = function () {};
187  *     // Make this tool available in our toolFactory and thus our toolbar
188  *     toolFactory.register( SearchTool );
190  *     // Register two more tools, nothing interesting here
191  *     function SettingsTool() {
192  *         SettingsTool.parent.apply( this, arguments );
193  *         this.reallyActive = false;
194  *     }
195  *     OO.inheritClass( SettingsTool, OO.ui.Tool );
196  *     SettingsTool.static.name = 'settings';
197  *     SettingsTool.static.icon = 'settings';
198  *     SettingsTool.static.title = 'Change settings';
199  *     SettingsTool.prototype.onSelect = function () {
200  *         $area.text( 'Settings tool clicked!' );
201  *         // Toggle the active state on each click
202  *         this.reallyActive = !this.reallyActive;
203  *         this.setActive( this.reallyActive );
204  *         // To update the menu label
205  *         this.toolbar.emit( 'updateState' );
206  *     };
207  *     SettingsTool.prototype.onUpdateState = function () {};
208  *     toolFactory.register( SettingsTool );
210  *     // Register two more tools, nothing interesting here
211  *     function StuffTool() {
212  *         StuffTool.parent.apply( this, arguments );
213  *         this.reallyActive = false;
214  *     }
215  *     OO.inheritClass( StuffTool, OO.ui.Tool );
216  *     StuffTool.static.name = 'stuff';
217  *     StuffTool.static.icon = 'ellipsis';
218  *     StuffTool.static.title = 'More stuff';
219  *     StuffTool.prototype.onSelect = function () {
220  *         $area.text( 'More stuff tool clicked!' );
221  *         // Toggle the active state on each click
222  *         this.reallyActive = !this.reallyActive;
223  *         this.setActive( this.reallyActive );
224  *         // To update the menu label
225  *         this.toolbar.emit( 'updateState' );
226  *     };
227  *     StuffTool.prototype.onUpdateState = function () {};
228  *     toolFactory.register( StuffTool );
230  *     // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
231  *     // little popup window (a PopupWidget). 'onUpdateState' is also already implemented.
232  *     function HelpTool( toolGroup, config ) {
233  *         OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
234  *             padded: true,
235  *             label: 'Help',
236  *             head: true
237  *         } }, config ) );
238  *         this.popup.$body.append( '<p>I am helpful!</p>' );
239  *     }
240  *     OO.inheritClass( HelpTool, OO.ui.PopupTool );
241  *     HelpTool.static.name = 'help';
242  *     HelpTool.static.icon = 'help';
243  *     HelpTool.static.title = 'Help';
244  *     toolFactory.register( HelpTool );
246  *     // Finally define which tools and in what order appear in the toolbar. Each tool may only be
247  *     // used once (but not all defined tools must be used).
248  *     toolbar.setup( [
249  *         {
250  *             // 'bar' tool groups display tools' icons only, side-by-side.
251  *             type: 'bar',
252  *             include: [ 'search', 'help' ]
253  *         },
254  *         {
255  *             // 'menu' tool groups display both the titles and icons, in a dropdown menu.
256  *             // Menu label indicates which items are selected.
257  *             type: 'menu',
258  *             indicator: 'down',
259  *             include: [ 'settings', 'stuff' ]
260  *         }
261  *     ] );
263  *     // Create some UI around the toolbar and place it in the document
264  *     var frame = new OO.ui.PanelLayout( {
265  *         expanded: false,
266  *         framed: true
267  *     } );
268  *     var contentFrame = new OO.ui.PanelLayout( {
269  *         expanded: false,
270  *         padded: true
271  *     } );
272  *     frame.$element.append(
273  *         toolbar.$element,
274  *         contentFrame.$element.append( $area )
275  *     );
276  *     $( 'body' ).append( frame.$element );
278  *     // Here is where the toolbar is actually built. This must be done after inserting it into the
279  *     // document.
280  *     toolbar.initialize();
281  *     toolbar.emit( 'updateState' );
283  * @class
284  * @extends OO.ui.Element
285  * @mixins OO.EventEmitter
286  * @mixins OO.ui.mixin.GroupElement
288  * @constructor
289  * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
290  * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating toolgroups
291  * @param {Object} [config] Configuration options
292  * @cfg {boolean} [actions] Add an actions section to the toolbar. Actions are commands that are included
293  *  in the toolbar, but are not configured as tools. By default, actions are displayed on the right side of
294  *  the toolbar.
295  * @cfg {boolean} [shadow] Add a shadow below the toolbar.
296  */
297 OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
298         // Allow passing positional parameters inside the config object
299         if ( OO.isPlainObject( toolFactory ) && config === undefined ) {
300                 config = toolFactory;
301                 toolFactory = config.toolFactory;
302                 toolGroupFactory = config.toolGroupFactory;
303         }
305         // Configuration initialization
306         config = config || {};
308         // Parent constructor
309         OO.ui.Toolbar.parent.call( this, config );
311         // Mixin constructors
312         OO.EventEmitter.call( this );
313         OO.ui.mixin.GroupElement.call( this, config );
315         // Properties
316         this.toolFactory = toolFactory;
317         this.toolGroupFactory = toolGroupFactory;
318         this.groups = [];
319         this.tools = {};
320         this.$bar = $( '<div>' );
321         this.$actions = $( '<div>' );
322         this.initialized = false;
323         this.narrowThreshold = null;
324         this.onWindowResizeHandler = this.onWindowResize.bind( this );
326         // Events
327         this.$element
328                 .add( this.$bar ).add( this.$group ).add( this.$actions )
329                 .on( 'mousedown keydown', this.onPointerDown.bind( this ) );
331         // Initialization
332         this.$group.addClass( 'oo-ui-toolbar-tools' );
333         if ( config.actions ) {
334                 this.$bar.append( this.$actions.addClass( 'oo-ui-toolbar-actions' ) );
335         }
336         this.$bar
337                 .addClass( 'oo-ui-toolbar-bar' )
338                 .append( this.$group, '<div style="clear:both"></div>' );
339         if ( config.shadow ) {
340                 this.$bar.append( '<div class="oo-ui-toolbar-shadow"></div>' );
341         }
342         this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar );
345 /* Setup */
347 OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
348 OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
349 OO.mixinClass( OO.ui.Toolbar, OO.ui.mixin.GroupElement );
351 /* Events */
354  * @event updateState
356  * An 'updateState' event must be emitted on the Toolbar (by calling `toolbar.emit( 'updateState' )`)
357  * every time the state of the application using the toolbar changes, and an update to the state of
358  * tools is required.
360  * @param {...Mixed} data Application-defined parameters
361  */
363 /* Methods */
366  * Get the tool factory.
368  * @return {OO.ui.ToolFactory} Tool factory
369  */
370 OO.ui.Toolbar.prototype.getToolFactory = function () {
371         return this.toolFactory;
375  * Get the toolgroup factory.
377  * @return {OO.Factory} Toolgroup factory
378  */
379 OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
380         return this.toolGroupFactory;
384  * Handles mouse down events.
386  * @private
387  * @param {jQuery.Event} e Mouse down event
388  */
389 OO.ui.Toolbar.prototype.onPointerDown = function ( e ) {
390         var $closestWidgetToEvent = $( e.target ).closest( '.oo-ui-widget' ),
391                 $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
392         if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[ 0 ] === $closestWidgetToToolbar[ 0 ] ) {
393                 return false;
394         }
398  * Handle window resize event.
400  * @private
401  * @param {jQuery.Event} e Window resize event
402  */
403 OO.ui.Toolbar.prototype.onWindowResize = function () {
404         this.$element.toggleClass(
405                 'oo-ui-toolbar-narrow',
406                 this.$bar.width() <= this.getNarrowThreshold()
407         );
411  * Get the (lazily-computed) width threshold for applying the oo-ui-toolbar-narrow
412  * class.
414  * @private
415  * @return {number} Width threshold in pixels
416  */
417 OO.ui.Toolbar.prototype.getNarrowThreshold = function () {
418         if ( this.narrowThreshold === null ) {
419                 this.narrowThreshold = this.$group.width() + this.$actions.width();
420         }
421         return this.narrowThreshold;
425  * Sets up handles and preloads required information for the toolbar to work.
426  * This must be called after it is attached to a visible document and before doing anything else.
427  */
428 OO.ui.Toolbar.prototype.initialize = function () {
429         if ( !this.initialized ) {
430                 this.initialized = true;
431                 $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
432                 this.onWindowResize();
433         }
437  * Set up the toolbar.
439  * The toolbar is set up with a list of toolgroup configurations that specify the type of
440  * toolgroup ({@link OO.ui.BarToolGroup bar}, {@link OO.ui.MenuToolGroup menu}, or {@link OO.ui.ListToolGroup list})
441  * to add and which tools to include, exclude, promote, or demote within that toolgroup. Please
442  * see {@link OO.ui.ToolGroup toolgroups} for more information about including tools in toolgroups.
444  * @param {Object.<string,Array>} groups List of toolgroup configurations
445  * @param {Array|string} [groups.include] Tools to include in the toolgroup
446  * @param {Array|string} [groups.exclude] Tools to exclude from the toolgroup
447  * @param {Array|string} [groups.promote] Tools to promote to the beginning of the toolgroup
448  * @param {Array|string} [groups.demote] Tools to demote to the end of the toolgroup
449  */
450 OO.ui.Toolbar.prototype.setup = function ( groups ) {
451         var i, len, type, group,
452                 items = [],
453                 defaultType = 'bar';
455         // Cleanup previous groups
456         this.reset();
458         // Build out new groups
459         for ( i = 0, len = groups.length; i < len; i++ ) {
460                 group = groups[ i ];
461                 if ( group.include === '*' ) {
462                         // Apply defaults to catch-all groups
463                         if ( group.type === undefined ) {
464                                 group.type = 'list';
465                         }
466                         if ( group.label === undefined ) {
467                                 group.label = OO.ui.msg( 'ooui-toolbar-more' );
468                         }
469                 }
470                 // Check type has been registered
471                 type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType;
472                 items.push(
473                         this.getToolGroupFactory().create( type, this, group )
474                 );
475         }
476         this.addItems( items );
480  * Remove all tools and toolgroups from the toolbar.
481  */
482 OO.ui.Toolbar.prototype.reset = function () {
483         var i, len;
485         this.groups = [];
486         this.tools = {};
487         for ( i = 0, len = this.items.length; i < len; i++ ) {
488                 this.items[ i ].destroy();
489         }
490         this.clearItems();
494  * Destroy the toolbar.
496  * Destroying the toolbar removes all event handlers and DOM elements that constitute the toolbar. Call
497  * this method whenever you are done using a toolbar.
498  */
499 OO.ui.Toolbar.prototype.destroy = function () {
500         $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
501         this.reset();
502         this.$element.remove();
506  * Check if the tool is available.
508  * Available tools are ones that have not yet been added to the toolbar.
510  * @param {string} name Symbolic name of tool
511  * @return {boolean} Tool is available
512  */
513 OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
514         return !this.tools[ name ];
518  * Prevent tool from being used again.
520  * @param {OO.ui.Tool} tool Tool to reserve
521  */
522 OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
523         this.tools[ tool.getName() ] = tool;
527  * Allow tool to be used again.
529  * @param {OO.ui.Tool} tool Tool to release
530  */
531 OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
532         delete this.tools[ tool.getName() ];
536  * Get accelerator label for tool.
538  * The OOjs UI library does not contain an accelerator system, but this is the hook for one. To
539  * use an accelerator system, subclass the toolbar and override this method, which is meant to return a label
540  * that describes the accelerator keys for the tool passed (by symbolic name) to the method.
542  * @param {string} name Symbolic name of tool
543  * @return {string|undefined} Tool accelerator label if available
544  */
545 OO.ui.Toolbar.prototype.getToolAccelerator = function () {
546         return undefined;
550  * Tools, together with {@link OO.ui.ToolGroup toolgroups}, constitute {@link OO.ui.Toolbar toolbars}.
551  * Each tool is configured with a static name, title, and icon and is customized with the command to carry
552  * out when the tool is selected. Tools must also be registered with a {@link OO.ui.ToolFactory tool factory},
553  * which creates the tools on demand.
555  * Every Tool subclass must implement two methods:
557  * - {@link #onUpdateState}
558  * - {@link #onSelect}
560  * Tools are added to toolgroups ({@link OO.ui.ListToolGroup ListToolGroup},
561  * {@link OO.ui.BarToolGroup BarToolGroup}, or {@link OO.ui.MenuToolGroup MenuToolGroup}), which determine how
562  * the tool is displayed in the toolbar. See {@link OO.ui.Toolbar toolbars} for an example.
564  * For more information, please see the [OOjs UI documentation on MediaWiki][1].
565  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
567  * @abstract
568  * @class
569  * @extends OO.ui.Widget
570  * @mixins OO.ui.mixin.IconElement
571  * @mixins OO.ui.mixin.FlaggedElement
572  * @mixins OO.ui.mixin.TabIndexedElement
574  * @constructor
575  * @param {OO.ui.ToolGroup} toolGroup
576  * @param {Object} [config] Configuration options
577  * @cfg {string|Function} [title] Title text or a function that returns text. If this config is omitted, the value of
578  *  the {@link #static-title static title} property is used.
580  *  The title is used in different ways depending on the type of toolgroup that contains the tool. The
581  *  title is used as a tooltip if the tool is part of a {@link OO.ui.BarToolGroup bar} toolgroup, or as the label text if the tool is
582  *  part of a {@link OO.ui.ListToolGroup list} or {@link OO.ui.MenuToolGroup menu} toolgroup.
584  *  For bar toolgroups, a description of the accelerator key is appended to the title if an accelerator key
585  *  is associated with an action by the same name as the tool and accelerator functionality has been added to the application.
586  *  To add accelerator key functionality, you must subclass OO.ui.Toolbar and override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method.
587  */
588 OO.ui.Tool = function OoUiTool( toolGroup, config ) {
589         // Allow passing positional parameters inside the config object
590         if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
591                 config = toolGroup;
592                 toolGroup = config.toolGroup;
593         }
595         // Configuration initialization
596         config = config || {};
598         // Parent constructor
599         OO.ui.Tool.parent.call( this, config );
601         // Properties
602         this.toolGroup = toolGroup;
603         this.toolbar = this.toolGroup.getToolbar();
604         this.active = false;
605         this.$title = $( '<span>' );
606         this.$accel = $( '<span>' );
607         this.$link = $( '<a>' );
608         this.title = null;
610         // Mixin constructors
611         OO.ui.mixin.IconElement.call( this, config );
612         OO.ui.mixin.FlaggedElement.call( this, config );
613         OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$link } ) );
615         // Events
616         this.toolbar.connect( this, { updateState: 'onUpdateState' } );
618         // Initialization
619         this.$title.addClass( 'oo-ui-tool-title' );
620         this.$accel
621                 .addClass( 'oo-ui-tool-accel' )
622                 .prop( {
623                         // This may need to be changed if the key names are ever localized,
624                         // but for now they are essentially written in English
625                         dir: 'ltr',
626                         lang: 'en'
627                 } );
628         this.$link
629                 .addClass( 'oo-ui-tool-link' )
630                 .append( this.$icon, this.$title, this.$accel )
631                 .attr( 'role', 'button' );
632         this.$element
633                 .data( 'oo-ui-tool', this )
634                 .addClass(
635                         'oo-ui-tool ' + 'oo-ui-tool-name-' +
636                         this.constructor.static.name.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' )
637                 )
638                 .toggleClass( 'oo-ui-tool-with-label', this.constructor.static.displayBothIconAndLabel )
639                 .append( this.$link );
640         this.setTitle( config.title || this.constructor.static.title );
643 /* Setup */
645 OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
646 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.IconElement );
647 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.FlaggedElement );
648 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.TabIndexedElement );
650 /* Static Properties */
653  * @static
654  * @inheritdoc
655  */
656 OO.ui.Tool.static.tagName = 'span';
659  * Symbolic name of tool.
661  * The symbolic name is used internally to register the tool with a {@link OO.ui.ToolFactory ToolFactory}. It can
662  * also be used when adding tools to toolgroups.
664  * @abstract
665  * @static
666  * @inheritable
667  * @property {string}
668  */
669 OO.ui.Tool.static.name = '';
672  * Symbolic name of the group.
674  * The group name is used to associate tools with each other so that they can be selected later by
675  * a {@link OO.ui.ToolGroup toolgroup}.
677  * @abstract
678  * @static
679  * @inheritable
680  * @property {string}
681  */
682 OO.ui.Tool.static.group = '';
685  * Tool title text or a function that returns title text. The value of the static property is overridden if the #title config option is used.
687  * @abstract
688  * @static
689  * @inheritable
690  * @property {string|Function}
691  */
692 OO.ui.Tool.static.title = '';
695  * Display both icon and label when the tool is used in a {@link OO.ui.BarToolGroup bar} toolgroup.
696  * Normally only the icon is displayed, or only the label if no icon is given.
698  * @static
699  * @inheritable
700  * @property {boolean}
701  */
702 OO.ui.Tool.static.displayBothIconAndLabel = false;
705  * Add tool to catch-all groups automatically.
707  * A catch-all group, which contains all tools that do not currently belong to a toolgroup,
708  * can be included in a toolgroup using the wildcard selector, an asterisk (*).
710  * @static
711  * @inheritable
712  * @property {boolean}
713  */
714 OO.ui.Tool.static.autoAddToCatchall = true;
717  * Add tool to named groups automatically.
719  * By default, tools that are configured with a static ‘group’ property are added
720  * to that group and will be selected when the symbolic name of the group is specified (e.g., when
721  * toolgroups include tools by group name).
723  * @static
724  * @property {boolean}
725  * @inheritable
726  */
727 OO.ui.Tool.static.autoAddToGroup = true;
730  * Check if this tool is compatible with given data.
732  * This is a stub that can be overridden to provide support for filtering tools based on an
733  * arbitrary piece of information  (e.g., where the cursor is in a document). The implementation
734  * must also call this method so that the compatibility check can be performed.
736  * @static
737  * @inheritable
738  * @param {Mixed} data Data to check
739  * @return {boolean} Tool can be used with data
740  */
741 OO.ui.Tool.static.isCompatibleWith = function () {
742         return false;
745 /* Methods */
748  * Handle the toolbar state being updated. This method is called when the
749  * {@link OO.ui.Toolbar#event-updateState 'updateState' event} is emitted on the
750  * {@link OO.ui.Toolbar Toolbar} that uses this tool, and should set the state of this tool
751  * depending on application state (usually by calling #setDisabled to enable or disable the tool,
752  * or #setActive to mark is as currently in-use or not).
754  * This is an abstract method that must be overridden in a concrete subclass.
756  * @method
757  * @protected
758  * @abstract
759  */
760 OO.ui.Tool.prototype.onUpdateState = null;
763  * Handle the tool being selected. This method is called when the user triggers this tool,
764  * usually by clicking on its label/icon.
766  * This is an abstract method that must be overridden in a concrete subclass.
768  * @method
769  * @protected
770  * @abstract
771  */
772 OO.ui.Tool.prototype.onSelect = null;
775  * Check if the tool is active.
777  * Tools become active when their #onSelect or #onUpdateState handlers change them to appear pressed
778  * with the #setActive method. Additional CSS is applied to the tool to reflect the active state.
780  * @return {boolean} Tool is active
781  */
782 OO.ui.Tool.prototype.isActive = function () {
783         return this.active;
787  * Make the tool appear active or inactive.
789  * This method should be called within #onSelect or #onUpdateState event handlers to make the tool
790  * appear pressed or not.
792  * @param {boolean} state Make tool appear active
793  */
794 OO.ui.Tool.prototype.setActive = function ( state ) {
795         this.active = !!state;
796         if ( this.active ) {
797                 this.$element.addClass( 'oo-ui-tool-active' );
798                 this.setFlags( 'progressive' );
799         } else {
800                 this.$element.removeClass( 'oo-ui-tool-active' );
801                 this.clearFlags();
802         }
806  * Set the tool #title.
808  * @param {string|Function} title Title text or a function that returns text
809  * @chainable
810  */
811 OO.ui.Tool.prototype.setTitle = function ( title ) {
812         this.title = OO.ui.resolveMsg( title );
813         this.updateTitle();
814         return this;
818  * Get the tool #title.
820  * @return {string} Title text
821  */
822 OO.ui.Tool.prototype.getTitle = function () {
823         return this.title;
827  * Get the tool's symbolic name.
829  * @return {string} Symbolic name of tool
830  */
831 OO.ui.Tool.prototype.getName = function () {
832         return this.constructor.static.name;
836  * Update the title.
837  */
838 OO.ui.Tool.prototype.updateTitle = function () {
839         var titleTooltips = this.toolGroup.constructor.static.titleTooltips,
840                 accelTooltips = this.toolGroup.constructor.static.accelTooltips,
841                 accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
842                 tooltipParts = [];
844         this.$title.text( this.title );
845         this.$accel.text( accel );
847         if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
848                 tooltipParts.push( this.title );
849         }
850         if ( accelTooltips && typeof accel === 'string' && accel.length ) {
851                 tooltipParts.push( accel );
852         }
853         if ( tooltipParts.length ) {
854                 this.$link.attr( 'title', tooltipParts.join( ' ' ) );
855         } else {
856                 this.$link.removeAttr( 'title' );
857         }
861  * Destroy tool.
863  * Destroying the tool removes all event handlers and the tool’s DOM elements.
864  * Call this method whenever you are done using a tool.
865  */
866 OO.ui.Tool.prototype.destroy = function () {
867         this.toolbar.disconnect( this );
868         this.$element.remove();
872  * ToolGroups are collections of {@link OO.ui.Tool tools} that are used in a {@link OO.ui.Toolbar toolbar}.
873  * The type of toolgroup ({@link OO.ui.ListToolGroup list}, {@link OO.ui.BarToolGroup bar}, or {@link OO.ui.MenuToolGroup menu})
874  * to which a tool belongs determines how the tool is arranged and displayed in the toolbar. Toolgroups
875  * themselves are created on demand with a {@link OO.ui.ToolGroupFactory toolgroup factory}.
877  * Toolgroups can contain individual tools, groups of tools, or all available tools, as specified
878  * using the `include` config option. See OO.ui.ToolFactory#extract on documentation of the format.
879  * The options `exclude`, `promote`, and `demote` support the same formats.
881  * See {@link OO.ui.Toolbar toolbars} for a full example. For more information about toolbars in general,
882  * please see the [OOjs UI documentation on MediaWiki][1].
884  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
886  * @abstract
887  * @class
888  * @extends OO.ui.Widget
889  * @mixins OO.ui.mixin.GroupElement
891  * @constructor
892  * @param {OO.ui.Toolbar} toolbar
893  * @param {Object} [config] Configuration options
894  * @cfg {Array|string} [include] List of tools to include in the toolgroup, see above.
895  * @cfg {Array|string} [exclude] List of tools to exclude from the toolgroup, see above.
896  * @cfg {Array|string} [promote] List of tools to promote to the beginning of the toolgroup, see above.
897  * @cfg {Array|string} [demote] List of tools to demote to the end of the toolgroup, see above.
898  *  This setting is particularly useful when tools have been added to the toolgroup
899  *  en masse (e.g., via the catch-all selector).
900  */
901 OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
902         // Allow passing positional parameters inside the config object
903         if ( OO.isPlainObject( toolbar ) && config === undefined ) {
904                 config = toolbar;
905                 toolbar = config.toolbar;
906         }
908         // Configuration initialization
909         config = config || {};
911         // Parent constructor
912         OO.ui.ToolGroup.parent.call( this, config );
914         // Mixin constructors
915         OO.ui.mixin.GroupElement.call( this, config );
917         // Properties
918         this.toolbar = toolbar;
919         this.tools = {};
920         this.pressed = null;
921         this.autoDisabled = false;
922         this.include = config.include || [];
923         this.exclude = config.exclude || [];
924         this.promote = config.promote || [];
925         this.demote = config.demote || [];
926         this.onCapturedMouseKeyUpHandler = this.onCapturedMouseKeyUp.bind( this );
928         // Events
929         this.$element.on( {
930                 mousedown: this.onMouseKeyDown.bind( this ),
931                 mouseup: this.onMouseKeyUp.bind( this ),
932                 keydown: this.onMouseKeyDown.bind( this ),
933                 keyup: this.onMouseKeyUp.bind( this ),
934                 focus: this.onMouseOverFocus.bind( this ),
935                 blur: this.onMouseOutBlur.bind( this ),
936                 mouseover: this.onMouseOverFocus.bind( this ),
937                 mouseout: this.onMouseOutBlur.bind( this )
938         } );
939         this.toolbar.getToolFactory().connect( this, { register: 'onToolFactoryRegister' } );
940         this.aggregate( { disable: 'itemDisable' } );
941         this.connect( this, { itemDisable: 'updateDisabled' } );
943         // Initialization
944         this.$group.addClass( 'oo-ui-toolGroup-tools' );
945         this.$element
946                 .addClass( 'oo-ui-toolGroup' )
947                 .append( this.$group );
948         this.populate();
951 /* Setup */
953 OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget );
954 OO.mixinClass( OO.ui.ToolGroup, OO.ui.mixin.GroupElement );
956 /* Events */
959  * @event update
960  */
962 /* Static Properties */
965  * Show labels in tooltips.
967  * @static
968  * @inheritable
969  * @property {boolean}
970  */
971 OO.ui.ToolGroup.static.titleTooltips = false;
974  * Show acceleration labels in tooltips.
976  * Note: The OOjs UI library does not include an accelerator system, but does contain
977  * a hook for one. To use an accelerator system, subclass the {@link OO.ui.Toolbar toolbar} and
978  * override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method, which is
979  * meant to return a label that describes the accelerator keys for a given tool (e.g., 'Ctrl + M').
981  * @static
982  * @inheritable
983  * @property {boolean}
984  */
985 OO.ui.ToolGroup.static.accelTooltips = false;
988  * Automatically disable the toolgroup when all tools are disabled
990  * @static
991  * @inheritable
992  * @property {boolean}
993  */
994 OO.ui.ToolGroup.static.autoDisable = true;
996 /* Methods */
999  * @inheritdoc
1000  */
1001 OO.ui.ToolGroup.prototype.isDisabled = function () {
1002         return this.autoDisabled || OO.ui.ToolGroup.parent.prototype.isDisabled.apply( this, arguments );
1006  * @inheritdoc
1007  */
1008 OO.ui.ToolGroup.prototype.updateDisabled = function () {
1009         var i, item, allDisabled = true;
1011         if ( this.constructor.static.autoDisable ) {
1012                 for ( i = this.items.length - 1; i >= 0; i-- ) {
1013                         item = this.items[ i ];
1014                         if ( !item.isDisabled() ) {
1015                                 allDisabled = false;
1016                                 break;
1017                         }
1018                 }
1019                 this.autoDisabled = allDisabled;
1020         }
1021         OO.ui.ToolGroup.parent.prototype.updateDisabled.apply( this, arguments );
1025  * Handle mouse down and key down events.
1027  * @protected
1028  * @param {jQuery.Event} e Mouse down or key down event
1029  */
1030 OO.ui.ToolGroup.prototype.onMouseKeyDown = function ( e ) {
1031         if (
1032                 !this.isDisabled() &&
1033                 ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
1034         ) {
1035                 this.pressed = this.getTargetTool( e );
1036                 if ( this.pressed ) {
1037                         this.pressed.setActive( true );
1038                         this.getElementDocument().addEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler, true );
1039                         this.getElementDocument().addEventListener( 'keyup', this.onCapturedMouseKeyUpHandler, true );
1040                 }
1041                 return false;
1042         }
1046  * Handle captured mouse up and key up events.
1048  * @protected
1049  * @param {MouseEvent|KeyboardEvent} e Mouse up or key up event
1050  */
1051 OO.ui.ToolGroup.prototype.onCapturedMouseKeyUp = function ( e ) {
1052         this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler, true );
1053         this.getElementDocument().removeEventListener( 'keyup', this.onCapturedMouseKeyUpHandler, true );
1054         // onMouseKeyUp may be called a second time, depending on where the mouse is when the button is
1055         // released, but since `this.pressed` will no longer be true, the second call will be ignored.
1056         this.onMouseKeyUp( e );
1060  * Handle mouse up and key up events.
1062  * @protected
1063  * @param {MouseEvent|KeyboardEvent} e Mouse up or key up event
1064  */
1065 OO.ui.ToolGroup.prototype.onMouseKeyUp = function ( e ) {
1066         var tool = this.getTargetTool( e );
1068         if (
1069                 !this.isDisabled() && this.pressed && this.pressed === tool &&
1070                 ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
1071         ) {
1072                 this.pressed.onSelect();
1073                 this.pressed = null;
1074                 e.preventDefault();
1075                 e.stopPropagation();
1076         }
1078         this.pressed = null;
1082  * Handle mouse over and focus events.
1084  * @protected
1085  * @param {jQuery.Event} e Mouse over or focus event
1086  */
1087 OO.ui.ToolGroup.prototype.onMouseOverFocus = function ( e ) {
1088         var tool = this.getTargetTool( e );
1090         if ( this.pressed && this.pressed === tool ) {
1091                 this.pressed.setActive( true );
1092         }
1096  * Handle mouse out and blur events.
1098  * @protected
1099  * @param {jQuery.Event} e Mouse out or blur event
1100  */
1101 OO.ui.ToolGroup.prototype.onMouseOutBlur = function ( e ) {
1102         var tool = this.getTargetTool( e );
1104         if ( this.pressed && this.pressed === tool ) {
1105                 this.pressed.setActive( false );
1106         }
1110  * Get the closest tool to a jQuery.Event.
1112  * Only tool links are considered, which prevents other elements in the tool such as popups from
1113  * triggering tool group interactions.
1115  * @private
1116  * @param {jQuery.Event} e
1117  * @return {OO.ui.Tool|null} Tool, `null` if none was found
1118  */
1119 OO.ui.ToolGroup.prototype.getTargetTool = function ( e ) {
1120         var tool,
1121                 $item = $( e.target ).closest( '.oo-ui-tool-link' );
1123         if ( $item.length ) {
1124                 tool = $item.parent().data( 'oo-ui-tool' );
1125         }
1127         return tool && !tool.isDisabled() ? tool : null;
1131  * Handle tool registry register events.
1133  * If a tool is registered after the group is created, we must repopulate the list to account for:
1135  * - a tool being added that may be included
1136  * - a tool already included being overridden
1138  * @protected
1139  * @param {string} name Symbolic name of tool
1140  */
1141 OO.ui.ToolGroup.prototype.onToolFactoryRegister = function () {
1142         this.populate();
1146  * Get the toolbar that contains the toolgroup.
1148  * @return {OO.ui.Toolbar} Toolbar that contains the toolgroup
1149  */
1150 OO.ui.ToolGroup.prototype.getToolbar = function () {
1151         return this.toolbar;
1155  * Add and remove tools based on configuration.
1156  */
1157 OO.ui.ToolGroup.prototype.populate = function () {
1158         var i, len, name, tool,
1159                 toolFactory = this.toolbar.getToolFactory(),
1160                 names = {},
1161                 add = [],
1162                 remove = [],
1163                 list = this.toolbar.getToolFactory().getTools(
1164                         this.include, this.exclude, this.promote, this.demote
1165                 );
1167         // Build a list of needed tools
1168         for ( i = 0, len = list.length; i < len; i++ ) {
1169                 name = list[ i ];
1170                 if (
1171                         // Tool exists
1172                         toolFactory.lookup( name ) &&
1173                         // Tool is available or is already in this group
1174                         ( this.toolbar.isToolAvailable( name ) || this.tools[ name ] )
1175                 ) {
1176                         // Hack to prevent infinite recursion via ToolGroupTool. We need to reserve the tool before
1177                         // creating it, but we can't call reserveTool() yet because we haven't created the tool.
1178                         this.toolbar.tools[ name ] = true;
1179                         tool = this.tools[ name ];
1180                         if ( !tool ) {
1181                                 // Auto-initialize tools on first use
1182                                 this.tools[ name ] = tool = toolFactory.create( name, this );
1183                                 tool.updateTitle();
1184                         }
1185                         this.toolbar.reserveTool( tool );
1186                         add.push( tool );
1187                         names[ name ] = true;
1188                 }
1189         }
1190         // Remove tools that are no longer needed
1191         for ( name in this.tools ) {
1192                 if ( !names[ name ] ) {
1193                         this.tools[ name ].destroy();
1194                         this.toolbar.releaseTool( this.tools[ name ] );
1195                         remove.push( this.tools[ name ] );
1196                         delete this.tools[ name ];
1197                 }
1198         }
1199         if ( remove.length ) {
1200                 this.removeItems( remove );
1201         }
1202         // Update emptiness state
1203         if ( add.length ) {
1204                 this.$element.removeClass( 'oo-ui-toolGroup-empty' );
1205         } else {
1206                 this.$element.addClass( 'oo-ui-toolGroup-empty' );
1207         }
1208         // Re-add tools (moving existing ones to new locations)
1209         this.addItems( add );
1210         // Disabled state may depend on items
1211         this.updateDisabled();
1215  * Destroy toolgroup.
1216  */
1217 OO.ui.ToolGroup.prototype.destroy = function () {
1218         var name;
1220         this.clearItems();
1221         this.toolbar.getToolFactory().disconnect( this );
1222         for ( name in this.tools ) {
1223                 this.toolbar.releaseTool( this.tools[ name ] );
1224                 this.tools[ name ].disconnect( this ).destroy();
1225                 delete this.tools[ name ];
1226         }
1227         this.$element.remove();
1231  * A ToolFactory creates tools on demand. All tools ({@link OO.ui.Tool Tools}, {@link OO.ui.PopupTool PopupTools},
1232  * and {@link OO.ui.ToolGroupTool ToolGroupTools}) must be registered with a tool factory. Tools are
1233  * registered by their symbolic name. See {@link OO.ui.Toolbar toolbars} for an example.
1235  * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
1237  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
1239  * @class
1240  * @extends OO.Factory
1241  * @constructor
1242  */
1243 OO.ui.ToolFactory = function OoUiToolFactory() {
1244         // Parent constructor
1245         OO.ui.ToolFactory.parent.call( this );
1248 /* Setup */
1250 OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
1252 /* Methods */
1255  * Get tools from the factory
1257  * @param {Array|string} [include] Included tools, see #extract for format
1258  * @param {Array|string} [exclude] Excluded tools, see #extract for format
1259  * @param {Array|string} [promote] Promoted tools, see #extract for format
1260  * @param {Array|string} [demote] Demoted tools, see #extract for format
1261  * @return {string[]} List of tools
1262  */
1263 OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
1264         var i, len, included, promoted, demoted,
1265                 auto = [],
1266                 used = {};
1268         // Collect included and not excluded tools
1269         included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
1271         // Promotion
1272         promoted = this.extract( promote, used );
1273         demoted = this.extract( demote, used );
1275         // Auto
1276         for ( i = 0, len = included.length; i < len; i++ ) {
1277                 if ( !used[ included[ i ] ] ) {
1278                         auto.push( included[ i ] );
1279                 }
1280         }
1282         return promoted.concat( auto ).concat( demoted );
1286  * Get a flat list of names from a list of names or groups.
1288  * Normally, `collection` is an array of tool specifications. Tools can be specified in the
1289  * following ways:
1291  * - To include an individual tool, use the symbolic name: `{ name: 'tool-name' }` or `'tool-name'`.
1292  * - To include all tools in a group, use the group name: `{ group: 'group-name' }`. (To assign the
1293  *   tool to a group, use OO.ui.Tool.static.group.)
1295  * Alternatively, to include all tools that are not yet assigned to any other toolgroup, use the
1296  * catch-all selector `'*'`.
1298  * If `used` is passed, tool names that appear as properties in this object will be considered
1299  * already assigned, and will not be returned even if specified otherwise. The tool names extracted
1300  * by this function call will be added as new properties in the object.
1302  * @private
1303  * @param {Array|string} collection List of tools, see above
1304  * @param {Object} [used] Object containing information about used tools, see above
1305  * @return {string[]} List of extracted tool names
1306  */
1307 OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
1308         var i, len, item, name, tool,
1309                 names = [];
1311         collection = !Array.isArray( collection ) ? [ collection ] : collection;
1313         for ( i = 0, len = collection.length; i < len; i++ ) {
1314                 item = collection[ i ];
1315                 if ( item === '*' ) {
1316                         for ( name in this.registry ) {
1317                                 tool = this.registry[ name ];
1318                                 if (
1319                                         // Only add tools by group name when auto-add is enabled
1320                                         tool.static.autoAddToCatchall &&
1321                                         // Exclude already used tools
1322                                         ( !used || !used[ name ] )
1323                                 ) {
1324                                         names.push( name );
1325                                         if ( used ) {
1326                                                 used[ name ] = true;
1327                                         }
1328                                 }
1329                         }
1330                 } else {
1331                         // Allow plain strings as shorthand for named tools
1332                         if ( typeof item === 'string' ) {
1333                                 item = { name: item };
1334                         }
1335                         if ( OO.isPlainObject( item ) ) {
1336                                 if ( item.group ) {
1337                                         for ( name in this.registry ) {
1338                                                 tool = this.registry[ name ];
1339                                                 if (
1340                                                         // Include tools with matching group
1341                                                         tool.static.group === item.group &&
1342                                                         // Only add tools by group name when auto-add is enabled
1343                                                         tool.static.autoAddToGroup &&
1344                                                         // Exclude already used tools
1345                                                         ( !used || !used[ name ] )
1346                                                 ) {
1347                                                         names.push( name );
1348                                                         if ( used ) {
1349                                                                 used[ name ] = true;
1350                                                         }
1351                                                 }
1352                                         }
1353                                 // Include tools with matching name and exclude already used tools
1354                                 } else if ( item.name && ( !used || !used[ item.name ] ) ) {
1355                                         names.push( item.name );
1356                                         if ( used ) {
1357                                                 used[ item.name ] = true;
1358                                         }
1359                                 }
1360                         }
1361                 }
1362         }
1363         return names;
1367  * ToolGroupFactories create {@link OO.ui.ToolGroup toolgroups} on demand. The toolgroup classes must
1368  * specify a symbolic name and be registered with the factory. The following classes are registered by
1369  * default:
1371  * - {@link OO.ui.BarToolGroup BarToolGroups} (‘bar’)
1372  * - {@link OO.ui.MenuToolGroup MenuToolGroups} (‘menu’)
1373  * - {@link OO.ui.ListToolGroup ListToolGroups} (‘list’)
1375  * See {@link OO.ui.Toolbar toolbars} for an example.
1377  * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
1379  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
1381  * @class
1382  * @extends OO.Factory
1383  * @constructor
1384  */
1385 OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() {
1386         var i, l, defaultClasses;
1387         // Parent constructor
1388         OO.Factory.call( this );
1390         defaultClasses = this.constructor.static.getDefaultClasses();
1392         // Register default toolgroups
1393         for ( i = 0, l = defaultClasses.length; i < l; i++ ) {
1394                 this.register( defaultClasses[ i ] );
1395         }
1398 /* Setup */
1400 OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory );
1402 /* Static Methods */
1405  * Get a default set of classes to be registered on construction.
1407  * @return {Function[]} Default classes
1408  */
1409 OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
1410         return [
1411                 OO.ui.BarToolGroup,
1412                 OO.ui.ListToolGroup,
1413                 OO.ui.MenuToolGroup
1414         ];
1418  * Popup tools open a popup window when they are selected from the {@link OO.ui.Toolbar toolbar}. Each popup tool is configured
1419  * with a static name, title, and icon, as well with as any popup configurations. Unlike other tools, popup tools do not require that developers specify
1420  * an #onSelect or #onUpdateState method, as these methods have been implemented already.
1422  *     // Example of a popup tool. When selected, a popup tool displays
1423  *     // a popup window.
1424  *     function HelpTool( toolGroup, config ) {
1425  *        OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
1426  *            padded: true,
1427  *            label: 'Help',
1428  *            head: true
1429  *        } }, config ) );
1430  *        this.popup.$body.append( '<p>I am helpful!</p>' );
1431  *     };
1432  *     OO.inheritClass( HelpTool, OO.ui.PopupTool );
1433  *     HelpTool.static.name = 'help';
1434  *     HelpTool.static.icon = 'help';
1435  *     HelpTool.static.title = 'Help';
1436  *     toolFactory.register( HelpTool );
1438  * For an example of a toolbar that contains a popup tool, see {@link OO.ui.Toolbar toolbars}. For more information about
1439  * toolbars in genreral, please see the [OOjs UI documentation on MediaWiki][1].
1441  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
1443  * @abstract
1444  * @class
1445  * @extends OO.ui.Tool
1446  * @mixins OO.ui.mixin.PopupElement
1448  * @constructor
1449  * @param {OO.ui.ToolGroup} toolGroup
1450  * @param {Object} [config] Configuration options
1451  */
1452 OO.ui.PopupTool = function OoUiPopupTool( toolGroup, config ) {
1453         // Allow passing positional parameters inside the config object
1454         if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
1455                 config = toolGroup;
1456                 toolGroup = config.toolGroup;
1457         }
1459         // Parent constructor
1460         OO.ui.PopupTool.parent.call( this, toolGroup, config );
1462         // Mixin constructors
1463         OO.ui.mixin.PopupElement.call( this, config );
1465         // Initialization
1466         this.$element
1467                 .addClass( 'oo-ui-popupTool' )
1468                 .append( this.popup.$element );
1471 /* Setup */
1473 OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool );
1474 OO.mixinClass( OO.ui.PopupTool, OO.ui.mixin.PopupElement );
1476 /* Methods */
1479  * Handle the tool being selected.
1481  * @inheritdoc
1482  */
1483 OO.ui.PopupTool.prototype.onSelect = function () {
1484         if ( !this.isDisabled() ) {
1485                 this.popup.toggle();
1486         }
1487         this.setActive( false );
1488         return false;
1492  * Handle the toolbar state being updated.
1494  * @inheritdoc
1495  */
1496 OO.ui.PopupTool.prototype.onUpdateState = function () {
1497         this.setActive( false );
1501  * A ToolGroupTool is a special sort of tool that can contain other {@link OO.ui.Tool tools}
1502  * and {@link OO.ui.ToolGroup toolgroups}. The ToolGroupTool was specifically designed to be used
1503  * inside a {@link OO.ui.BarToolGroup bar} toolgroup to provide access to additional tools from
1504  * the bar item. Included tools will be displayed in a dropdown {@link OO.ui.ListToolGroup list}
1505  * when the ToolGroupTool is selected.
1507  *     // Example: ToolGroupTool with two nested tools, 'setting1' and 'setting2', defined elsewhere.
1509  *     function SettingsTool() {
1510  *         SettingsTool.parent.apply( this, arguments );
1511  *     };
1512  *     OO.inheritClass( SettingsTool, OO.ui.ToolGroupTool );
1513  *     SettingsTool.static.name = 'settings';
1514  *     SettingsTool.static.title = 'Change settings';
1515  *     SettingsTool.static.groupConfig = {
1516  *         icon: 'settings',
1517  *         label: 'ToolGroupTool',
1518  *         include: [  'setting1', 'setting2'  ]
1519  *     };
1520  *     toolFactory.register( SettingsTool );
1522  * For more information, please see the [OOjs UI documentation on MediaWiki][1].
1524  * Please note that this implementation is subject to change per [T74159] [2].
1526  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars#ToolGroupTool
1527  * [2]: https://phabricator.wikimedia.org/T74159
1529  * @abstract
1530  * @class
1531  * @extends OO.ui.Tool
1533  * @constructor
1534  * @param {OO.ui.ToolGroup} toolGroup
1535  * @param {Object} [config] Configuration options
1536  */
1537 OO.ui.ToolGroupTool = function OoUiToolGroupTool( toolGroup, config ) {
1538         // Allow passing positional parameters inside the config object
1539         if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
1540                 config = toolGroup;
1541                 toolGroup = config.toolGroup;
1542         }
1544         // Parent constructor
1545         OO.ui.ToolGroupTool.parent.call( this, toolGroup, config );
1547         // Properties
1548         this.innerToolGroup = this.createGroup( this.constructor.static.groupConfig );
1550         // Events
1551         this.innerToolGroup.connect( this, { disable: 'onToolGroupDisable' } );
1553         // Initialization
1554         this.$link.remove();
1555         this.$element
1556                 .addClass( 'oo-ui-toolGroupTool' )
1557                 .append( this.innerToolGroup.$element );
1560 /* Setup */
1562 OO.inheritClass( OO.ui.ToolGroupTool, OO.ui.Tool );
1564 /* Static Properties */
1567  * Toolgroup configuration.
1569  * The toolgroup configuration consists of the tools to include, as well as an icon and label
1570  * to use for the bar item. Tools can be included by symbolic name, group, or with the
1571  * wildcard selector. Please see {@link OO.ui.ToolGroup toolgroup} for more information.
1573  * @property {Object.<string,Array>}
1574  */
1575 OO.ui.ToolGroupTool.static.groupConfig = {};
1577 /* Methods */
1580  * Handle the tool being selected.
1582  * @inheritdoc
1583  */
1584 OO.ui.ToolGroupTool.prototype.onSelect = function () {
1585         this.innerToolGroup.setActive( !this.innerToolGroup.active );
1586         return false;
1590  * Synchronize disabledness state of the tool with the inner toolgroup.
1592  * @private
1593  * @param {boolean} disabled Element is disabled
1594  */
1595 OO.ui.ToolGroupTool.prototype.onToolGroupDisable = function ( disabled ) {
1596         this.setDisabled( disabled );
1600  * Handle the toolbar state being updated.
1602  * @inheritdoc
1603  */
1604 OO.ui.ToolGroupTool.prototype.onUpdateState = function () {
1605         this.setActive( false );
1609  * Build a {@link OO.ui.ToolGroup toolgroup} from the specified configuration.
1611  * @param {Object.<string,Array>} group Toolgroup configuration. Please see {@link OO.ui.ToolGroup toolgroup} for
1612  *  more information.
1613  * @return {OO.ui.ListToolGroup}
1614  */
1615 OO.ui.ToolGroupTool.prototype.createGroup = function ( group ) {
1616         if ( group.include === '*' ) {
1617                 // Apply defaults to catch-all groups
1618                 if ( group.label === undefined ) {
1619                         group.label = OO.ui.msg( 'ooui-toolbar-more' );
1620                 }
1621         }
1623         return this.toolbar.getToolGroupFactory().create( 'list', this.toolbar, group );
1627  * BarToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
1628  * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.MenuToolGroup MenuToolGroup}
1629  * and {@link OO.ui.ListToolGroup ListToolGroup}). The {@link OO.ui.Tool tools} in a BarToolGroup are
1630  * displayed by icon in a single row. The title of the tool is displayed when users move the mouse over
1631  * the tool.
1633  * BarToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar is
1634  * set up.
1636  *     @example
1637  *     // Example of a BarToolGroup with two tools
1638  *     var toolFactory = new OO.ui.ToolFactory();
1639  *     var toolGroupFactory = new OO.ui.ToolGroupFactory();
1640  *     var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
1642  *     // We will be placing status text in this element when tools are used
1643  *     var $area = $( '<p>' ).text( 'Example of a BarToolGroup with two tools.' );
1645  *     // Define the tools that we're going to place in our toolbar
1647  *     // Create a class inheriting from OO.ui.Tool
1648  *     function SearchTool() {
1649  *         SearchTool.parent.apply( this, arguments );
1650  *     }
1651  *     OO.inheritClass( SearchTool, OO.ui.Tool );
1652  *     // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
1653  *     // of 'icon' and 'title' (displayed icon and text).
1654  *     SearchTool.static.name = 'search';
1655  *     SearchTool.static.icon = 'search';
1656  *     SearchTool.static.title = 'Search...';
1657  *     // Defines the action that will happen when this tool is selected (clicked).
1658  *     SearchTool.prototype.onSelect = function () {
1659  *         $area.text( 'Search tool clicked!' );
1660  *         // Never display this tool as "active" (selected).
1661  *         this.setActive( false );
1662  *     };
1663  *     SearchTool.prototype.onUpdateState = function () {};
1664  *     // Make this tool available in our toolFactory and thus our toolbar
1665  *     toolFactory.register( SearchTool );
1667  *     // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
1668  *     // little popup window (a PopupWidget).
1669  *     function HelpTool( toolGroup, config ) {
1670  *         OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
1671  *             padded: true,
1672  *             label: 'Help',
1673  *             head: true
1674  *         } }, config ) );
1675  *         this.popup.$body.append( '<p>I am helpful!</p>' );
1676  *     }
1677  *     OO.inheritClass( HelpTool, OO.ui.PopupTool );
1678  *     HelpTool.static.name = 'help';
1679  *     HelpTool.static.icon = 'help';
1680  *     HelpTool.static.title = 'Help';
1681  *     toolFactory.register( HelpTool );
1683  *     // Finally define which tools and in what order appear in the toolbar. Each tool may only be
1684  *     // used once (but not all defined tools must be used).
1685  *     toolbar.setup( [
1686  *         {
1687  *             // 'bar' tool groups display tools by icon only
1688  *             type: 'bar',
1689  *             include: [ 'search', 'help' ]
1690  *         }
1691  *     ] );
1693  *     // Create some UI around the toolbar and place it in the document
1694  *     var frame = new OO.ui.PanelLayout( {
1695  *         expanded: false,
1696  *         framed: true
1697  *     } );
1698  *     var contentFrame = new OO.ui.PanelLayout( {
1699  *         expanded: false,
1700  *         padded: true
1701  *     } );
1702  *     frame.$element.append(
1703  *         toolbar.$element,
1704  *         contentFrame.$element.append( $area )
1705  *     );
1706  *     $( 'body' ).append( frame.$element );
1708  *     // Here is where the toolbar is actually built. This must be done after inserting it into the
1709  *     // document.
1710  *     toolbar.initialize();
1712  * For more information about how to add tools to a bar tool group, please see {@link OO.ui.ToolGroup toolgroup}.
1713  * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
1715  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
1717  * @class
1718  * @extends OO.ui.ToolGroup
1720  * @constructor
1721  * @param {OO.ui.Toolbar} toolbar
1722  * @param {Object} [config] Configuration options
1723  */
1724 OO.ui.BarToolGroup = function OoUiBarToolGroup( toolbar, config ) {
1725         // Allow passing positional parameters inside the config object
1726         if ( OO.isPlainObject( toolbar ) && config === undefined ) {
1727                 config = toolbar;
1728                 toolbar = config.toolbar;
1729         }
1731         // Parent constructor
1732         OO.ui.BarToolGroup.parent.call( this, toolbar, config );
1734         // Initialization
1735         this.$element.addClass( 'oo-ui-barToolGroup' );
1738 /* Setup */
1740 OO.inheritClass( OO.ui.BarToolGroup, OO.ui.ToolGroup );
1742 /* Static Properties */
1744 OO.ui.BarToolGroup.static.titleTooltips = true;
1746 OO.ui.BarToolGroup.static.accelTooltips = true;
1748 OO.ui.BarToolGroup.static.name = 'bar';
1751  * PopupToolGroup is an abstract base class used by both {@link OO.ui.MenuToolGroup MenuToolGroup}
1752  * and {@link OO.ui.ListToolGroup ListToolGroup} to provide a popup--an overlaid menu or list of tools with an
1753  * optional icon and label. This class can be used for other base classes that also use this functionality.
1755  * @abstract
1756  * @class
1757  * @extends OO.ui.ToolGroup
1758  * @mixins OO.ui.mixin.IconElement
1759  * @mixins OO.ui.mixin.IndicatorElement
1760  * @mixins OO.ui.mixin.LabelElement
1761  * @mixins OO.ui.mixin.TitledElement
1762  * @mixins OO.ui.mixin.ClippableElement
1763  * @mixins OO.ui.mixin.TabIndexedElement
1765  * @constructor
1766  * @param {OO.ui.Toolbar} toolbar
1767  * @param {Object} [config] Configuration options
1768  * @cfg {string} [header] Text to display at the top of the popup
1769  */
1770 OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
1771         // Allow passing positional parameters inside the config object
1772         if ( OO.isPlainObject( toolbar ) && config === undefined ) {
1773                 config = toolbar;
1774                 toolbar = config.toolbar;
1775         }
1777         // Configuration initialization
1778         config = config || {};
1780         // Parent constructor
1781         OO.ui.PopupToolGroup.parent.call( this, toolbar, config );
1783         // Properties
1784         this.active = false;
1785         this.dragging = false;
1786         this.onBlurHandler = this.onBlur.bind( this );
1787         this.$handle = $( '<span>' );
1789         // Mixin constructors
1790         OO.ui.mixin.IconElement.call( this, config );
1791         OO.ui.mixin.IndicatorElement.call( this, config );
1792         OO.ui.mixin.LabelElement.call( this, config );
1793         OO.ui.mixin.TitledElement.call( this, config );
1794         OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
1795         OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
1797         // Events
1798         this.$handle.on( {
1799                 keydown: this.onHandleMouseKeyDown.bind( this ),
1800                 keyup: this.onHandleMouseKeyUp.bind( this ),
1801                 mousedown: this.onHandleMouseKeyDown.bind( this ),
1802                 mouseup: this.onHandleMouseKeyUp.bind( this )
1803         } );
1805         // Initialization
1806         this.$handle
1807                 .addClass( 'oo-ui-popupToolGroup-handle' )
1808                 .append( this.$icon, this.$label, this.$indicator );
1809         // If the pop-up should have a header, add it to the top of the toolGroup.
1810         // Note: If this feature is useful for other widgets, we could abstract it into an
1811         // OO.ui.HeaderedElement mixin constructor.
1812         if ( config.header !== undefined ) {
1813                 this.$group
1814                         .prepend( $( '<span>' )
1815                                 .addClass( 'oo-ui-popupToolGroup-header' )
1816                                 .text( config.header )
1817                         );
1818         }
1819         this.$element
1820                 .addClass( 'oo-ui-popupToolGroup' )
1821                 .prepend( this.$handle );
1824 /* Setup */
1826 OO.inheritClass( OO.ui.PopupToolGroup, OO.ui.ToolGroup );
1827 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.IconElement );
1828 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.IndicatorElement );
1829 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.LabelElement );
1830 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.TitledElement );
1831 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.ClippableElement );
1832 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.TabIndexedElement );
1834 /* Methods */
1837  * @inheritdoc
1838  */
1839 OO.ui.PopupToolGroup.prototype.setDisabled = function () {
1840         // Parent method
1841         OO.ui.PopupToolGroup.parent.prototype.setDisabled.apply( this, arguments );
1843         if ( this.isDisabled() && this.isElementAttached() ) {
1844                 this.setActive( false );
1845         }
1849  * Handle focus being lost.
1851  * The event is actually generated from a mouseup/keyup, so it is not a normal blur event object.
1853  * @protected
1854  * @param {MouseEvent|KeyboardEvent} e Mouse up or key up event
1855  */
1856 OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) {
1857         // Only deactivate when clicking outside the dropdown element
1858         if ( $( e.target ).closest( '.oo-ui-popupToolGroup' )[ 0 ] !== this.$element[ 0 ] ) {
1859                 this.setActive( false );
1860         }
1864  * @inheritdoc
1865  */
1866 OO.ui.PopupToolGroup.prototype.onMouseKeyUp = function ( e ) {
1867         // Only close toolgroup when a tool was actually selected
1868         if (
1869                 !this.isDisabled() && this.pressed && this.pressed === this.getTargetTool( e ) &&
1870                 ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
1871         ) {
1872                 this.setActive( false );
1873         }
1874         return OO.ui.PopupToolGroup.parent.prototype.onMouseKeyUp.call( this, e );
1878  * Handle mouse up and key up events.
1880  * @protected
1881  * @param {jQuery.Event} e Mouse up or key up event
1882  */
1883 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyUp = function ( e ) {
1884         if (
1885                 !this.isDisabled() &&
1886                 ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
1887         ) {
1888                 return false;
1889         }
1893  * Handle mouse down and key down events.
1895  * @protected
1896  * @param {jQuery.Event} e Mouse down or key down event
1897  */
1898 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyDown = function ( e ) {
1899         if (
1900                 !this.isDisabled() &&
1901                 ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
1902         ) {
1903                 this.setActive( !this.active );
1904                 return false;
1905         }
1909  * Switch into 'active' mode.
1911  * When active, the popup is visible. A mouseup event anywhere in the document will trigger
1912  * deactivation.
1914  * @param {boolean} value The active state to set
1915  */
1916 OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
1917         var containerWidth, containerLeft;
1918         value = !!value;
1919         if ( this.active !== value ) {
1920                 this.active = value;
1921                 if ( value ) {
1922                         this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler, true );
1923                         this.getElementDocument().addEventListener( 'keyup', this.onBlurHandler, true );
1925                         this.$clippable.css( 'left', '' );
1926                         // Try anchoring the popup to the left first
1927                         this.$element.addClass( 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left' );
1928                         this.toggleClipping( true );
1929                         if ( this.isClippedHorizontally() ) {
1930                                 // Anchoring to the left caused the popup to clip, so anchor it to the right instead
1931                                 this.toggleClipping( false );
1932                                 this.$element
1933                                         .removeClass( 'oo-ui-popupToolGroup-left' )
1934                                         .addClass( 'oo-ui-popupToolGroup-right' );
1935                                 this.toggleClipping( true );
1936                         }
1937                         if ( this.isClippedHorizontally() ) {
1938                                 // Anchoring to the right also caused the popup to clip, so just make it fill the container
1939                                 containerWidth = this.$clippableScrollableContainer.width();
1940                                 containerLeft = this.$clippableScrollableContainer.offset().left;
1942                                 this.toggleClipping( false );
1943                                 this.$element.removeClass( 'oo-ui-popupToolGroup-right' );
1945                                 this.$clippable.css( {
1946                                         left: -( this.$element.offset().left - containerLeft ),
1947                                         width: containerWidth
1948                                 } );
1949                         }
1950                 } else {
1951                         this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler, true );
1952                         this.getElementDocument().removeEventListener( 'keyup', this.onBlurHandler, true );
1953                         this.$element.removeClass(
1954                                 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left  oo-ui-popupToolGroup-right'
1955                         );
1956                         this.toggleClipping( false );
1957                 }
1958         }
1962  * ListToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
1963  * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.MenuToolGroup MenuToolGroup}
1964  * and {@link OO.ui.BarToolGroup BarToolGroup}). The {@link OO.ui.Tool tools} in a ListToolGroup are displayed
1965  * by label in a dropdown menu. The title of the tool is used as the label text. The menu itself can be configured
1966  * with a label, icon, indicator, header, and title.
1968  * ListToolGroups can be configured to be expanded and collapsed. Collapsed lists will have a ‘More’ option that
1969  * users can select to see the full list of tools. If a collapsed toolgroup is expanded, a ‘Fewer’ option permits
1970  * users to collapse the list again.
1972  * ListToolGroups are created by a {@link OO.ui.ToolGroupFactory toolgroup factory} when the toolbar is set up. The factory
1973  * requires the ListToolGroup's symbolic name, 'list', which is specified along with the other configurations. For more
1974  * information about how to add tools to a ListToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
1976  *     @example
1977  *     // Example of a ListToolGroup
1978  *     var toolFactory = new OO.ui.ToolFactory();
1979  *     var toolGroupFactory = new OO.ui.ToolGroupFactory();
1980  *     var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
1982  *     // Configure and register two tools
1983  *     function SettingsTool() {
1984  *         SettingsTool.parent.apply( this, arguments );
1985  *     }
1986  *     OO.inheritClass( SettingsTool, OO.ui.Tool );
1987  *     SettingsTool.static.name = 'settings';
1988  *     SettingsTool.static.icon = 'settings';
1989  *     SettingsTool.static.title = 'Change settings';
1990  *     SettingsTool.prototype.onSelect = function () {
1991  *         this.setActive( false );
1992  *     };
1993  *     SettingsTool.prototype.onUpdateState = function () {};
1994  *     toolFactory.register( SettingsTool );
1995  *     // Register two more tools, nothing interesting here
1996  *     function StuffTool() {
1997  *         StuffTool.parent.apply( this, arguments );
1998  *     }
1999  *     OO.inheritClass( StuffTool, OO.ui.Tool );
2000  *     StuffTool.static.name = 'stuff';
2001  *     StuffTool.static.icon = 'search';
2002  *     StuffTool.static.title = 'Change the world';
2003  *     StuffTool.prototype.onSelect = function () {
2004  *         this.setActive( false );
2005  *     };
2006  *     StuffTool.prototype.onUpdateState = function () {};
2007  *     toolFactory.register( StuffTool );
2008  *     toolbar.setup( [
2009  *         {
2010  *             // Configurations for list toolgroup.
2011  *             type: 'list',
2012  *             label: 'ListToolGroup',
2013  *             indicator: 'down',
2014  *             icon: 'ellipsis',
2015  *             title: 'This is the title, displayed when user moves the mouse over the list toolgroup',
2016  *             header: 'This is the header',
2017  *             include: [ 'settings', 'stuff' ],
2018  *             allowCollapse: ['stuff']
2019  *         }
2020  *     ] );
2022  *     // Create some UI around the toolbar and place it in the document
2023  *     var frame = new OO.ui.PanelLayout( {
2024  *         expanded: false,
2025  *         framed: true
2026  *     } );
2027  *     frame.$element.append(
2028  *         toolbar.$element
2029  *     );
2030  *     $( 'body' ).append( frame.$element );
2031  *     // Build the toolbar. This must be done after the toolbar has been appended to the document.
2032  *     toolbar.initialize();
2034  * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
2036  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
2038  * @class
2039  * @extends OO.ui.PopupToolGroup
2041  * @constructor
2042  * @param {OO.ui.Toolbar} toolbar
2043  * @param {Object} [config] Configuration options
2044  * @cfg {Array} [allowCollapse] Allow the specified tools to be collapsed. By default, collapsible tools
2045  *  will only be displayed if users click the ‘More’ option displayed at the bottom of the list. If
2046  *  the list is expanded, a ‘Fewer’ option permits users to collapse the list again. Any tools that
2047  *  are included in the toolgroup, but are not designated as collapsible, will always be displayed.
2048  *  To open a collapsible list in its expanded state, set #expanded to 'true'.
2049  * @cfg {Array} [forceExpand] Expand the specified tools. All other tools will be designated as collapsible.
2050  *  Unless #expanded is set to true, the collapsible tools will be collapsed when the list is first opened.
2051  * @cfg {boolean} [expanded=false] Expand collapsible tools. This config is only relevant if tools have
2052  *  been designated as collapsible. When expanded is set to true, all tools in the group will be displayed
2053  *  when the list is first opened. Users can collapse the list with a ‘Fewer’ option at the bottom.
2054  */
2055 OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
2056         // Allow passing positional parameters inside the config object
2057         if ( OO.isPlainObject( toolbar ) && config === undefined ) {
2058                 config = toolbar;
2059                 toolbar = config.toolbar;
2060         }
2062         // Configuration initialization
2063         config = config || {};
2065         // Properties (must be set before parent constructor, which calls #populate)
2066         this.allowCollapse = config.allowCollapse;
2067         this.forceExpand = config.forceExpand;
2068         this.expanded = config.expanded !== undefined ? config.expanded : false;
2069         this.collapsibleTools = [];
2071         // Parent constructor
2072         OO.ui.ListToolGroup.parent.call( this, toolbar, config );
2074         // Initialization
2075         this.$element.addClass( 'oo-ui-listToolGroup' );
2078 /* Setup */
2080 OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup );
2082 /* Static Properties */
2084 OO.ui.ListToolGroup.static.name = 'list';
2086 /* Methods */
2089  * @inheritdoc
2090  */
2091 OO.ui.ListToolGroup.prototype.populate = function () {
2092         var i, len, allowCollapse = [];
2094         OO.ui.ListToolGroup.parent.prototype.populate.call( this );
2096         // Update the list of collapsible tools
2097         if ( this.allowCollapse !== undefined ) {
2098                 allowCollapse = this.allowCollapse;
2099         } else if ( this.forceExpand !== undefined ) {
2100                 allowCollapse = OO.simpleArrayDifference( Object.keys( this.tools ), this.forceExpand );
2101         }
2103         this.collapsibleTools = [];
2104         for ( i = 0, len = allowCollapse.length; i < len; i++ ) {
2105                 if ( this.tools[ allowCollapse[ i ] ] !== undefined ) {
2106                         this.collapsibleTools.push( this.tools[ allowCollapse[ i ] ] );
2107                 }
2108         }
2110         // Keep at the end, even when tools are added
2111         this.$group.append( this.getExpandCollapseTool().$element );
2113         this.getExpandCollapseTool().toggle( this.collapsibleTools.length !== 0 );
2114         this.updateCollapsibleState();
2118  * Get the expand/collapse tool for this group
2120  * @return {OO.ui.Tool} Expand collapse tool
2121  */
2122 OO.ui.ListToolGroup.prototype.getExpandCollapseTool = function () {
2123         var ExpandCollapseTool;
2124         if ( this.expandCollapseTool === undefined ) {
2125                 ExpandCollapseTool = function () {
2126                         ExpandCollapseTool.parent.apply( this, arguments );
2127                 };
2129                 OO.inheritClass( ExpandCollapseTool, OO.ui.Tool );
2131                 ExpandCollapseTool.prototype.onSelect = function () {
2132                         this.toolGroup.expanded = !this.toolGroup.expanded;
2133                         this.toolGroup.updateCollapsibleState();
2134                         this.setActive( false );
2135                 };
2136                 ExpandCollapseTool.prototype.onUpdateState = function () {
2137                         // Do nothing. Tool interface requires an implementation of this function.
2138                 };
2140                 ExpandCollapseTool.static.name = 'more-fewer';
2142                 this.expandCollapseTool = new ExpandCollapseTool( this );
2143         }
2144         return this.expandCollapseTool;
2148  * @inheritdoc
2149  */
2150 OO.ui.ListToolGroup.prototype.onMouseKeyUp = function ( e ) {
2151         // Do not close the popup when the user wants to show more/fewer tools
2152         if (
2153                 $( e.target ).closest( '.oo-ui-tool-name-more-fewer' ).length &&
2154                 ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
2155         ) {
2156                 // HACK: Prevent the popup list from being hidden. Skip the PopupToolGroup implementation (which
2157                 // hides the popup list when a tool is selected) and call ToolGroup's implementation directly.
2158                 return OO.ui.ListToolGroup.parent.parent.prototype.onMouseKeyUp.call( this, e );
2159         } else {
2160                 return OO.ui.ListToolGroup.parent.prototype.onMouseKeyUp.call( this, e );
2161         }
2164 OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () {
2165         var i, len;
2167         this.getExpandCollapseTool()
2168                 .setIcon( this.expanded ? 'collapse' : 'expand' )
2169                 .setTitle( OO.ui.msg( this.expanded ? 'ooui-toolgroup-collapse' : 'ooui-toolgroup-expand' ) );
2171         for ( i = 0, len = this.collapsibleTools.length; i < len; i++ ) {
2172                 this.collapsibleTools[ i ].toggle( this.expanded );
2173         }
2177  * MenuToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
2178  * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.BarToolGroup BarToolGroup}
2179  * and {@link OO.ui.ListToolGroup ListToolGroup}). MenuToolGroups contain selectable {@link OO.ui.Tool tools},
2180  * which are displayed by label in a dropdown menu. The tool's title is used as the label text, and the
2181  * menu label is updated to reflect which tool or tools are currently selected. If no tools are selected,
2182  * the menu label is empty. The menu can be configured with an indicator, icon, title, and/or header.
2184  * MenuToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar
2185  * is set up.
2187  *     @example
2188  *     // Example of a MenuToolGroup
2189  *     var toolFactory = new OO.ui.ToolFactory();
2190  *     var toolGroupFactory = new OO.ui.ToolGroupFactory();
2191  *     var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
2193  *     // We will be placing status text in this element when tools are used
2194  *     var $area = $( '<p>' ).text( 'An example of a MenuToolGroup. Select a tool from the dropdown menu.' );
2196  *     // Define the tools that we're going to place in our toolbar
2198  *     function SettingsTool() {
2199  *         SettingsTool.parent.apply( this, arguments );
2200  *         this.reallyActive = false;
2201  *     }
2202  *     OO.inheritClass( SettingsTool, OO.ui.Tool );
2203  *     SettingsTool.static.name = 'settings';
2204  *     SettingsTool.static.icon = 'settings';
2205  *     SettingsTool.static.title = 'Change settings';
2206  *     SettingsTool.prototype.onSelect = function () {
2207  *         $area.text( 'Settings tool clicked!' );
2208  *         // Toggle the active state on each click
2209  *         this.reallyActive = !this.reallyActive;
2210  *         this.setActive( this.reallyActive );
2211  *         // To update the menu label
2212  *         this.toolbar.emit( 'updateState' );
2213  *     };
2214  *     SettingsTool.prototype.onUpdateState = function () {};
2215  *     toolFactory.register( SettingsTool );
2217  *     function StuffTool() {
2218  *         StuffTool.parent.apply( this, arguments );
2219  *         this.reallyActive = false;
2220  *     }
2221  *     OO.inheritClass( StuffTool, OO.ui.Tool );
2222  *     StuffTool.static.name = 'stuff';
2223  *     StuffTool.static.icon = 'ellipsis';
2224  *     StuffTool.static.title = 'More stuff';
2225  *     StuffTool.prototype.onSelect = function () {
2226  *         $area.text( 'More stuff tool clicked!' );
2227  *         // Toggle the active state on each click
2228  *         this.reallyActive = !this.reallyActive;
2229  *         this.setActive( this.reallyActive );
2230  *         // To update the menu label
2231  *         this.toolbar.emit( 'updateState' );
2232  *     };
2233  *     StuffTool.prototype.onUpdateState = function () {};
2234  *     toolFactory.register( StuffTool );
2236  *     // Finally define which tools and in what order appear in the toolbar. Each tool may only be
2237  *     // used once (but not all defined tools must be used).
2238  *     toolbar.setup( [
2239  *         {
2240  *             type: 'menu',
2241  *             header: 'This is the (optional) header',
2242  *             title: 'This is the (optional) title',
2243  *             indicator: 'down',
2244  *             include: [ 'settings', 'stuff' ]
2245  *         }
2246  *     ] );
2248  *     // Create some UI around the toolbar and place it in the document
2249  *     var frame = new OO.ui.PanelLayout( {
2250  *         expanded: false,
2251  *         framed: true
2252  *     } );
2253  *     var contentFrame = new OO.ui.PanelLayout( {
2254  *         expanded: false,
2255  *         padded: true
2256  *     } );
2257  *     frame.$element.append(
2258  *         toolbar.$element,
2259  *         contentFrame.$element.append( $area )
2260  *     );
2261  *     $( 'body' ).append( frame.$element );
2263  *     // Here is where the toolbar is actually built. This must be done after inserting it into the
2264  *     // document.
2265  *     toolbar.initialize();
2266  *     toolbar.emit( 'updateState' );
2268  * For more information about how to add tools to a MenuToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
2269  * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki] [1].
2271  * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
2273  * @class
2274  * @extends OO.ui.PopupToolGroup
2276  * @constructor
2277  * @param {OO.ui.Toolbar} toolbar
2278  * @param {Object} [config] Configuration options
2279  */
2280 OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
2281         // Allow passing positional parameters inside the config object
2282         if ( OO.isPlainObject( toolbar ) && config === undefined ) {
2283                 config = toolbar;
2284                 toolbar = config.toolbar;
2285         }
2287         // Configuration initialization
2288         config = config || {};
2290         // Parent constructor
2291         OO.ui.MenuToolGroup.parent.call( this, toolbar, config );
2293         // Events
2294         this.toolbar.connect( this, { updateState: 'onUpdateState' } );
2296         // Initialization
2297         this.$element.addClass( 'oo-ui-menuToolGroup' );
2300 /* Setup */
2302 OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup );
2304 /* Static Properties */
2306 OO.ui.MenuToolGroup.static.name = 'menu';
2308 /* Methods */
2311  * Handle the toolbar state being updated.
2313  * When the state changes, the title of each active item in the menu will be joined together and
2314  * used as a label for the group. The label will be empty if none of the items are active.
2316  * @private
2317  */
2318 OO.ui.MenuToolGroup.prototype.onUpdateState = function () {
2319         var name,
2320                 labelTexts = [];
2322         for ( name in this.tools ) {
2323                 if ( this.tools[ name ].isActive() ) {
2324                         labelTexts.push( this.tools[ name ].getTitle() );
2325                 }
2326         }
2328         this.setLabel( labelTexts.join( ', ' ) || ' ' );
2331 }( OO ) );
2333 //# sourceMappingURL=oojs-ui-toolbars.js.map