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