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