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