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