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