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