22fbdbbe6cc0a108441be39ff44d2e81ec9434b0
[lhc/web/wiklou.git] / resources / lib / ooui / oojs-ui-toolbars.js
1 /*!
2 * OOUI v0.29.3
3 * https://www.mediawiki.org/wiki/OOUI
4 *
5 * Copyright 2011–2018 OOUI Team and other contributors.
6 * Released under the MIT license
7 * http://oojs.mit-license.org
8 *
9 * Date: 2018-11-01T02:03:33Z
10 */
11 ( function ( OO ) {
12
13 'use strict';
14
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 );
115 *
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 * ] );
135 *
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 );
150 *
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' );
155 *
156 * The following example extends the previous one to illustrate 'menu' toolgroups and the usage of
157 * {@link #event-updateState 'updateState' event}.
158 *
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 );
164 *
165 * // We will be placing status text in this element when tools are used
166 * var $area = $( '<p>' ).text( 'Toolbar example' );
167 *
168 * // Define the tools that we're going to place in our toolbar
169 *
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 );
189 *
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 );
209 *
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 );
229 *
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 );
245 *
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 * ] );
262 *
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 );
277 *
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' );
282 *
283 * @class
284 * @extends OO.ui.Element
285 * @mixins OO.EventEmitter
286 * @mixins OO.ui.mixin.GroupElement
287 *
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 {string} [position='top'] Whether the toolbar is positioned above ('top') or below ('bottom') content.
296 * @cfg {jQuery} [$overlay] An overlay for the popup.
297 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
298 */
299 OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
300 // Allow passing positional parameters inside the config object
301 if ( OO.isPlainObject( toolFactory ) && config === undefined ) {
302 config = toolFactory;
303 toolFactory = config.toolFactory;
304 toolGroupFactory = config.toolGroupFactory;
305 }
306
307 // Configuration initialization
308 config = config || {};
309
310 // Parent constructor
311 OO.ui.Toolbar.parent.call( this, config );
312
313 // Mixin constructors
314 OO.EventEmitter.call( this );
315 OO.ui.mixin.GroupElement.call( this, config );
316
317 // Properties
318 this.toolFactory = toolFactory;
319 this.toolGroupFactory = toolGroupFactory;
320 this.groupsByName = {};
321 this.activeToolGroups = 0;
322 this.tools = {};
323 this.position = config.position || 'top';
324 this.$bar = $( '<div>' );
325 this.$actions = $( '<div>' );
326 this.$popups = $( '<div>' );
327 this.initialized = false;
328 this.narrowThreshold = null;
329 this.onWindowResizeHandler = this.onWindowResize.bind( this );
330 this.$overlay = ( config.$overlay === true ? OO.ui.getDefaultOverlay() : config.$overlay ) || this.$element;
331
332 // Events
333 this.$element
334 .add( this.$bar ).add( this.$group ).add( this.$actions )
335 .on( 'mousedown keydown', this.onPointerDown.bind( this ) );
336
337 // Initialization
338 this.$group.addClass( 'oo-ui-toolbar-tools' );
339 if ( config.actions ) {
340 this.$bar.append( this.$actions.addClass( 'oo-ui-toolbar-actions' ) );
341 }
342 this.$popups.addClass( 'oo-ui-toolbar-popups' );
343 this.$bar
344 .addClass( 'oo-ui-toolbar-bar' )
345 .append( this.$group, '<div style="clear:both"></div>' );
346 // Possible classes: oo-ui-toolbar-position-top, oo-ui-toolbar-position-bottom
347 this.$element.addClass( 'oo-ui-toolbar oo-ui-toolbar-position-' + this.position ).append( this.$bar );
348 this.$overlay.append( this.$popups );
349 };
350
351 /* Setup */
352
353 OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
354 OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
355 OO.mixinClass( OO.ui.Toolbar, OO.ui.mixin.GroupElement );
356
357 /* Events */
358
359 /**
360 * @event updateState
361 *
362 * An 'updateState' event must be emitted on the Toolbar (by calling `toolbar.emit( 'updateState' )`)
363 * every time the state of the application using the toolbar changes, and an update to the state of
364 * tools is required.
365 *
366 * @param {...Mixed} data Application-defined parameters
367 */
368
369 /**
370 * @event active
371 *
372 * An 'active' event is emitted when the number of active toolgroups increases from 0, or
373 * returns to 0.
374 *
375 * @param {boolean} There are active toolgroups in this toolbar
376 */
377
378 /* Methods */
379
380 /**
381 * Get the tool factory.
382 *
383 * @return {OO.ui.ToolFactory} Tool factory
384 */
385 OO.ui.Toolbar.prototype.getToolFactory = function () {
386 return this.toolFactory;
387 };
388
389 /**
390 * Get the toolgroup factory.
391 *
392 * @return {OO.Factory} Toolgroup factory
393 */
394 OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
395 return this.toolGroupFactory;
396 };
397
398 /**
399 * Handles mouse down events.
400 *
401 * @private
402 * @param {jQuery.Event} e Mouse down event
403 */
404 OO.ui.Toolbar.prototype.onPointerDown = function ( e ) {
405 var $closestWidgetToEvent = $( e.target ).closest( '.oo-ui-widget' ),
406 $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
407 if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[ 0 ] === $closestWidgetToToolbar[ 0 ] ) {
408 return false;
409 }
410 };
411
412 /**
413 * Handle window resize event.
414 *
415 * @private
416 * @param {jQuery.Event} e Window resize event
417 */
418 OO.ui.Toolbar.prototype.onWindowResize = function () {
419 this.$element.add( this.$popups ).toggleClass(
420 'oo-ui-toolbar-narrow',
421 this.$bar[ 0 ].clientWidth <= this.getNarrowThreshold()
422 );
423 };
424
425 /**
426 * Get the (lazily-computed) width threshold for applying the oo-ui-toolbar-narrow
427 * class.
428 *
429 * @private
430 * @return {number} Width threshold in pixels
431 */
432 OO.ui.Toolbar.prototype.getNarrowThreshold = function () {
433 if ( this.narrowThreshold === null ) {
434 this.narrowThreshold = this.$group[ 0 ].offsetWidth + this.$actions[ 0 ].offsetWidth;
435 }
436 return this.narrowThreshold;
437 };
438
439 /**
440 * Sets up handles and preloads required information for the toolbar to work.
441 * This must be called after it is attached to a visible document and before doing anything else.
442 */
443 OO.ui.Toolbar.prototype.initialize = function () {
444 if ( !this.initialized ) {
445 this.initialized = true;
446 $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
447 this.onWindowResize();
448 }
449 };
450
451 /**
452 * Set up the toolbar.
453 *
454 * The toolbar is set up with a list of toolgroup configurations that specify the type of
455 * toolgroup ({@link OO.ui.BarToolGroup bar}, {@link OO.ui.MenuToolGroup menu}, or {@link OO.ui.ListToolGroup list})
456 * to add and which tools to include, exclude, promote, or demote within that toolgroup. Please
457 * see {@link OO.ui.ToolGroup toolgroups} for more information about including tools in toolgroups.
458 *
459 * @param {Object.<string,Array>} groups List of toolgroup configurations
460 * @param {string} [groups.name] Symbolic name for this toolgroup
461 * @param {string} [groups.type] Toolgroup type, should exist in the toolgroup factory
462 * @param {Array|string} [groups.include] Tools to include in the toolgroup
463 * @param {Array|string} [groups.exclude] Tools to exclude from the toolgroup
464 * @param {Array|string} [groups.promote] Tools to promote to the beginning of the toolgroup
465 * @param {Array|string} [groups.demote] Tools to demote to the end of the toolgroup
466 */
467 OO.ui.Toolbar.prototype.setup = function ( groups ) {
468 var i, len, type, toolGroup, groupConfig,
469 items = [],
470 defaultType = 'bar';
471
472 // Cleanup previous groups
473 this.reset();
474
475 // Build out new groups
476 for ( i = 0, len = groups.length; i < len; i++ ) {
477 groupConfig = groups[ i ];
478 if ( groupConfig.include === '*' ) {
479 // Apply defaults to catch-all groups
480 if ( groupConfig.type === undefined ) {
481 groupConfig.type = 'list';
482 }
483 if ( groupConfig.label === undefined ) {
484 groupConfig.label = OO.ui.msg( 'ooui-toolbar-more' );
485 }
486 }
487 // Check type has been registered
488 type = this.getToolGroupFactory().lookup( groupConfig.type ) ? groupConfig.type : defaultType;
489 toolGroup = this.getToolGroupFactory().create( type, this, groupConfig );
490 items.push( toolGroup );
491 if ( groupConfig.name ) {
492 this.groupsByName[ groupConfig.name ] = toolGroup;
493 } else {
494 // Groups without name are deprecated
495 OO.ui.warnDeprecation( 'Toolgroups must have a \'name\' property' );
496 }
497 toolGroup.connect( this, { active: 'onToolGroupActive' } );
498 }
499 this.addItems( items );
500 };
501
502 /**
503 * Handle active events from tool groups
504 *
505 * @param {boolean} active Tool group has become active, inactive if false
506 * @fires active
507 */
508 OO.ui.Toolbar.prototype.onToolGroupActive = function ( active ) {
509 if ( active ) {
510 this.activeToolGroups++;
511 if ( this.activeToolGroups === 1 ) {
512 this.emit( 'active', true );
513 }
514 } else {
515 this.activeToolGroups--;
516 if ( this.activeToolGroups === 0 ) {
517 this.emit( 'active', false );
518 }
519 }
520 };
521
522 /**
523 * Get a toolgroup by name
524 *
525 * @param {string} name Group name
526 * @return {OO.ui.ToolGroup|null} Tool group, or null if none found by that name
527 */
528 OO.ui.Toolbar.prototype.getToolGroupByName = function ( name ) {
529 return this.groupsByName[ name ] || null;
530 };
531
532 /**
533 * Remove all tools and toolgroups from the toolbar.
534 */
535 OO.ui.Toolbar.prototype.reset = function () {
536 var i, len;
537
538 this.groupsByName = {};
539 this.tools = {};
540 for ( i = 0, len = this.items.length; i < len; i++ ) {
541 this.items[ i ].destroy();
542 }
543 this.clearItems();
544 };
545
546 /**
547 * Destroy the toolbar.
548 *
549 * Destroying the toolbar removes all event handlers and DOM elements that constitute the toolbar. Call
550 * this method whenever you are done using a toolbar.
551 */
552 OO.ui.Toolbar.prototype.destroy = function () {
553 $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
554 this.reset();
555 this.$element.remove();
556 };
557
558 /**
559 * Check if the tool is available.
560 *
561 * Available tools are ones that have not yet been added to the toolbar.
562 *
563 * @param {string} name Symbolic name of tool
564 * @return {boolean} Tool is available
565 */
566 OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
567 return !this.tools[ name ];
568 };
569
570 /**
571 * Prevent tool from being used again.
572 *
573 * @param {OO.ui.Tool} tool Tool to reserve
574 */
575 OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
576 this.tools[ tool.getName() ] = tool;
577 };
578
579 /**
580 * Allow tool to be used again.
581 *
582 * @param {OO.ui.Tool} tool Tool to release
583 */
584 OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
585 delete this.tools[ tool.getName() ];
586 };
587
588 /**
589 * Get accelerator label for tool.
590 *
591 * The OOUI library does not contain an accelerator system, but this is the hook for one. To
592 * use an accelerator system, subclass the toolbar and override this method, which is meant to return a label
593 * that describes the accelerator keys for the tool passed (by symbolic name) to the method.
594 *
595 * @param {string} name Symbolic name of tool
596 * @return {string|undefined} Tool accelerator label if available
597 */
598 OO.ui.Toolbar.prototype.getToolAccelerator = function () {
599 return undefined;
600 };
601
602 /**
603 * Tools, together with {@link OO.ui.ToolGroup toolgroups}, constitute {@link OO.ui.Toolbar toolbars}.
604 * Each tool is configured with a static name, title, and icon and is customized with the command to carry
605 * out when the tool is selected. Tools must also be registered with a {@link OO.ui.ToolFactory tool factory},
606 * which creates the tools on demand.
607 *
608 * Every Tool subclass must implement two methods:
609 *
610 * - {@link #onUpdateState}
611 * - {@link #onSelect}
612 *
613 * Tools are added to toolgroups ({@link OO.ui.ListToolGroup ListToolGroup},
614 * {@link OO.ui.BarToolGroup BarToolGroup}, or {@link OO.ui.MenuToolGroup MenuToolGroup}), which determine how
615 * the tool is displayed in the toolbar. See {@link OO.ui.Toolbar toolbars} for an example.
616 *
617 * For more information, please see the [OOUI documentation on MediaWiki][1].
618 * [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars
619 *
620 * @abstract
621 * @class
622 * @extends OO.ui.Widget
623 * @mixins OO.ui.mixin.IconElement
624 * @mixins OO.ui.mixin.FlaggedElement
625 * @mixins OO.ui.mixin.TabIndexedElement
626 *
627 * @constructor
628 * @param {OO.ui.ToolGroup} toolGroup
629 * @param {Object} [config] Configuration options
630 * @cfg {string|Function} [title] Title text or a function that returns text. If this config is omitted, the value of
631 * the {@link #static-title static title} property is used.
632 *
633 * The title is used in different ways depending on the type of toolgroup that contains the tool. The
634 * 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
635 * part of a {@link OO.ui.ListToolGroup list} or {@link OO.ui.MenuToolGroup menu} toolgroup.
636 *
637 * For bar toolgroups, a description of the accelerator key is appended to the title if an accelerator key
638 * is associated with an action by the same name as the tool and accelerator functionality has been added to the application.
639 * To add accelerator key functionality, you must subclass OO.ui.Toolbar and override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method.
640 */
641 OO.ui.Tool = function OoUiTool( toolGroup, config ) {
642 // Allow passing positional parameters inside the config object
643 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
644 config = toolGroup;
645 toolGroup = config.toolGroup;
646 }
647
648 // Configuration initialization
649 config = config || {};
650
651 // Parent constructor
652 OO.ui.Tool.parent.call( this, config );
653
654 // Properties
655 this.toolGroup = toolGroup;
656 this.toolbar = this.toolGroup.getToolbar();
657 this.active = false;
658 this.$title = $( '<span>' );
659 this.$accel = $( '<span>' );
660 this.$link = $( '<a>' );
661 this.title = null;
662 this.checkIcon = new OO.ui.IconWidget( {
663 icon: 'check',
664 classes: [ 'oo-ui-tool-checkIcon' ]
665 } );
666
667 // Mixin constructors
668 OO.ui.mixin.IconElement.call( this, config );
669 OO.ui.mixin.FlaggedElement.call( this, config );
670 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$link } ) );
671
672 // Events
673 this.toolbar.connect( this, { updateState: 'onUpdateState' } );
674
675 // Initialization
676 this.$title.addClass( 'oo-ui-tool-title' );
677 this.$accel
678 .addClass( 'oo-ui-tool-accel' )
679 .prop( {
680 // This may need to be changed if the key names are ever localized,
681 // but for now they are essentially written in English
682 dir: 'ltr',
683 lang: 'en'
684 } );
685 this.$link
686 .addClass( 'oo-ui-tool-link' )
687 .append( this.checkIcon.$element, this.$icon, this.$title, this.$accel )
688 .attr( 'role', 'button' );
689 this.$element
690 .data( 'oo-ui-tool', this )
691 .addClass( 'oo-ui-tool' )
692 .addClass( 'oo-ui-tool-name-' + this.constructor.static.name.replace( /^([^/]+)\/([^/]+).*$/, '$1-$2' ) )
693 .toggleClass( 'oo-ui-tool-with-label', this.constructor.static.displayBothIconAndLabel )
694 .append( this.$link );
695 this.setTitle( config.title || this.constructor.static.title );
696 };
697
698 /* Setup */
699
700 OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
701 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.IconElement );
702 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.FlaggedElement );
703 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.TabIndexedElement );
704
705 /* Static Properties */
706
707 /**
708 * @static
709 * @inheritdoc
710 */
711 OO.ui.Tool.static.tagName = 'span';
712
713 /**
714 * Symbolic name of tool.
715 *
716 * The symbolic name is used internally to register the tool with a {@link OO.ui.ToolFactory ToolFactory}. It can
717 * also be used when adding tools to toolgroups.
718 *
719 * @abstract
720 * @static
721 * @inheritable
722 * @property {string}
723 */
724 OO.ui.Tool.static.name = '';
725
726 /**
727 * Symbolic name of the group.
728 *
729 * The group name is used to associate tools with each other so that they can be selected later by
730 * a {@link OO.ui.ToolGroup toolgroup}.
731 *
732 * @abstract
733 * @static
734 * @inheritable
735 * @property {string}
736 */
737 OO.ui.Tool.static.group = '';
738
739 /**
740 * 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.
741 *
742 * @abstract
743 * @static
744 * @inheritable
745 * @property {string|Function}
746 */
747 OO.ui.Tool.static.title = '';
748
749 /**
750 * Display both icon and label when the tool is used in a {@link OO.ui.BarToolGroup bar} toolgroup.
751 * Normally only the icon is displayed, or only the label if no icon is given.
752 *
753 * @static
754 * @inheritable
755 * @property {boolean}
756 */
757 OO.ui.Tool.static.displayBothIconAndLabel = false;
758
759 /**
760 * Add tool to catch-all groups automatically.
761 *
762 * A catch-all group, which contains all tools that do not currently belong to a toolgroup,
763 * can be included in a toolgroup using the wildcard selector, an asterisk (*).
764 *
765 * @static
766 * @inheritable
767 * @property {boolean}
768 */
769 OO.ui.Tool.static.autoAddToCatchall = true;
770
771 /**
772 * Add tool to named groups automatically.
773 *
774 * By default, tools that are configured with a static ‘group’ property are added
775 * to that group and will be selected when the symbolic name of the group is specified (e.g., when
776 * toolgroups include tools by group name).
777 *
778 * @static
779 * @property {boolean}
780 * @inheritable
781 */
782 OO.ui.Tool.static.autoAddToGroup = true;
783
784 /**
785 * Check if this tool is compatible with given data.
786 *
787 * This is a stub that can be overridden to provide support for filtering tools based on an
788 * arbitrary piece of information (e.g., where the cursor is in a document). The implementation
789 * must also call this method so that the compatibility check can be performed.
790 *
791 * @static
792 * @inheritable
793 * @param {Mixed} data Data to check
794 * @return {boolean} Tool can be used with data
795 */
796 OO.ui.Tool.static.isCompatibleWith = function () {
797 return false;
798 };
799
800 /* Methods */
801
802 /**
803 * Handle the toolbar state being updated. This method is called when the
804 * {@link OO.ui.Toolbar#event-updateState 'updateState' event} is emitted on the
805 * {@link OO.ui.Toolbar Toolbar} that uses this tool, and should set the state of this tool
806 * depending on application state (usually by calling #setDisabled to enable or disable the tool,
807 * or #setActive to mark is as currently in-use or not).
808 *
809 * This is an abstract method that must be overridden in a concrete subclass.
810 *
811 * @method
812 * @protected
813 * @abstract
814 */
815 OO.ui.Tool.prototype.onUpdateState = null;
816
817 /**
818 * Handle the tool being selected. This method is called when the user triggers this tool,
819 * usually by clicking on its label/icon.
820 *
821 * This is an abstract method that must be overridden in a concrete subclass.
822 *
823 * @method
824 * @protected
825 * @abstract
826 */
827 OO.ui.Tool.prototype.onSelect = null;
828
829 /**
830 * Check if the tool is active.
831 *
832 * Tools become active when their #onSelect or #onUpdateState handlers change them to appear pressed
833 * with the #setActive method. Additional CSS is applied to the tool to reflect the active state.
834 *
835 * @return {boolean} Tool is active
836 */
837 OO.ui.Tool.prototype.isActive = function () {
838 return this.active;
839 };
840
841 /**
842 * Make the tool appear active or inactive.
843 *
844 * This method should be called within #onSelect or #onUpdateState event handlers to make the tool
845 * appear pressed or not.
846 *
847 * @param {boolean} state Make tool appear active
848 */
849 OO.ui.Tool.prototype.setActive = function ( state ) {
850 this.active = !!state;
851 this.$element.toggleClass( 'oo-ui-tool-active', this.active );
852 this.updateThemeClasses();
853 };
854
855 /**
856 * Set the tool #title.
857 *
858 * @param {string|Function} title Title text or a function that returns text
859 * @chainable
860 */
861 OO.ui.Tool.prototype.setTitle = function ( title ) {
862 this.title = OO.ui.resolveMsg( title );
863 this.updateTitle();
864 return this;
865 };
866
867 /**
868 * Get the tool #title.
869 *
870 * @return {string} Title text
871 */
872 OO.ui.Tool.prototype.getTitle = function () {
873 return this.title;
874 };
875
876 /**
877 * Get the tool's symbolic name.
878 *
879 * @return {string} Symbolic name of tool
880 */
881 OO.ui.Tool.prototype.getName = function () {
882 return this.constructor.static.name;
883 };
884
885 /**
886 * Update the title.
887 */
888 OO.ui.Tool.prototype.updateTitle = function () {
889 var titleTooltips = this.toolGroup.constructor.static.titleTooltips,
890 accelTooltips = this.toolGroup.constructor.static.accelTooltips,
891 accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
892 tooltipParts = [];
893
894 this.$title.text( this.title );
895 this.$accel.text( accel );
896
897 if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
898 tooltipParts.push( this.title );
899 }
900 if ( accelTooltips && typeof accel === 'string' && accel.length ) {
901 tooltipParts.push( accel );
902 }
903 if ( tooltipParts.length ) {
904 this.$link.attr( 'title', tooltipParts.join( ' ' ) );
905 } else {
906 this.$link.removeAttr( 'title' );
907 }
908 };
909
910 /**
911 * Destroy tool.
912 *
913 * Destroying the tool removes all event handlers and the tool’s DOM elements.
914 * Call this method whenever you are done using a tool.
915 */
916 OO.ui.Tool.prototype.destroy = function () {
917 this.toolbar.disconnect( this );
918 this.$element.remove();
919 };
920
921 /**
922 * ToolGroups are collections of {@link OO.ui.Tool tools} that are used in a {@link OO.ui.Toolbar toolbar}.
923 * The type of toolgroup ({@link OO.ui.ListToolGroup list}, {@link OO.ui.BarToolGroup bar}, or {@link OO.ui.MenuToolGroup menu})
924 * to which a tool belongs determines how the tool is arranged and displayed in the toolbar. Toolgroups
925 * themselves are created on demand with a {@link OO.ui.ToolGroupFactory toolgroup factory}.
926 *
927 * Toolgroups can contain individual tools, groups of tools, or all available tools, as specified
928 * using the `include` config option. See OO.ui.ToolFactory#extract on documentation of the format.
929 * The options `exclude`, `promote`, and `demote` support the same formats.
930 *
931 * See {@link OO.ui.Toolbar toolbars} for a full example. For more information about toolbars in general,
932 * please see the [OOUI documentation on MediaWiki][1].
933 *
934 * [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars
935 *
936 * @abstract
937 * @class
938 * @extends OO.ui.Widget
939 * @mixins OO.ui.mixin.GroupElement
940 *
941 * @constructor
942 * @param {OO.ui.Toolbar} toolbar
943 * @param {Object} [config] Configuration options
944 * @cfg {Array|string} [include] List of tools to include in the toolgroup, see above.
945 * @cfg {Array|string} [exclude] List of tools to exclude from the toolgroup, see above.
946 * @cfg {Array|string} [promote] List of tools to promote to the beginning of the toolgroup, see above.
947 * @cfg {Array|string} [demote] List of tools to demote to the end of the toolgroup, see above.
948 * This setting is particularly useful when tools have been added to the toolgroup
949 * en masse (e.g., via the catch-all selector).
950 */
951 OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
952 // Allow passing positional parameters inside the config object
953 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
954 config = toolbar;
955 toolbar = config.toolbar;
956 }
957
958 // Configuration initialization
959 config = config || {};
960
961 // Parent constructor
962 OO.ui.ToolGroup.parent.call( this, config );
963
964 // Mixin constructors
965 OO.ui.mixin.GroupElement.call( this, config );
966
967 // Properties
968 this.toolbar = toolbar;
969 this.tools = {};
970 this.pressed = null;
971 this.autoDisabled = false;
972 this.include = config.include || [];
973 this.exclude = config.exclude || [];
974 this.promote = config.promote || [];
975 this.demote = config.demote || [];
976 this.onDocumentMouseKeyUpHandler = this.onDocumentMouseKeyUp.bind( this );
977
978 // Events
979 this.$group.on( {
980 mousedown: this.onMouseKeyDown.bind( this ),
981 mouseup: this.onMouseKeyUp.bind( this ),
982 keydown: this.onMouseKeyDown.bind( this ),
983 keyup: this.onMouseKeyUp.bind( this ),
984 focus: this.onMouseOverFocus.bind( this ),
985 blur: this.onMouseOutBlur.bind( this ),
986 mouseover: this.onMouseOverFocus.bind( this ),
987 mouseout: this.onMouseOutBlur.bind( this )
988 } );
989 this.toolbar.getToolFactory().connect( this, { register: 'onToolFactoryRegister' } );
990 this.aggregate( { disable: 'itemDisable' } );
991 this.connect( this, {
992 itemDisable: 'updateDisabled',
993 disable: 'onDisable'
994 } );
995
996 // Initialization
997 this.$group.addClass( 'oo-ui-toolGroup-tools' );
998 this.$element
999 .addClass( 'oo-ui-toolGroup' )
1000 .append( this.$group );
1001 this.onDisable( this.isDisabled() );
1002 this.populate();
1003 };
1004
1005 /* Setup */
1006
1007 OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget );
1008 OO.mixinClass( OO.ui.ToolGroup, OO.ui.mixin.GroupElement );
1009
1010 /* Events */
1011
1012 /**
1013 * @event update
1014 */
1015
1016 /**
1017 * @event active
1018 *
1019 * An 'active' event is emitted when any popup is shown/hidden.
1020 *
1021 * @param {boolean} The popup is visible
1022 */
1023
1024 /* Static Properties */
1025
1026 /**
1027 * Show labels in tooltips.
1028 *
1029 * @static
1030 * @inheritable
1031 * @property {boolean}
1032 */
1033 OO.ui.ToolGroup.static.titleTooltips = false;
1034
1035 /**
1036 * Show acceleration labels in tooltips.
1037 *
1038 * Note: The OOUI library does not include an accelerator system, but does contain
1039 * a hook for one. To use an accelerator system, subclass the {@link OO.ui.Toolbar toolbar} and
1040 * override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method, which is
1041 * meant to return a label that describes the accelerator keys for a given tool (e.g., 'Ctrl + M').
1042 *
1043 * @static
1044 * @inheritable
1045 * @property {boolean}
1046 */
1047 OO.ui.ToolGroup.static.accelTooltips = false;
1048
1049 /**
1050 * Automatically disable the toolgroup when all tools are disabled
1051 *
1052 * @static
1053 * @inheritable
1054 * @property {boolean}
1055 */
1056 OO.ui.ToolGroup.static.autoDisable = true;
1057
1058 /**
1059 * @abstract
1060 * @static
1061 * @inheritable
1062 * @property {string}
1063 */
1064 OO.ui.ToolGroup.static.name = null;
1065
1066 /* Methods */
1067
1068 /**
1069 * @inheritdoc
1070 */
1071 OO.ui.ToolGroup.prototype.isDisabled = function () {
1072 return this.autoDisabled || OO.ui.ToolGroup.parent.prototype.isDisabled.apply( this, arguments );
1073 };
1074
1075 /**
1076 * @inheritdoc
1077 */
1078 OO.ui.ToolGroup.prototype.updateDisabled = function () {
1079 var i, item, allDisabled = true;
1080
1081 if ( this.constructor.static.autoDisable ) {
1082 for ( i = this.items.length - 1; i >= 0; i-- ) {
1083 item = this.items[ i ];
1084 if ( !item.isDisabled() ) {
1085 allDisabled = false;
1086 break;
1087 }
1088 }
1089 this.autoDisabled = allDisabled;
1090 }
1091 OO.ui.ToolGroup.parent.prototype.updateDisabled.apply( this, arguments );
1092 };
1093
1094 /**
1095 * Handle disable events.
1096 *
1097 * @protected
1098 * @param {boolean} isDisabled
1099 */
1100 OO.ui.ToolGroup.prototype.onDisable = function ( isDisabled ) {
1101 this.$group.toggleClass( 'oo-ui-toolGroup-disabled-tools', isDisabled );
1102 this.$group.toggleClass( 'oo-ui-toolGroup-enabled-tools', !isDisabled );
1103 };
1104
1105 /**
1106 * Handle mouse down and key down events.
1107 *
1108 * @protected
1109 * @param {jQuery.Event} e Mouse down or key down event
1110 */
1111 OO.ui.ToolGroup.prototype.onMouseKeyDown = function ( e ) {
1112 if (
1113 !this.isDisabled() &&
1114 ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
1115 ) {
1116 this.pressed = this.findTargetTool( e );
1117 if ( this.pressed ) {
1118 this.pressed.setActive( true );
1119 this.getElementDocument().addEventListener( 'mouseup', this.onDocumentMouseKeyUpHandler, true );
1120 this.getElementDocument().addEventListener( 'keyup', this.onDocumentMouseKeyUpHandler, true );
1121 return false;
1122 }
1123 }
1124 };
1125
1126 /**
1127 * Handle document mouse up and key up events.
1128 *
1129 * @protected
1130 * @param {MouseEvent|KeyboardEvent} e Mouse up or key up event
1131 */
1132 OO.ui.ToolGroup.prototype.onDocumentMouseKeyUp = function ( e ) {
1133 this.getElementDocument().removeEventListener( 'mouseup', this.onDocumentMouseKeyUpHandler, true );
1134 this.getElementDocument().removeEventListener( 'keyup', this.onDocumentMouseKeyUpHandler, true );
1135 // onMouseKeyUp may be called a second time, depending on where the mouse is when the button is
1136 // released, but since `this.pressed` will no longer be true, the second call will be ignored.
1137 this.onMouseKeyUp( e );
1138 };
1139
1140 // Deprecated alias since 0.28.3
1141 OO.ui.ToolGroup.prototype.onCapturedMouseKeyUp = function () {
1142 OO.ui.warnDeprecation( 'onCapturedMouseKeyUp is deprecated, use onDocumentMouseKeyUp instead' );
1143 this.onDocumentMouseKeyUp.apply( this, arguments );
1144 };
1145
1146 /**
1147 * Handle mouse up and key up events.
1148 *
1149 * @protected
1150 * @param {MouseEvent|KeyboardEvent} e Mouse up or key up event
1151 */
1152 OO.ui.ToolGroup.prototype.onMouseKeyUp = function ( e ) {
1153 var tool = this.findTargetTool( e );
1154
1155 if (
1156 !this.isDisabled() && this.pressed && this.pressed === tool &&
1157 ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
1158 ) {
1159 this.pressed.onSelect();
1160 this.pressed = null;
1161 e.preventDefault();
1162 e.stopPropagation();
1163 }
1164
1165 this.pressed = null;
1166 };
1167
1168 /**
1169 * Handle mouse over and focus events.
1170 *
1171 * @protected
1172 * @param {jQuery.Event} e Mouse over or focus event
1173 */
1174 OO.ui.ToolGroup.prototype.onMouseOverFocus = function ( e ) {
1175 var tool = this.findTargetTool( e );
1176
1177 if ( this.pressed && this.pressed === tool ) {
1178 this.pressed.setActive( true );
1179 }
1180 };
1181
1182 /**
1183 * Handle mouse out and blur events.
1184 *
1185 * @protected
1186 * @param {jQuery.Event} e Mouse out or blur event
1187 */
1188 OO.ui.ToolGroup.prototype.onMouseOutBlur = function ( e ) {
1189 var tool = this.findTargetTool( e );
1190
1191 if ( this.pressed && this.pressed === tool ) {
1192 this.pressed.setActive( false );
1193 }
1194 };
1195
1196 /**
1197 * Get the closest tool to a jQuery.Event.
1198 *
1199 * Only tool links are considered, which prevents other elements in the tool such as popups from
1200 * triggering tool group interactions.
1201 *
1202 * @private
1203 * @param {jQuery.Event} e
1204 * @return {OO.ui.Tool|null} Tool, `null` if none was found
1205 */
1206 OO.ui.ToolGroup.prototype.findTargetTool = function ( e ) {
1207 var tool,
1208 $item = $( e.target ).closest( '.oo-ui-tool-link' );
1209
1210 if ( $item.length ) {
1211 tool = $item.parent().data( 'oo-ui-tool' );
1212 }
1213
1214 return tool && !tool.isDisabled() ? tool : null;
1215 };
1216
1217 /**
1218 * Handle tool registry register events.
1219 *
1220 * If a tool is registered after the group is created, we must repopulate the list to account for:
1221 *
1222 * - a tool being added that may be included
1223 * - a tool already included being overridden
1224 *
1225 * @protected
1226 * @param {string} name Symbolic name of tool
1227 */
1228 OO.ui.ToolGroup.prototype.onToolFactoryRegister = function () {
1229 this.populate();
1230 };
1231
1232 /**
1233 * Get the toolbar that contains the toolgroup.
1234 *
1235 * @return {OO.ui.Toolbar} Toolbar that contains the toolgroup
1236 */
1237 OO.ui.ToolGroup.prototype.getToolbar = function () {
1238 return this.toolbar;
1239 };
1240
1241 /**
1242 * Add and remove tools based on configuration.
1243 */
1244 OO.ui.ToolGroup.prototype.populate = function () {
1245 var i, len, name, tool,
1246 toolFactory = this.toolbar.getToolFactory(),
1247 names = {},
1248 add = [],
1249 remove = [],
1250 list = this.toolbar.getToolFactory().getTools(
1251 this.include, this.exclude, this.promote, this.demote
1252 );
1253
1254 // Build a list of needed tools
1255 for ( i = 0, len = list.length; i < len; i++ ) {
1256 name = list[ i ];
1257 if (
1258 // Tool exists
1259 toolFactory.lookup( name ) &&
1260 // Tool is available or is already in this group
1261 ( this.toolbar.isToolAvailable( name ) || this.tools[ name ] )
1262 ) {
1263 // Hack to prevent infinite recursion via ToolGroupTool. We need to reserve the tool before
1264 // creating it, but we can't call reserveTool() yet because we haven't created the tool.
1265 this.toolbar.tools[ name ] = true;
1266 tool = this.tools[ name ];
1267 if ( !tool ) {
1268 // Auto-initialize tools on first use
1269 this.tools[ name ] = tool = toolFactory.create( name, this );
1270 tool.updateTitle();
1271 }
1272 this.toolbar.reserveTool( tool );
1273 add.push( tool );
1274 names[ name ] = true;
1275 }
1276 }
1277 // Remove tools that are no longer needed
1278 for ( name in this.tools ) {
1279 if ( !names[ name ] ) {
1280 this.tools[ name ].destroy();
1281 this.toolbar.releaseTool( this.tools[ name ] );
1282 remove.push( this.tools[ name ] );
1283 delete this.tools[ name ];
1284 }
1285 }
1286 if ( remove.length ) {
1287 this.removeItems( remove );
1288 }
1289 // Update emptiness state
1290 if ( add.length ) {
1291 this.$element.removeClass( 'oo-ui-toolGroup-empty' );
1292 } else {
1293 this.$element.addClass( 'oo-ui-toolGroup-empty' );
1294 }
1295 // Re-add tools (moving existing ones to new locations)
1296 this.addItems( add );
1297 // Disabled state may depend on items
1298 this.updateDisabled();
1299 };
1300
1301 /**
1302 * Destroy toolgroup.
1303 */
1304 OO.ui.ToolGroup.prototype.destroy = function () {
1305 var name;
1306
1307 this.clearItems();
1308 this.toolbar.getToolFactory().disconnect( this );
1309 for ( name in this.tools ) {
1310 this.toolbar.releaseTool( this.tools[ name ] );
1311 this.tools[ name ].disconnect( this ).destroy();
1312 delete this.tools[ name ];
1313 }
1314 this.$element.remove();
1315 };
1316
1317 /**
1318 * A ToolFactory creates tools on demand. All tools ({@link OO.ui.Tool Tools}, {@link OO.ui.PopupTool PopupTools},
1319 * and {@link OO.ui.ToolGroupTool ToolGroupTools}) must be registered with a tool factory. Tools are
1320 * registered by their symbolic name. See {@link OO.ui.Toolbar toolbars} for an example.
1321 *
1322 * For more information about toolbars in general, please see the [OOUI documentation on MediaWiki][1].
1323 *
1324 * [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars
1325 *
1326 * @class
1327 * @extends OO.Factory
1328 * @constructor
1329 */
1330 OO.ui.ToolFactory = function OoUiToolFactory() {
1331 // Parent constructor
1332 OO.ui.ToolFactory.parent.call( this );
1333 };
1334
1335 /* Setup */
1336
1337 OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
1338
1339 /* Methods */
1340
1341 /**
1342 * Get tools from the factory
1343 *
1344 * @param {Array|string} [include] Included tools, see #extract for format
1345 * @param {Array|string} [exclude] Excluded tools, see #extract for format
1346 * @param {Array|string} [promote] Promoted tools, see #extract for format
1347 * @param {Array|string} [demote] Demoted tools, see #extract for format
1348 * @return {string[]} List of tools
1349 */
1350 OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
1351 var i, len, included, promoted, demoted,
1352 auto = [],
1353 used = {};
1354
1355 // Collect included and not excluded tools
1356 included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
1357
1358 // Promotion
1359 promoted = this.extract( promote, used );
1360 demoted = this.extract( demote, used );
1361
1362 // Auto
1363 for ( i = 0, len = included.length; i < len; i++ ) {
1364 if ( !used[ included[ i ] ] ) {
1365 auto.push( included[ i ] );
1366 }
1367 }
1368
1369 return promoted.concat( auto ).concat( demoted );
1370 };
1371
1372 /**
1373 * Get a flat list of names from a list of names or groups.
1374 *
1375 * Normally, `collection` is an array of tool specifications. Tools can be specified in the
1376 * following ways:
1377 *
1378 * - To include an individual tool, use the symbolic name: `{ name: 'tool-name' }` or `'tool-name'`.
1379 * - To include all tools in a group, use the group name: `{ group: 'group-name' }`. (To assign the
1380 * tool to a group, use OO.ui.Tool.static.group.)
1381 *
1382 * Alternatively, to include all tools that are not yet assigned to any other toolgroup, use the
1383 * catch-all selector `'*'`.
1384 *
1385 * If `used` is passed, tool names that appear as properties in this object will be considered
1386 * already assigned, and will not be returned even if specified otherwise. The tool names extracted
1387 * by this function call will be added as new properties in the object.
1388 *
1389 * @private
1390 * @param {Array|string} collection List of tools, see above
1391 * @param {Object} [used] Object containing information about used tools, see above
1392 * @return {string[]} List of extracted tool names
1393 */
1394 OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
1395 var i, len, item, name, tool,
1396 names = [];
1397
1398 collection = !Array.isArray( collection ) ? [ collection ] : collection;
1399
1400 for ( i = 0, len = collection.length; i < len; i++ ) {
1401 item = collection[ i ];
1402 if ( item === '*' ) {
1403 for ( name in this.registry ) {
1404 tool = this.registry[ name ];
1405 if (
1406 // Only add tools by group name when auto-add is enabled
1407 tool.static.autoAddToCatchall &&
1408 // Exclude already used tools
1409 ( !used || !used[ name ] )
1410 ) {
1411 names.push( name );
1412 if ( used ) {
1413 used[ name ] = true;
1414 }
1415 }
1416 }
1417 } else {
1418 // Allow plain strings as shorthand for named tools
1419 if ( typeof item === 'string' ) {
1420 item = { name: item };
1421 }
1422 if ( OO.isPlainObject( item ) ) {
1423 if ( item.group ) {
1424 for ( name in this.registry ) {
1425 tool = this.registry[ name ];
1426 if (
1427 // Include tools with matching group
1428 tool.static.group === item.group &&
1429 // Only add tools by group name when auto-add is enabled
1430 tool.static.autoAddToGroup &&
1431 // Exclude already used tools
1432 ( !used || !used[ name ] )
1433 ) {
1434 names.push( name );
1435 if ( used ) {
1436 used[ name ] = true;
1437 }
1438 }
1439 }
1440 // Include tools with matching name and exclude already used tools
1441 } else if ( item.name && ( !used || !used[ item.name ] ) ) {
1442 names.push( item.name );
1443 if ( used ) {
1444 used[ item.name ] = true;
1445 }
1446 }
1447 }
1448 }
1449 }
1450 return names;
1451 };
1452
1453 /**
1454 * ToolGroupFactories create {@link OO.ui.ToolGroup toolgroups} on demand. The toolgroup classes must
1455 * specify a symbolic name and be registered with the factory. The following classes are registered by
1456 * default:
1457 *
1458 * - {@link OO.ui.BarToolGroup BarToolGroups} (‘bar’)
1459 * - {@link OO.ui.MenuToolGroup MenuToolGroups} (‘menu’)
1460 * - {@link OO.ui.ListToolGroup ListToolGroups} (‘list’)
1461 *
1462 * See {@link OO.ui.Toolbar toolbars} for an example.
1463 *
1464 * For more information about toolbars in general, please see the [OOUI documentation on MediaWiki][1].
1465 *
1466 * [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars
1467 *
1468 * @class
1469 * @extends OO.Factory
1470 * @constructor
1471 */
1472 OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() {
1473 var i, l, defaultClasses;
1474 // Parent constructor
1475 OO.Factory.call( this );
1476
1477 defaultClasses = this.constructor.static.getDefaultClasses();
1478
1479 // Register default toolgroups
1480 for ( i = 0, l = defaultClasses.length; i < l; i++ ) {
1481 this.register( defaultClasses[ i ] );
1482 }
1483 };
1484
1485 /* Setup */
1486
1487 OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory );
1488
1489 /* Static Methods */
1490
1491 /**
1492 * Get a default set of classes to be registered on construction.
1493 *
1494 * @return {Function[]} Default classes
1495 */
1496 OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
1497 return [
1498 OO.ui.BarToolGroup,
1499 OO.ui.ListToolGroup,
1500 OO.ui.MenuToolGroup
1501 ];
1502 };
1503
1504 /**
1505 * Popup tools open a popup window when they are selected from the {@link OO.ui.Toolbar toolbar}. Each popup tool is configured
1506 * 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
1507 * an #onSelect or #onUpdateState method, as these methods have been implemented already.
1508 *
1509 * // Example of a popup tool. When selected, a popup tool displays
1510 * // a popup window.
1511 * function HelpTool( toolGroup, config ) {
1512 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
1513 * padded: true,
1514 * label: 'Help',
1515 * head: true
1516 * } }, config ) );
1517 * this.popup.$body.append( '<p>I am helpful!</p>' );
1518 * };
1519 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
1520 * HelpTool.static.name = 'help';
1521 * HelpTool.static.icon = 'help';
1522 * HelpTool.static.title = 'Help';
1523 * toolFactory.register( HelpTool );
1524 *
1525 * For an example of a toolbar that contains a popup tool, see {@link OO.ui.Toolbar toolbars}. For more information about
1526 * toolbars in general, please see the [OOUI documentation on MediaWiki][1].
1527 *
1528 * [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars
1529 *
1530 * @abstract
1531 * @class
1532 * @extends OO.ui.Tool
1533 * @mixins OO.ui.mixin.PopupElement
1534 *
1535 * @constructor
1536 * @param {OO.ui.ToolGroup} toolGroup
1537 * @param {Object} [config] Configuration options
1538 */
1539 OO.ui.PopupTool = function OoUiPopupTool( toolGroup, config ) {
1540 // Allow passing positional parameters inside the config object
1541 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
1542 config = toolGroup;
1543 toolGroup = config.toolGroup;
1544 }
1545
1546 // Parent constructor
1547 OO.ui.PopupTool.parent.call( this, toolGroup, config );
1548
1549 // Mixin constructors
1550 OO.ui.mixin.PopupElement.call( this, config );
1551
1552 // Events
1553 this.popup.connect( this, { toggle: 'onPopupToggle' } );
1554
1555 // Initialization
1556 this.popup.setPosition( toolGroup.getToolbar().position === 'bottom' ? 'above' : 'below' );
1557 this.$element.addClass( 'oo-ui-popupTool' );
1558 this.popup.$element.addClass( 'oo-ui-popupTool-popup' );
1559 this.toolbar.$popups.append( this.popup.$element );
1560 };
1561
1562 /* Setup */
1563
1564 OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool );
1565 OO.mixinClass( OO.ui.PopupTool, OO.ui.mixin.PopupElement );
1566
1567 /* Methods */
1568
1569 /**
1570 * Handle the tool being selected.
1571 *
1572 * @inheritdoc
1573 */
1574 OO.ui.PopupTool.prototype.onSelect = function () {
1575 if ( !this.isDisabled() ) {
1576 this.popup.toggle();
1577 }
1578 return false;
1579 };
1580
1581 /**
1582 * Handle the toolbar state being updated.
1583 *
1584 * @inheritdoc
1585 */
1586 OO.ui.PopupTool.prototype.onUpdateState = function () {
1587 };
1588
1589 /**
1590 * Handle popup visibility being toggled.
1591 *
1592 * @param {boolean} isVisible
1593 */
1594 OO.ui.PopupTool.prototype.onPopupToggle = function ( isVisible ) {
1595 this.setActive( isVisible );
1596 this.toolGroup.emit( 'active', isVisible );
1597 };
1598
1599 /**
1600 * A ToolGroupTool is a special sort of tool that can contain other {@link OO.ui.Tool tools}
1601 * and {@link OO.ui.ToolGroup toolgroups}. The ToolGroupTool was specifically designed to be used
1602 * inside a {@link OO.ui.BarToolGroup bar} toolgroup to provide access to additional tools from
1603 * the bar item. Included tools will be displayed in a dropdown {@link OO.ui.ListToolGroup list}
1604 * when the ToolGroupTool is selected.
1605 *
1606 * // Example: ToolGroupTool with two nested tools, 'setting1' and 'setting2', defined elsewhere.
1607 *
1608 * function SettingsTool() {
1609 * SettingsTool.parent.apply( this, arguments );
1610 * };
1611 * OO.inheritClass( SettingsTool, OO.ui.ToolGroupTool );
1612 * SettingsTool.static.name = 'settings';
1613 * SettingsTool.static.title = 'Change settings';
1614 * SettingsTool.static.groupConfig = {
1615 * icon: 'settings',
1616 * label: 'ToolGroupTool',
1617 * include: [ 'setting1', 'setting2' ]
1618 * };
1619 * toolFactory.register( SettingsTool );
1620 *
1621 * For more information, please see the [OOUI documentation on MediaWiki][1].
1622 *
1623 * Please note that this implementation is subject to change per [T74159] [2].
1624 *
1625 * [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars#ToolGroupTool
1626 * [2]: https://phabricator.wikimedia.org/T74159
1627 *
1628 * @abstract
1629 * @class
1630 * @extends OO.ui.Tool
1631 *
1632 * @constructor
1633 * @param {OO.ui.ToolGroup} toolGroup
1634 * @param {Object} [config] Configuration options
1635 */
1636 OO.ui.ToolGroupTool = function OoUiToolGroupTool( toolGroup, config ) {
1637 // Allow passing positional parameters inside the config object
1638 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
1639 config = toolGroup;
1640 toolGroup = config.toolGroup;
1641 }
1642
1643 // Parent constructor
1644 OO.ui.ToolGroupTool.parent.call( this, toolGroup, config );
1645
1646 // Properties
1647 this.innerToolGroup = this.createGroup( this.constructor.static.groupConfig );
1648
1649 // Events
1650 this.innerToolGroup.connect( this, {
1651 disable: 'onToolGroupDisable',
1652 // Re-emit active events from the innerToolGroup on the parent toolGroup
1653 active: this.toolGroup.emit.bind( this.toolGroup, 'active' )
1654 } );
1655
1656 // Initialization
1657 this.$link.remove();
1658 this.$element
1659 .addClass( 'oo-ui-toolGroupTool' )
1660 .append( this.innerToolGroup.$element );
1661 };
1662
1663 /* Setup */
1664
1665 OO.inheritClass( OO.ui.ToolGroupTool, OO.ui.Tool );
1666
1667 /* Static Properties */
1668
1669 /**
1670 * Toolgroup configuration.
1671 *
1672 * The toolgroup configuration consists of the tools to include, as well as an icon and label
1673 * to use for the bar item. Tools can be included by symbolic name, group, or with the
1674 * wildcard selector. Please see {@link OO.ui.ToolGroup toolgroup} for more information.
1675 *
1676 * @property {Object.<string,Array>}
1677 */
1678 OO.ui.ToolGroupTool.static.groupConfig = {};
1679
1680 /* Methods */
1681
1682 /**
1683 * Handle the tool being selected.
1684 *
1685 * @inheritdoc
1686 */
1687 OO.ui.ToolGroupTool.prototype.onSelect = function () {
1688 this.innerToolGroup.setActive( !this.innerToolGroup.active );
1689 return false;
1690 };
1691
1692 /**
1693 * Synchronize disabledness state of the tool with the inner toolgroup.
1694 *
1695 * @private
1696 * @param {boolean} disabled Element is disabled
1697 */
1698 OO.ui.ToolGroupTool.prototype.onToolGroupDisable = function ( disabled ) {
1699 this.setDisabled( disabled );
1700 };
1701
1702 /**
1703 * Handle the toolbar state being updated.
1704 *
1705 * @inheritdoc
1706 */
1707 OO.ui.ToolGroupTool.prototype.onUpdateState = function () {
1708 this.setActive( false );
1709 };
1710
1711 /**
1712 * Build a {@link OO.ui.ToolGroup toolgroup} from the specified configuration.
1713 *
1714 * @param {Object.<string,Array>} group Toolgroup configuration. Please see {@link OO.ui.ToolGroup toolgroup} for
1715 * more information.
1716 * @return {OO.ui.ListToolGroup}
1717 */
1718 OO.ui.ToolGroupTool.prototype.createGroup = function ( group ) {
1719 if ( group.include === '*' ) {
1720 // Apply defaults to catch-all groups
1721 if ( group.label === undefined ) {
1722 group.label = OO.ui.msg( 'ooui-toolbar-more' );
1723 }
1724 }
1725
1726 return this.toolbar.getToolGroupFactory().create( 'list', this.toolbar, group );
1727 };
1728
1729 /**
1730 * BarToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
1731 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.MenuToolGroup MenuToolGroup}
1732 * and {@link OO.ui.ListToolGroup ListToolGroup}). The {@link OO.ui.Tool tools} in a BarToolGroup are
1733 * displayed by icon in a single row. The title of the tool is displayed when users move the mouse over
1734 * the tool.
1735 *
1736 * BarToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar is
1737 * set up.
1738 *
1739 * @example
1740 * // Example of a BarToolGroup with two tools
1741 * var toolFactory = new OO.ui.ToolFactory();
1742 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
1743 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
1744 *
1745 * // We will be placing status text in this element when tools are used
1746 * var $area = $( '<p>' ).text( 'Example of a BarToolGroup with two tools.' );
1747 *
1748 * // Define the tools that we're going to place in our toolbar
1749 *
1750 * // Create a class inheriting from OO.ui.Tool
1751 * function SearchTool() {
1752 * SearchTool.parent.apply( this, arguments );
1753 * }
1754 * OO.inheritClass( SearchTool, OO.ui.Tool );
1755 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
1756 * // of 'icon' and 'title' (displayed icon and text).
1757 * SearchTool.static.name = 'search';
1758 * SearchTool.static.icon = 'search';
1759 * SearchTool.static.title = 'Search...';
1760 * // Defines the action that will happen when this tool is selected (clicked).
1761 * SearchTool.prototype.onSelect = function () {
1762 * $area.text( 'Search tool clicked!' );
1763 * // Never display this tool as "active" (selected).
1764 * this.setActive( false );
1765 * };
1766 * SearchTool.prototype.onUpdateState = function () {};
1767 * // Make this tool available in our toolFactory and thus our toolbar
1768 * toolFactory.register( SearchTool );
1769 *
1770 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
1771 * // little popup window (a PopupWidget).
1772 * function HelpTool( toolGroup, config ) {
1773 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
1774 * padded: true,
1775 * label: 'Help',
1776 * head: true
1777 * } }, config ) );
1778 * this.popup.$body.append( '<p>I am helpful!</p>' );
1779 * }
1780 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
1781 * HelpTool.static.name = 'help';
1782 * HelpTool.static.icon = 'help';
1783 * HelpTool.static.title = 'Help';
1784 * toolFactory.register( HelpTool );
1785 *
1786 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
1787 * // used once (but not all defined tools must be used).
1788 * toolbar.setup( [
1789 * {
1790 * // 'bar' tool groups display tools by icon only
1791 * type: 'bar',
1792 * include: [ 'search', 'help' ]
1793 * }
1794 * ] );
1795 *
1796 * // Create some UI around the toolbar and place it in the document
1797 * var frame = new OO.ui.PanelLayout( {
1798 * expanded: false,
1799 * framed: true
1800 * } );
1801 * var contentFrame = new OO.ui.PanelLayout( {
1802 * expanded: false,
1803 * padded: true
1804 * } );
1805 * frame.$element.append(
1806 * toolbar.$element,
1807 * contentFrame.$element.append( $area )
1808 * );
1809 * $( 'body' ).append( frame.$element );
1810 *
1811 * // Here is where the toolbar is actually built. This must be done after inserting it into the
1812 * // document.
1813 * toolbar.initialize();
1814 *
1815 * For more information about how to add tools to a bar tool group, please see {@link OO.ui.ToolGroup toolgroup}.
1816 * For more information about toolbars in general, please see the [OOUI documentation on MediaWiki][1].
1817 *
1818 * [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars
1819 *
1820 * @class
1821 * @extends OO.ui.ToolGroup
1822 *
1823 * @constructor
1824 * @param {OO.ui.Toolbar} toolbar
1825 * @param {Object} [config] Configuration options
1826 */
1827 OO.ui.BarToolGroup = function OoUiBarToolGroup( toolbar, config ) {
1828 // Allow passing positional parameters inside the config object
1829 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
1830 config = toolbar;
1831 toolbar = config.toolbar;
1832 }
1833
1834 // Parent constructor
1835 OO.ui.BarToolGroup.parent.call( this, toolbar, config );
1836
1837 // Initialization
1838 this.$element.addClass( 'oo-ui-barToolGroup' );
1839 this.$group.addClass( 'oo-ui-barToolGroup-tools' );
1840 };
1841
1842 /* Setup */
1843
1844 OO.inheritClass( OO.ui.BarToolGroup, OO.ui.ToolGroup );
1845
1846 /* Static Properties */
1847
1848 /**
1849 * @static
1850 * @inheritdoc
1851 */
1852 OO.ui.BarToolGroup.static.titleTooltips = true;
1853
1854 /**
1855 * @static
1856 * @inheritdoc
1857 */
1858 OO.ui.BarToolGroup.static.accelTooltips = true;
1859
1860 /**
1861 * @static
1862 * @inheritdoc
1863 */
1864 OO.ui.BarToolGroup.static.name = 'bar';
1865
1866 /**
1867 * PopupToolGroup is an abstract base class used by both {@link OO.ui.MenuToolGroup MenuToolGroup}
1868 * and {@link OO.ui.ListToolGroup ListToolGroup} to provide a popup (an overlaid menu or list of tools with an
1869 * optional icon and label). This class can be used for other base classes that also use this functionality.
1870 *
1871 * @abstract
1872 * @class
1873 * @extends OO.ui.ToolGroup
1874 * @mixins OO.ui.mixin.IconElement
1875 * @mixins OO.ui.mixin.IndicatorElement
1876 * @mixins OO.ui.mixin.LabelElement
1877 * @mixins OO.ui.mixin.TitledElement
1878 * @mixins OO.ui.mixin.FlaggedElement
1879 * @mixins OO.ui.mixin.ClippableElement
1880 * @mixins OO.ui.mixin.FloatableElement
1881 * @mixins OO.ui.mixin.TabIndexedElement
1882 *
1883 * @constructor
1884 * @param {OO.ui.Toolbar} toolbar
1885 * @param {Object} [config] Configuration options
1886 * @cfg {string} [header] Text to display at the top of the popup
1887 */
1888 OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
1889 // Allow passing positional parameters inside the config object
1890 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
1891 config = toolbar;
1892 toolbar = config.toolbar;
1893 }
1894
1895 // Configuration initialization
1896 config = $.extend( {
1897 indicator: config.indicator === undefined ? ( toolbar.position === 'bottom' ? 'up' : 'down' ) : config.indicator
1898 }, config );
1899
1900 // Parent constructor
1901 OO.ui.PopupToolGroup.parent.call( this, toolbar, config );
1902
1903 // Properties
1904 this.active = false;
1905 this.dragging = false;
1906 // Don't conflict with parent method of the same name
1907 this.onPopupDocumentMouseKeyUpHandler = this.onPopupDocumentMouseKeyUp.bind( this );
1908 this.$handle = $( '<span>' );
1909
1910 // Mixin constructors
1911 OO.ui.mixin.IconElement.call( this, config );
1912 OO.ui.mixin.IndicatorElement.call( this, config );
1913 OO.ui.mixin.LabelElement.call( this, config );
1914 OO.ui.mixin.TitledElement.call( this, config );
1915 OO.ui.mixin.FlaggedElement.call( this, config );
1916 OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
1917 OO.ui.mixin.FloatableElement.call( this, $.extend( {}, config, {
1918 $floatable: this.$group,
1919 $floatableContainer: this.$handle,
1920 hideWhenOutOfView: false,
1921 verticalPosition: this.toolbar.position === 'bottom' ? 'above' : 'below'
1922 } ) );
1923 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
1924
1925 // Events
1926 this.$handle.on( {
1927 keydown: this.onHandleMouseKeyDown.bind( this ),
1928 keyup: this.onHandleMouseKeyUp.bind( this ),
1929 mousedown: this.onHandleMouseKeyDown.bind( this ),
1930 mouseup: this.onHandleMouseKeyUp.bind( this )
1931 } );
1932
1933 // Initialization
1934 this.$handle
1935 .addClass( 'oo-ui-popupToolGroup-handle' )
1936 .attr( 'role', 'button' )
1937 .append( this.$icon, this.$label, this.$indicator );
1938 // If the pop-up should have a header, add it to the top of the toolGroup.
1939 // Note: If this feature is useful for other widgets, we could abstract it into an
1940 // OO.ui.HeaderedElement mixin constructor.
1941 if ( config.header !== undefined ) {
1942 this.$group
1943 .prepend( $( '<span>' )
1944 .addClass( 'oo-ui-popupToolGroup-header' )
1945 .text( config.header )
1946 );
1947 }
1948 this.$element
1949 .addClass( 'oo-ui-popupToolGroup' )
1950 .prepend( this.$handle );
1951 this.$group.addClass( 'oo-ui-popupToolGroup-tools' );
1952 this.toolbar.$popups.append( this.$group );
1953 };
1954
1955 /* Setup */
1956
1957 OO.inheritClass( OO.ui.PopupToolGroup, OO.ui.ToolGroup );
1958 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.IconElement );
1959 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.IndicatorElement );
1960 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.LabelElement );
1961 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.TitledElement );
1962 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.FlaggedElement );
1963 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.ClippableElement );
1964 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.FloatableElement );
1965 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.TabIndexedElement );
1966
1967 /* Methods */
1968
1969 /**
1970 * @inheritdoc
1971 */
1972 OO.ui.PopupToolGroup.prototype.setDisabled = function () {
1973 // Parent method
1974 OO.ui.PopupToolGroup.parent.prototype.setDisabled.apply( this, arguments );
1975
1976 if ( this.isDisabled() && this.isElementAttached() ) {
1977 this.setActive( false );
1978 }
1979 };
1980
1981 /**
1982 * Handle document mouse up and key up events.
1983 *
1984 * @protected
1985 * @param {MouseEvent|KeyboardEvent} e Mouse up or key up event
1986 */
1987 OO.ui.PopupToolGroup.prototype.onPopupDocumentMouseKeyUp = function ( e ) {
1988 var $target = $( e.target );
1989 // Only deactivate when clicking outside the dropdown element
1990 if ( $target.closest( '.oo-ui-popupToolGroup' )[ 0 ] === this.$element[ 0 ] ) {
1991 return;
1992 }
1993 if ( $target.closest( '.oo-ui-popupToolGroup-tools' )[ 0 ] === this.$group[ 0 ] ) {
1994 return;
1995 }
1996 this.setActive( false );
1997 };
1998
1999 // Deprecated alias since 0.28.3
2000 OO.ui.PopupToolGroup.prototype.onBlur = function () {
2001 OO.ui.warnDeprecation( 'onBlur is deprecated, use onPopupDocumentMouseKeyUp instead' );
2002 this.onPopupDocumentMouseKeyUp.apply( this, arguments );
2003 };
2004
2005 /**
2006 * @inheritdoc
2007 */
2008 OO.ui.PopupToolGroup.prototype.onMouseKeyUp = function ( e ) {
2009 // Only close toolgroup when a tool was actually selected
2010 if (
2011 !this.isDisabled() && this.pressed && this.pressed === this.findTargetTool( e ) &&
2012 ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
2013 ) {
2014 this.setActive( false );
2015 }
2016 return OO.ui.PopupToolGroup.parent.prototype.onMouseKeyUp.call( this, e );
2017 };
2018
2019 /**
2020 * @inheritdoc
2021 */
2022 OO.ui.PopupToolGroup.prototype.onMouseKeyDown = function ( e ) {
2023 var $focused, $firstFocusable, $lastFocusable;
2024 // Shift-Tab on the first tool in the group jumps to the handle.
2025 // Tab on the last tool in the group jumps to the next group.
2026 if ( !this.isDisabled() && e.which === OO.ui.Keys.TAB ) {
2027 // (We can't use this.items because ListToolGroup inserts the extra fake expand/collapse tool.)
2028 $focused = $( document.activeElement );
2029 $firstFocusable = OO.ui.findFocusable( this.$group );
2030 if ( $focused[ 0 ] === $firstFocusable[ 0 ] && e.shiftKey ) {
2031 this.$handle.focus();
2032 return false;
2033 }
2034 $lastFocusable = OO.ui.findFocusable( this.$group, true );
2035 if ( $focused[ 0 ] === $lastFocusable[ 0 ] && !e.shiftKey ) {
2036 // Focus this group's handle and let the browser's tab handling happen (no 'return false').
2037 // This way we don't have to fiddle with other ToolGroups' business, or worry what to do
2038 // if the next group is not a PopupToolGroup or doesn't exist at all.
2039 this.$handle.focus();
2040 // Close the popup so that we don't move back inside it (if this is the last group).
2041 this.setActive( false );
2042 }
2043 }
2044 return OO.ui.PopupToolGroup.parent.prototype.onMouseKeyDown.call( this, e );
2045 };
2046
2047 /**
2048 * Handle mouse up and key up events.
2049 *
2050 * @protected
2051 * @param {jQuery.Event} e Mouse up or key up event
2052 */
2053 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyUp = function ( e ) {
2054 if (
2055 !this.isDisabled() &&
2056 ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
2057 ) {
2058 return false;
2059 }
2060 };
2061
2062 /**
2063 * Handle mouse down and key down events.
2064 *
2065 * @protected
2066 * @param {jQuery.Event} e Mouse down or key down event
2067 */
2068 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyDown = function ( e ) {
2069 var $focusable;
2070 if ( !this.isDisabled() ) {
2071 // Tab on the handle jumps to the first tool in the group (if the popup is open).
2072 if ( e.which === OO.ui.Keys.TAB && !e.shiftKey ) {
2073 $focusable = OO.ui.findFocusable( this.$group );
2074 if ( $focusable.length ) {
2075 $focusable.focus();
2076 return false;
2077 }
2078 }
2079 if ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) {
2080 this.setActive( !this.active );
2081 return false;
2082 }
2083 }
2084 };
2085
2086 /**
2087 * Check if the tool group is active.
2088 *
2089 * @return {boolean} Tool group is active
2090 */
2091 OO.ui.PopupToolGroup.prototype.isActive = function () {
2092 return this.active;
2093 };
2094
2095 /**
2096 * Switch into 'active' mode.
2097 *
2098 * When active, the popup is visible. A mouseup event anywhere in the document will trigger
2099 * deactivation.
2100 *
2101 * @param {boolean} value The active state to set
2102 * @fires active
2103 */
2104 OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
2105 var containerWidth, containerLeft;
2106 value = !!value;
2107 if ( this.active !== value ) {
2108 this.active = value;
2109 if ( value ) {
2110 this.getElementDocument().addEventListener( 'mouseup', this.onPopupDocumentMouseKeyUpHandler, true );
2111 this.getElementDocument().addEventListener( 'keyup', this.onPopupDocumentMouseKeyUpHandler, true );
2112
2113 this.$clippable.css( 'left', '' );
2114 this.$element.addClass( 'oo-ui-popupToolGroup-active' );
2115 this.$group.addClass( 'oo-ui-popupToolGroup-active-tools' );
2116 this.togglePositioning( true );
2117 this.toggleClipping( true );
2118
2119 // Try anchoring the popup to the left first
2120 this.setHorizontalPosition( 'start' );
2121
2122 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
2123 // Anchoring to the left caused the popup to clip, so anchor it to the right instead
2124 this.setHorizontalPosition( 'end' );
2125 }
2126 if ( this.isClippedHorizontally() || this.isFloatableOutOfView() ) {
2127 // Anchoring to the right also caused the popup to clip, so just make it fill the container
2128 containerWidth = this.$clippableScrollableContainer.width();
2129 containerLeft = this.$clippableScrollableContainer[ 0 ] === document.documentElement ?
2130 0 :
2131 this.$clippableScrollableContainer.offset().left;
2132
2133 this.toggleClipping( false );
2134 this.setHorizontalPosition( 'start' );
2135
2136 this.$clippable.css( {
2137 'margin-left': -( this.$element.offset().left - containerLeft ),
2138 width: containerWidth
2139 } );
2140 }
2141 } else {
2142 this.getElementDocument().removeEventListener( 'mouseup', this.onPopupDocumentMouseKeyUpHandler, true );
2143 this.getElementDocument().removeEventListener( 'keyup', this.onPopupDocumentMouseKeyUpHandler, true );
2144 this.$element.removeClass( 'oo-ui-popupToolGroup-active' );
2145 this.$group.removeClass( 'oo-ui-popupToolGroup-active-tools' );
2146 this.togglePositioning( false );
2147 this.toggleClipping( false );
2148 }
2149 this.emit( 'active', this.active );
2150 this.updateThemeClasses();
2151 }
2152 };
2153
2154 /**
2155 * ListToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
2156 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.MenuToolGroup MenuToolGroup}
2157 * and {@link OO.ui.BarToolGroup BarToolGroup}). The {@link OO.ui.Tool tools} in a ListToolGroup are displayed
2158 * by label in a dropdown menu. The title of the tool is used as the label text. The menu itself can be configured
2159 * with a label, icon, indicator, header, and title.
2160 *
2161 * ListToolGroups can be configured to be expanded and collapsed. Collapsed lists will have a ‘More’ option that
2162 * users can select to see the full list of tools. If a collapsed toolgroup is expanded, a ‘Fewer’ option permits
2163 * users to collapse the list again.
2164 *
2165 * ListToolGroups are created by a {@link OO.ui.ToolGroupFactory toolgroup factory} when the toolbar is set up. The factory
2166 * requires the ListToolGroup's symbolic name, 'list', which is specified along with the other configurations. For more
2167 * information about how to add tools to a ListToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
2168 *
2169 * @example
2170 * // Example of a ListToolGroup
2171 * var toolFactory = new OO.ui.ToolFactory();
2172 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
2173 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
2174 *
2175 * // Configure and register two tools
2176 * function SettingsTool() {
2177 * SettingsTool.parent.apply( this, arguments );
2178 * }
2179 * OO.inheritClass( SettingsTool, OO.ui.Tool );
2180 * SettingsTool.static.name = 'settings';
2181 * SettingsTool.static.icon = 'settings';
2182 * SettingsTool.static.title = 'Change settings';
2183 * SettingsTool.prototype.onSelect = function () {
2184 * this.setActive( false );
2185 * };
2186 * SettingsTool.prototype.onUpdateState = function () {};
2187 * toolFactory.register( SettingsTool );
2188 * // Register two more tools, nothing interesting here
2189 * function StuffTool() {
2190 * StuffTool.parent.apply( this, arguments );
2191 * }
2192 * OO.inheritClass( StuffTool, OO.ui.Tool );
2193 * StuffTool.static.name = 'stuff';
2194 * StuffTool.static.icon = 'search';
2195 * StuffTool.static.title = 'Change the world';
2196 * StuffTool.prototype.onSelect = function () {
2197 * this.setActive( false );
2198 * };
2199 * StuffTool.prototype.onUpdateState = function () {};
2200 * toolFactory.register( StuffTool );
2201 * toolbar.setup( [
2202 * {
2203 * // Configurations for list toolgroup.
2204 * type: 'list',
2205 * label: 'ListToolGroup',
2206 * icon: 'ellipsis',
2207 * title: 'This is the title, displayed when user moves the mouse over the list toolgroup',
2208 * header: 'This is the header',
2209 * include: [ 'settings', 'stuff' ],
2210 * allowCollapse: ['stuff']
2211 * }
2212 * ] );
2213 *
2214 * // Create some UI around the toolbar and place it in the document
2215 * var frame = new OO.ui.PanelLayout( {
2216 * expanded: false,
2217 * framed: true
2218 * } );
2219 * frame.$element.append(
2220 * toolbar.$element
2221 * );
2222 * $( 'body' ).append( frame.$element );
2223 * // Build the toolbar. This must be done after the toolbar has been appended to the document.
2224 * toolbar.initialize();
2225 *
2226 * For more information about toolbars in general, please see the [OOUI documentation on MediaWiki][1].
2227 *
2228 * [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars
2229 *
2230 * @class
2231 * @extends OO.ui.PopupToolGroup
2232 *
2233 * @constructor
2234 * @param {OO.ui.Toolbar} toolbar
2235 * @param {Object} [config] Configuration options
2236 * @cfg {Array} [allowCollapse] Allow the specified tools to be collapsed. By default, collapsible tools
2237 * will only be displayed if users click the ‘More’ option displayed at the bottom of the list. If
2238 * the list is expanded, a ‘Fewer’ option permits users to collapse the list again. Any tools that
2239 * are included in the toolgroup, but are not designated as collapsible, will always be displayed.
2240 * To open a collapsible list in its expanded state, set #expanded to 'true'.
2241 * @cfg {Array} [forceExpand] Expand the specified tools. All other tools will be designated as collapsible.
2242 * Unless #expanded is set to true, the collapsible tools will be collapsed when the list is first opened.
2243 * @cfg {boolean} [expanded=false] Expand collapsible tools. This config is only relevant if tools have
2244 * been designated as collapsible. When expanded is set to true, all tools in the group will be displayed
2245 * when the list is first opened. Users can collapse the list with a ‘Fewer’ option at the bottom.
2246 */
2247 OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
2248 // Allow passing positional parameters inside the config object
2249 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
2250 config = toolbar;
2251 toolbar = config.toolbar;
2252 }
2253
2254 // Configuration initialization
2255 config = config || {};
2256
2257 // Properties (must be set before parent constructor, which calls #populate)
2258 this.allowCollapse = config.allowCollapse;
2259 this.forceExpand = config.forceExpand;
2260 this.expanded = config.expanded !== undefined ? config.expanded : false;
2261 this.collapsibleTools = [];
2262
2263 // Parent constructor
2264 OO.ui.ListToolGroup.parent.call( this, toolbar, config );
2265
2266 // Initialization
2267 this.$element.addClass( 'oo-ui-listToolGroup' );
2268 this.$group.addClass( 'oo-ui-listToolGroup-tools' );
2269 };
2270
2271 /* Setup */
2272
2273 OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup );
2274
2275 /* Static Properties */
2276
2277 /**
2278 * @static
2279 * @inheritdoc
2280 */
2281 OO.ui.ListToolGroup.static.name = 'list';
2282
2283 /* Methods */
2284
2285 /**
2286 * @inheritdoc
2287 */
2288 OO.ui.ListToolGroup.prototype.populate = function () {
2289 var i, len, allowCollapse = [];
2290
2291 OO.ui.ListToolGroup.parent.prototype.populate.call( this );
2292
2293 // Update the list of collapsible tools
2294 if ( this.allowCollapse !== undefined ) {
2295 allowCollapse = this.allowCollapse;
2296 } else if ( this.forceExpand !== undefined ) {
2297 allowCollapse = OO.simpleArrayDifference( Object.keys( this.tools ), this.forceExpand );
2298 }
2299
2300 this.collapsibleTools = [];
2301 for ( i = 0, len = allowCollapse.length; i < len; i++ ) {
2302 if ( this.tools[ allowCollapse[ i ] ] !== undefined ) {
2303 this.collapsibleTools.push( this.tools[ allowCollapse[ i ] ] );
2304 }
2305 }
2306
2307 // Keep at the end, even when tools are added
2308 this.$group.append( this.getExpandCollapseTool().$element );
2309
2310 this.getExpandCollapseTool().toggle( this.collapsibleTools.length !== 0 );
2311 this.updateCollapsibleState();
2312 };
2313
2314 /**
2315 * Get the expand/collapse tool for this group
2316 *
2317 * @return {OO.ui.Tool} Expand collapse tool
2318 */
2319 OO.ui.ListToolGroup.prototype.getExpandCollapseTool = function () {
2320 var ExpandCollapseTool;
2321 if ( this.expandCollapseTool === undefined ) {
2322 ExpandCollapseTool = function () {
2323 ExpandCollapseTool.parent.apply( this, arguments );
2324 };
2325
2326 OO.inheritClass( ExpandCollapseTool, OO.ui.Tool );
2327
2328 ExpandCollapseTool.prototype.onSelect = function () {
2329 this.toolGroup.expanded = !this.toolGroup.expanded;
2330 this.toolGroup.updateCollapsibleState();
2331 this.setActive( false );
2332 };
2333 ExpandCollapseTool.prototype.onUpdateState = function () {
2334 // Do nothing. Tool interface requires an implementation of this function.
2335 };
2336
2337 ExpandCollapseTool.static.name = 'more-fewer';
2338
2339 this.expandCollapseTool = new ExpandCollapseTool( this );
2340 }
2341 return this.expandCollapseTool;
2342 };
2343
2344 /**
2345 * @inheritdoc
2346 */
2347 OO.ui.ListToolGroup.prototype.onMouseKeyUp = function ( e ) {
2348 // Do not close the popup when the user wants to show more/fewer tools
2349 if (
2350 $( e.target ).closest( '.oo-ui-tool-name-more-fewer' ).length &&
2351 ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
2352 ) {
2353 // HACK: Prevent the popup list from being hidden. Skip the PopupToolGroup implementation (which
2354 // hides the popup list when a tool is selected) and call ToolGroup's implementation directly.
2355 return OO.ui.ListToolGroup.parent.parent.prototype.onMouseKeyUp.call( this, e );
2356 } else {
2357 return OO.ui.ListToolGroup.parent.prototype.onMouseKeyUp.call( this, e );
2358 }
2359 };
2360
2361 OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () {
2362 var i, icon, len;
2363
2364 if ( this.toolbar.position !== 'bottom' ) {
2365 icon = this.expanded ? 'collapse' : 'expand';
2366 } else {
2367 icon = this.expanded ? 'expand' : 'collapse';
2368 }
2369
2370 this.getExpandCollapseTool()
2371 .setIcon( icon )
2372 .setTitle( OO.ui.msg( this.expanded ? 'ooui-toolgroup-collapse' : 'ooui-toolgroup-expand' ) );
2373
2374 for ( i = 0, len = this.collapsibleTools.length; i < len; i++ ) {
2375 this.collapsibleTools[ i ].toggle( this.expanded );
2376 }
2377
2378 // Re-evaluate clipping, because our height has changed
2379 this.clip();
2380 };
2381
2382 /**
2383 * MenuToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
2384 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.BarToolGroup BarToolGroup}
2385 * and {@link OO.ui.ListToolGroup ListToolGroup}). MenuToolGroups contain selectable {@link OO.ui.Tool tools},
2386 * which are displayed by label in a dropdown menu. The tool's title is used as the label text, and the
2387 * menu label is updated to reflect which tool or tools are currently selected. If no tools are selected,
2388 * the menu label is empty. The menu can be configured with an indicator, icon, title, and/or header.
2389 *
2390 * MenuToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar
2391 * is set up.
2392 *
2393 * @example
2394 * // Example of a MenuToolGroup
2395 * var toolFactory = new OO.ui.ToolFactory();
2396 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
2397 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
2398 *
2399 * // We will be placing status text in this element when tools are used
2400 * var $area = $( '<p>' ).text( 'An example of a MenuToolGroup. Select a tool from the dropdown menu.' );
2401 *
2402 * // Define the tools that we're going to place in our toolbar
2403 *
2404 * function SettingsTool() {
2405 * SettingsTool.parent.apply( this, arguments );
2406 * this.reallyActive = false;
2407 * }
2408 * OO.inheritClass( SettingsTool, OO.ui.Tool );
2409 * SettingsTool.static.name = 'settings';
2410 * SettingsTool.static.icon = 'settings';
2411 * SettingsTool.static.title = 'Change settings';
2412 * SettingsTool.prototype.onSelect = function () {
2413 * $area.text( 'Settings tool clicked!' );
2414 * // Toggle the active state on each click
2415 * this.reallyActive = !this.reallyActive;
2416 * this.setActive( this.reallyActive );
2417 * // To update the menu label
2418 * this.toolbar.emit( 'updateState' );
2419 * };
2420 * SettingsTool.prototype.onUpdateState = function () {};
2421 * toolFactory.register( SettingsTool );
2422 *
2423 * function StuffTool() {
2424 * StuffTool.parent.apply( this, arguments );
2425 * this.reallyActive = false;
2426 * }
2427 * OO.inheritClass( StuffTool, OO.ui.Tool );
2428 * StuffTool.static.name = 'stuff';
2429 * StuffTool.static.icon = 'ellipsis';
2430 * StuffTool.static.title = 'More stuff';
2431 * StuffTool.prototype.onSelect = function () {
2432 * $area.text( 'More stuff tool clicked!' );
2433 * // Toggle the active state on each click
2434 * this.reallyActive = !this.reallyActive;
2435 * this.setActive( this.reallyActive );
2436 * // To update the menu label
2437 * this.toolbar.emit( 'updateState' );
2438 * };
2439 * StuffTool.prototype.onUpdateState = function () {};
2440 * toolFactory.register( StuffTool );
2441 *
2442 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
2443 * // used once (but not all defined tools must be used).
2444 * toolbar.setup( [
2445 * {
2446 * type: 'menu',
2447 * header: 'This is the (optional) header',
2448 * title: 'This is the (optional) title',
2449 * include: [ 'settings', 'stuff' ]
2450 * }
2451 * ] );
2452 *
2453 * // Create some UI around the toolbar and place it in the document
2454 * var frame = new OO.ui.PanelLayout( {
2455 * expanded: false,
2456 * framed: true
2457 * } );
2458 * var contentFrame = new OO.ui.PanelLayout( {
2459 * expanded: false,
2460 * padded: true
2461 * } );
2462 * frame.$element.append(
2463 * toolbar.$element,
2464 * contentFrame.$element.append( $area )
2465 * );
2466 * $( 'body' ).append( frame.$element );
2467 *
2468 * // Here is where the toolbar is actually built. This must be done after inserting it into the
2469 * // document.
2470 * toolbar.initialize();
2471 * toolbar.emit( 'updateState' );
2472 *
2473 * For more information about how to add tools to a MenuToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
2474 * For more information about toolbars in general, please see the [OOUI documentation on MediaWiki] [1].
2475 *
2476 * [1]: https://www.mediawiki.org/wiki/OOUI/Toolbars
2477 *
2478 * @class
2479 * @extends OO.ui.PopupToolGroup
2480 *
2481 * @constructor
2482 * @param {OO.ui.Toolbar} toolbar
2483 * @param {Object} [config] Configuration options
2484 */
2485 OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
2486 // Allow passing positional parameters inside the config object
2487 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
2488 config = toolbar;
2489 toolbar = config.toolbar;
2490 }
2491
2492 // Configuration initialization
2493 config = config || {};
2494
2495 // Parent constructor
2496 OO.ui.MenuToolGroup.parent.call( this, toolbar, config );
2497
2498 // Events
2499 this.toolbar.connect( this, { updateState: 'onUpdateState' } );
2500
2501 // Initialization
2502 this.$element.addClass( 'oo-ui-menuToolGroup' );
2503 this.$group.addClass( 'oo-ui-menuToolGroup-tools' );
2504 };
2505
2506 /* Setup */
2507
2508 OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup );
2509
2510 /* Static Properties */
2511
2512 /**
2513 * @static
2514 * @inheritdoc
2515 */
2516 OO.ui.MenuToolGroup.static.name = 'menu';
2517
2518 /* Methods */
2519
2520 /**
2521 * Handle the toolbar state being updated.
2522 *
2523 * When the state changes, the title of each active item in the menu will be joined together and
2524 * used as a label for the group. The label will be empty if none of the items are active.
2525 *
2526 * @private
2527 */
2528 OO.ui.MenuToolGroup.prototype.onUpdateState = function () {
2529 var name,
2530 labelTexts = [];
2531
2532 for ( name in this.tools ) {
2533 if ( this.tools[ name ].isActive() ) {
2534 labelTexts.push( this.tools[ name ].getTitle() );
2535 }
2536 }
2537
2538 this.setLabel( labelTexts.join( ', ' ) || ' ' );
2539 };
2540
2541 }( OO ) );
2542
2543 //# sourceMappingURL=oojs-ui-toolbars.js.map.json