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