065edfc7811505ade3e697d3d64256e9dc4c34ed
[lhc/web/wiklou.git] / resources / lib / oojs-ui / oojs-ui.js
1 /*!
2 * OOjs UI v0.9.1
3 * https://www.mediawiki.org/wiki/OOjs_UI
4 *
5 * Copyright 2011–2015 OOjs Team and other contributors.
6 * Released under the MIT license
7 * http://oojs.mit-license.org
8 *
9 * Date: 2015-03-12T19:08:47Z
10 */
11 ( function ( OO ) {
12
13 'use strict';
14
15 /**
16 * Namespace for all classes, static methods and static properties.
17 *
18 * @class
19 * @singleton
20 */
21 OO.ui = {};
22
23 OO.ui.bind = $.proxy;
24
25 /**
26 * @property {Object}
27 */
28 OO.ui.Keys = {
29 UNDEFINED: 0,
30 BACKSPACE: 8,
31 DELETE: 46,
32 LEFT: 37,
33 RIGHT: 39,
34 UP: 38,
35 DOWN: 40,
36 ENTER: 13,
37 END: 35,
38 HOME: 36,
39 TAB: 9,
40 PAGEUP: 33,
41 PAGEDOWN: 34,
42 ESCAPE: 27,
43 SHIFT: 16,
44 SPACE: 32
45 };
46
47 /**
48 * Get the user's language and any fallback languages.
49 *
50 * These language codes are used to localize user interface elements in the user's language.
51 *
52 * In environments that provide a localization system, this function should be overridden to
53 * return the user's language(s). The default implementation returns English (en) only.
54 *
55 * @return {string[]} Language codes, in descending order of priority
56 */
57 OO.ui.getUserLanguages = function () {
58 return [ 'en' ];
59 };
60
61 /**
62 * Get a value in an object keyed by language code.
63 *
64 * @param {Object.<string,Mixed>} obj Object keyed by language code
65 * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
66 * @param {string} [fallback] Fallback code, used if no matching language can be found
67 * @return {Mixed} Local value
68 */
69 OO.ui.getLocalValue = function ( obj, lang, fallback ) {
70 var i, len, langs;
71
72 // Requested language
73 if ( obj[ lang ] ) {
74 return obj[ lang ];
75 }
76 // Known user language
77 langs = OO.ui.getUserLanguages();
78 for ( i = 0, len = langs.length; i < len; i++ ) {
79 lang = langs[ i ];
80 if ( obj[ lang ] ) {
81 return obj[ lang ];
82 }
83 }
84 // Fallback language
85 if ( obj[ fallback ] ) {
86 return obj[ fallback ];
87 }
88 // First existing language
89 for ( lang in obj ) {
90 return obj[ lang ];
91 }
92
93 return undefined;
94 };
95
96 /**
97 * Check if a node is contained within another node
98 *
99 * Similar to jQuery#contains except a list of containers can be supplied
100 * and a boolean argument allows you to include the container in the match list
101 *
102 * @param {HTMLElement|HTMLElement[]} containers Container node(s) to search in
103 * @param {HTMLElement} contained Node to find
104 * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match, otherwise only match descendants
105 * @return {boolean} The node is in the list of target nodes
106 */
107 OO.ui.contains = function ( containers, contained, matchContainers ) {
108 var i;
109 if ( !Array.isArray( containers ) ) {
110 containers = [ containers ];
111 }
112 for ( i = containers.length - 1; i >= 0; i-- ) {
113 if ( ( matchContainers && contained === containers[ i ] ) || $.contains( containers[ i ], contained ) ) {
114 return true;
115 }
116 }
117 return false;
118 };
119
120 /**
121 * Reconstitute a JavaScript object corresponding to a widget created by
122 * the PHP implementation.
123 *
124 * This is an alias for `OO.ui.Element.static.infuse()`.
125 *
126 * @param {string|HTMLElement|jQuery} idOrNode
127 * A DOM id (if a string) or node for the widget to infuse.
128 * @return {OO.ui.Element}
129 * The `OO.ui.Element` corresponding to this (infusable) document node.
130 */
131 OO.ui.infuse = function ( idOrNode ) {
132 return OO.ui.Element.static.infuse( idOrNode );
133 };
134
135 ( function () {
136 /**
137 * Message store for the default implementation of OO.ui.msg
138 *
139 * Environments that provide a localization system should not use this, but should override
140 * OO.ui.msg altogether.
141 *
142 * @private
143 */
144 var messages = {
145 // Tool tip for a button that moves items in a list down one place
146 'ooui-outline-control-move-down': 'Move item down',
147 // Tool tip for a button that moves items in a list up one place
148 'ooui-outline-control-move-up': 'Move item up',
149 // Tool tip for a button that removes items from a list
150 'ooui-outline-control-remove': 'Remove item',
151 // Label for the toolbar group that contains a list of all other available tools
152 'ooui-toolbar-more': 'More',
153 // Label for the fake tool that expands the full list of tools in a toolbar group
154 'ooui-toolgroup-expand': 'More',
155 // Label for the fake tool that collapses the full list of tools in a toolbar group
156 'ooui-toolgroup-collapse': 'Fewer',
157 // Default label for the accept button of a confirmation dialog
158 'ooui-dialog-message-accept': 'OK',
159 // Default label for the reject button of a confirmation dialog
160 'ooui-dialog-message-reject': 'Cancel',
161 // Title for process dialog error description
162 'ooui-dialog-process-error': 'Something went wrong',
163 // Label for process dialog dismiss error button, visible when describing errors
164 'ooui-dialog-process-dismiss': 'Dismiss',
165 // Label for process dialog retry action button, visible when describing only recoverable errors
166 'ooui-dialog-process-retry': 'Try again',
167 // Label for process dialog retry action button, visible when describing only warnings
168 'ooui-dialog-process-continue': 'Continue'
169 };
170
171 /**
172 * Get a localized message.
173 *
174 * In environments that provide a localization system, this function should be overridden to
175 * return the message translated in the user's language. The default implementation always returns
176 * English messages.
177 *
178 * After the message key, message parameters may optionally be passed. In the default implementation,
179 * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
180 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
181 * they support unnamed, ordered message parameters.
182 *
183 * @abstract
184 * @param {string} key Message key
185 * @param {Mixed...} [params] Message parameters
186 * @return {string} Translated message with parameters substituted
187 */
188 OO.ui.msg = function ( key ) {
189 var message = messages[ key ],
190 params = Array.prototype.slice.call( arguments, 1 );
191 if ( typeof message === 'string' ) {
192 // Perform $1 substitution
193 message = message.replace( /\$(\d+)/g, function ( unused, n ) {
194 var i = parseInt( n, 10 );
195 return params[ i - 1 ] !== undefined ? params[ i - 1 ] : '$' + n;
196 } );
197 } else {
198 // Return placeholder if message not found
199 message = '[' + key + ']';
200 }
201 return message;
202 };
203
204 /**
205 * Package a message and arguments for deferred resolution.
206 *
207 * Use this when you are statically specifying a message and the message may not yet be present.
208 *
209 * @param {string} key Message key
210 * @param {Mixed...} [params] Message parameters
211 * @return {Function} Function that returns the resolved message when executed
212 */
213 OO.ui.deferMsg = function () {
214 var args = arguments;
215 return function () {
216 return OO.ui.msg.apply( OO.ui, args );
217 };
218 };
219
220 /**
221 * Resolve a message.
222 *
223 * If the message is a function it will be executed, otherwise it will pass through directly.
224 *
225 * @param {Function|string} msg Deferred message, or message text
226 * @return {string} Resolved message
227 */
228 OO.ui.resolveMsg = function ( msg ) {
229 if ( $.isFunction( msg ) ) {
230 return msg();
231 }
232 return msg;
233 };
234
235 } )();
236
237 /**
238 * Element that can be marked as pending.
239 *
240 * @abstract
241 * @class
242 *
243 * @constructor
244 * @param {Object} [config] Configuration options
245 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
246 */
247 OO.ui.PendingElement = function OoUiPendingElement( config ) {
248 // Configuration initialization
249 config = config || {};
250
251 // Properties
252 this.pending = 0;
253 this.$pending = null;
254
255 // Initialisation
256 this.setPendingElement( config.$pending || this.$element );
257 };
258
259 /* Setup */
260
261 OO.initClass( OO.ui.PendingElement );
262
263 /* Methods */
264
265 /**
266 * Set the pending element (and clean up any existing one).
267 *
268 * @param {jQuery} $pending The element to set to pending.
269 */
270 OO.ui.PendingElement.prototype.setPendingElement = function ( $pending ) {
271 if ( this.$pending ) {
272 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
273 }
274
275 this.$pending = $pending;
276 if ( this.pending > 0 ) {
277 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
278 }
279 };
280
281 /**
282 * Check if input is pending.
283 *
284 * @return {boolean}
285 */
286 OO.ui.PendingElement.prototype.isPending = function () {
287 return !!this.pending;
288 };
289
290 /**
291 * Increase the pending stack.
292 *
293 * @chainable
294 */
295 OO.ui.PendingElement.prototype.pushPending = function () {
296 if ( this.pending === 0 ) {
297 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
298 this.updateThemeClasses();
299 }
300 this.pending++;
301
302 return this;
303 };
304
305 /**
306 * Reduce the pending stack.
307 *
308 * Clamped at zero.
309 *
310 * @chainable
311 */
312 OO.ui.PendingElement.prototype.popPending = function () {
313 if ( this.pending === 1 ) {
314 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
315 this.updateThemeClasses();
316 }
317 this.pending = Math.max( 0, this.pending - 1 );
318
319 return this;
320 };
321
322 /**
323 * ActionSets manage the behavior of the {@link OO.ui.ActionWidget action widgets} that comprise them.
324 * Actions can be made available for specific contexts (modes) and circumstances
325 * (abilities). Action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
326 *
327 * ActionSets contain two types of actions:
328 *
329 * - Special: Special actions are the first visible actions with special flags, such as 'safe' and 'primary', the default special flags. Additional special flags can be configured in subclasses with the static #specialFlags property.
330 * - Other: Other actions include all non-special visible actions.
331 *
332 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
333 *
334 * @example
335 * // Example: An action set used in a process dialog
336 * function ProcessDialog( config ) {
337 * ProcessDialog.super.call( this, config );
338 * }
339 * OO.inheritClass( ProcessDialog, OO.ui.ProcessDialog );
340 * ProcessDialog.static.title = 'An action set in a process dialog';
341 * // An action set that uses modes ('edit' and 'help' mode, in this example).
342 * ProcessDialog.static.actions = [
343 * { action: 'continue', modes: 'edit', label: 'Continue', flags: [ 'primary', 'constructive' ] },
344 * { action: 'help', modes: 'edit', label: 'Help' },
345 * { modes: 'edit', label: 'Cancel', flags: 'safe' },
346 * { action: 'back', modes: 'help', label: 'Back', flags: 'safe' }
347 * ];
348 *
349 * ProcessDialog.prototype.initialize = function () {
350 * ProcessDialog.super.prototype.initialize.apply( this, arguments );
351 * this.panel1 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
352 * this.panel1.$element.append( '<p>This dialog uses an action set (continue, help, cancel, back) configured with modes. This is edit mode. Click \'help\' to see help mode. </p>' );
353 * this.panel2 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
354 * this.panel2.$element.append( '<p>This is help mode. Only the \'back\' action widget is configured to be visible here. Click \'back\' to return to \'edit\' mode</p>' );
355 * this.stackLayout= new OO.ui.StackLayout( {
356 * items: [ this.panel1, this.panel2 ]
357 * });
358 * this.$body.append( this.stackLayout.$element );
359 * };
360 * ProcessDialog.prototype.getSetupProcess = function ( data ) {
361 * return ProcessDialog.super.prototype.getSetupProcess.call( this, data )
362 * .next( function () {
363 * this.actions.setMode('edit');
364 * }, this );
365 * };
366 * ProcessDialog.prototype.getActionProcess = function ( action ) {
367 * if ( action === 'help' ) {
368 * this.actions.setMode( 'help' );
369 * this.stackLayout.setItem( this.panel2 );
370 * } else if ( action === 'back' ) {
371 * this.actions.setMode( 'edit' );
372 * this.stackLayout.setItem( this.panel1 );
373 * } else if ( action === 'continue' ) {
374 * var dialog = this;
375 * return new OO.ui.Process( function () {
376 * dialog.close();
377 * } );
378 * }
379 * return ProcessDialog.super.prototype.getActionProcess.call( this, action );
380 * };
381 * ProcessDialog.prototype.getBodyHeight = function () {
382 * return this.panel1.$element.outerHeight( true );
383 * };
384 * var windowManager = new OO.ui.WindowManager();
385 * $( 'body' ).append( windowManager.$element );
386 * var processDialog = new ProcessDialog({
387 * size: 'medium'});
388 * windowManager.addWindows( [ processDialog ] );
389 * windowManager.openWindow( processDialog );
390 *
391 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
392 *
393 * @abstract
394 * @class
395 * @mixins OO.EventEmitter
396 *
397 * @constructor
398 * @param {Object} [config] Configuration options
399 */
400 OO.ui.ActionSet = function OoUiActionSet( config ) {
401 // Configuration initialization
402 config = config || {};
403
404 // Mixin constructors
405 OO.EventEmitter.call( this );
406
407 // Properties
408 this.list = [];
409 this.categories = {
410 actions: 'getAction',
411 flags: 'getFlags',
412 modes: 'getModes'
413 };
414 this.categorized = {};
415 this.special = {};
416 this.others = [];
417 this.organized = false;
418 this.changing = false;
419 this.changed = false;
420 };
421
422 /* Setup */
423
424 OO.mixinClass( OO.ui.ActionSet, OO.EventEmitter );
425
426 /* Static Properties */
427
428 /**
429 * Symbolic name of the flags used to identify special actions. Special actions are displayed in the
430 * header of a {@link OO.ui.ProcessDialog process dialog}.
431 * See the [OOjs UI documentation on MediaWiki][2] for more information and examples.
432 *
433 * [2]:https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
434 *
435 * @abstract
436 * @static
437 * @inheritable
438 * @property {string}
439 */
440 OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ];
441
442 /* Events */
443
444 /**
445 * @event click
446 *
447 * A 'click' event is emitted when an action is clicked.
448 *
449 * @param {OO.ui.ActionWidget} action Action that was clicked
450 */
451
452 /**
453 * @event resize
454 *
455 * A 'resize' event is emitted when an action widget is resized.
456 *
457 * @param {OO.ui.ActionWidget} action Action that was resized
458 */
459
460 /**
461 * @event add
462 *
463 * An 'add' event is emitted when actions are {@link #method-add added} to the action set.
464 *
465 * @param {OO.ui.ActionWidget[]} added Actions added
466 */
467
468 /**
469 * @event remove
470 *
471 * A 'remove' event is emitted when actions are {@link #method-remove removed}
472 * or {@link #clear cleared}.
473 *
474 * @param {OO.ui.ActionWidget[]} added Actions removed
475 */
476
477 /**
478 * @event change
479 *
480 * A 'change' event is emitted when actions are {@link #method-add added}, {@link #clear cleared},
481 * or {@link #method-remove removed} from the action set or when the {@link #setMode mode} is changed.
482 *
483 */
484
485 /* Methods */
486
487 /**
488 * Handle action change events.
489 *
490 * @private
491 * @fires change
492 */
493 OO.ui.ActionSet.prototype.onActionChange = function () {
494 this.organized = false;
495 if ( this.changing ) {
496 this.changed = true;
497 } else {
498 this.emit( 'change' );
499 }
500 };
501
502 /**
503 * Check if an action is one of the special actions.
504 *
505 * @param {OO.ui.ActionWidget} action Action to check
506 * @return {boolean} Action is special
507 */
508 OO.ui.ActionSet.prototype.isSpecial = function ( action ) {
509 var flag;
510
511 for ( flag in this.special ) {
512 if ( action === this.special[ flag ] ) {
513 return true;
514 }
515 }
516
517 return false;
518 };
519
520 /**
521 * Get action widgets based on the specified filter: ‘actions’, ‘flags’, ‘modes’, ‘visible’,
522 * or ‘disabled’.
523 *
524 * @param {Object} [filters] Filters to use, omit to get all actions
525 * @param {string|string[]} [filters.actions] Actions that action widgets must have
526 * @param {string|string[]} [filters.flags] Flags that action widgets must have (e.g., 'safe')
527 * @param {string|string[]} [filters.modes] Modes that action widgets must have
528 * @param {boolean} [filters.visible] Action widgets must be visible
529 * @param {boolean} [filters.disabled] Action widgets must be disabled
530 * @return {OO.ui.ActionWidget[]} Action widgets matching all criteria
531 */
532 OO.ui.ActionSet.prototype.get = function ( filters ) {
533 var i, len, list, category, actions, index, match, matches;
534
535 if ( filters ) {
536 this.organize();
537
538 // Collect category candidates
539 matches = [];
540 for ( category in this.categorized ) {
541 list = filters[ category ];
542 if ( list ) {
543 if ( !Array.isArray( list ) ) {
544 list = [ list ];
545 }
546 for ( i = 0, len = list.length; i < len; i++ ) {
547 actions = this.categorized[ category ][ list[ i ] ];
548 if ( Array.isArray( actions ) ) {
549 matches.push.apply( matches, actions );
550 }
551 }
552 }
553 }
554 // Remove by boolean filters
555 for ( i = 0, len = matches.length; i < len; i++ ) {
556 match = matches[ i ];
557 if (
558 ( filters.visible !== undefined && match.isVisible() !== filters.visible ) ||
559 ( filters.disabled !== undefined && match.isDisabled() !== filters.disabled )
560 ) {
561 matches.splice( i, 1 );
562 len--;
563 i--;
564 }
565 }
566 // Remove duplicates
567 for ( i = 0, len = matches.length; i < len; i++ ) {
568 match = matches[ i ];
569 index = matches.lastIndexOf( match );
570 while ( index !== i ) {
571 matches.splice( index, 1 );
572 len--;
573 index = matches.lastIndexOf( match );
574 }
575 }
576 return matches;
577 }
578 return this.list.slice();
579 };
580
581 /**
582 * Get 'special' actions.
583 *
584 * Special actions are the first visible action widgets with special flags, such as 'safe' and 'primary'.
585 * Special flags can be configured in subclasses by changing the static #specialFlags property.
586 *
587 * @return {OO.ui.ActionWidget[]|null} 'Special' action widgets.
588 */
589 OO.ui.ActionSet.prototype.getSpecial = function () {
590 this.organize();
591 return $.extend( {}, this.special );
592 };
593
594 /**
595 * Get 'other' actions.
596 *
597 * Other actions include all non-special visible action widgets.
598 *
599 * @return {OO.ui.ActionWidget[]} 'Other' action widgets
600 */
601 OO.ui.ActionSet.prototype.getOthers = function () {
602 this.organize();
603 return this.others.slice();
604 };
605
606 /**
607 * Set the mode (e.g., ‘edit’ or ‘view’). Only {@link OO.ui.ActionWidget#modes actions} configured
608 * to be available in the specified mode will be made visible. All other actions will be hidden.
609 *
610 * @param {string} mode The mode. Only actions configured to be available in the specified
611 * mode will be made visible.
612 * @chainable
613 * @fires toggle
614 * @fires change
615 */
616 OO.ui.ActionSet.prototype.setMode = function ( mode ) {
617 var i, len, action;
618
619 this.changing = true;
620 for ( i = 0, len = this.list.length; i < len; i++ ) {
621 action = this.list[ i ];
622 action.toggle( action.hasMode( mode ) );
623 }
624
625 this.organized = false;
626 this.changing = false;
627 this.emit( 'change' );
628
629 return this;
630 };
631
632 /**
633 * Set the abilities of the specified actions.
634 *
635 * Action widgets that are configured with the specified actions will be enabled
636 * or disabled based on the boolean values specified in the `actions`
637 * parameter.
638 *
639 * @param {Object.<string,boolean>} actions A list keyed by action name with boolean
640 * values that indicate whether or not the action should be enabled.
641 * @chainable
642 */
643 OO.ui.ActionSet.prototype.setAbilities = function ( actions ) {
644 var i, len, action, item;
645
646 for ( i = 0, len = this.list.length; i < len; i++ ) {
647 item = this.list[ i ];
648 action = item.getAction();
649 if ( actions[ action ] !== undefined ) {
650 item.setDisabled( !actions[ action ] );
651 }
652 }
653
654 return this;
655 };
656
657 /**
658 * Executes a function once per action.
659 *
660 * When making changes to multiple actions, use this method instead of iterating over the actions
661 * manually to defer emitting a #change event until after all actions have been changed.
662 *
663 * @param {Object|null} actions Filters to use to determine which actions to iterate over; see #get
664 * @param {Function} callback Callback to run for each action; callback is invoked with three
665 * arguments: the action, the action's index, the list of actions being iterated over
666 * @chainable
667 */
668 OO.ui.ActionSet.prototype.forEach = function ( filter, callback ) {
669 this.changed = false;
670 this.changing = true;
671 this.get( filter ).forEach( callback );
672 this.changing = false;
673 if ( this.changed ) {
674 this.emit( 'change' );
675 }
676
677 return this;
678 };
679
680 /**
681 * Add action widgets to the action set.
682 *
683 * @param {OO.ui.ActionWidget[]} actions Action widgets to add
684 * @chainable
685 * @fires add
686 * @fires change
687 */
688 OO.ui.ActionSet.prototype.add = function ( actions ) {
689 var i, len, action;
690
691 this.changing = true;
692 for ( i = 0, len = actions.length; i < len; i++ ) {
693 action = actions[ i ];
694 action.connect( this, {
695 click: [ 'emit', 'click', action ],
696 resize: [ 'emit', 'resize', action ],
697 toggle: [ 'onActionChange' ]
698 } );
699 this.list.push( action );
700 }
701 this.organized = false;
702 this.emit( 'add', actions );
703 this.changing = false;
704 this.emit( 'change' );
705
706 return this;
707 };
708
709 /**
710 * Remove action widgets from the set.
711 *
712 * To remove all actions, you may wish to use the #clear method instead.
713 *
714 * @param {OO.ui.ActionWidget[]} actions Action widgets to remove
715 * @chainable
716 * @fires remove
717 * @fires change
718 */
719 OO.ui.ActionSet.prototype.remove = function ( actions ) {
720 var i, len, index, action;
721
722 this.changing = true;
723 for ( i = 0, len = actions.length; i < len; i++ ) {
724 action = actions[ i ];
725 index = this.list.indexOf( action );
726 if ( index !== -1 ) {
727 action.disconnect( this );
728 this.list.splice( index, 1 );
729 }
730 }
731 this.organized = false;
732 this.emit( 'remove', actions );
733 this.changing = false;
734 this.emit( 'change' );
735
736 return this;
737 };
738
739 /**
740 * Remove all action widets from the set.
741 *
742 * To remove only specified actions, use the {@link #method-remove remove} method instead.
743 *
744 * @chainable
745 * @fires remove
746 * @fires change
747 */
748 OO.ui.ActionSet.prototype.clear = function () {
749 var i, len, action,
750 removed = this.list.slice();
751
752 this.changing = true;
753 for ( i = 0, len = this.list.length; i < len; i++ ) {
754 action = this.list[ i ];
755 action.disconnect( this );
756 }
757
758 this.list = [];
759
760 this.organized = false;
761 this.emit( 'remove', removed );
762 this.changing = false;
763 this.emit( 'change' );
764
765 return this;
766 };
767
768 /**
769 * Organize actions.
770 *
771 * This is called whenever organized information is requested. It will only reorganize the actions
772 * if something has changed since the last time it ran.
773 *
774 * @private
775 * @chainable
776 */
777 OO.ui.ActionSet.prototype.organize = function () {
778 var i, iLen, j, jLen, flag, action, category, list, item, special,
779 specialFlags = this.constructor.static.specialFlags;
780
781 if ( !this.organized ) {
782 this.categorized = {};
783 this.special = {};
784 this.others = [];
785 for ( i = 0, iLen = this.list.length; i < iLen; i++ ) {
786 action = this.list[ i ];
787 if ( action.isVisible() ) {
788 // Populate categories
789 for ( category in this.categories ) {
790 if ( !this.categorized[ category ] ) {
791 this.categorized[ category ] = {};
792 }
793 list = action[ this.categories[ category ] ]();
794 if ( !Array.isArray( list ) ) {
795 list = [ list ];
796 }
797 for ( j = 0, jLen = list.length; j < jLen; j++ ) {
798 item = list[ j ];
799 if ( !this.categorized[ category ][ item ] ) {
800 this.categorized[ category ][ item ] = [];
801 }
802 this.categorized[ category ][ item ].push( action );
803 }
804 }
805 // Populate special/others
806 special = false;
807 for ( j = 0, jLen = specialFlags.length; j < jLen; j++ ) {
808 flag = specialFlags[ j ];
809 if ( !this.special[ flag ] && action.hasFlag( flag ) ) {
810 this.special[ flag ] = action;
811 special = true;
812 break;
813 }
814 }
815 if ( !special ) {
816 this.others.push( action );
817 }
818 }
819 }
820 this.organized = true;
821 }
822
823 return this;
824 };
825
826 /**
827 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
828 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
829 * connected to them and can't be interacted with.
830 *
831 * @abstract
832 * @class
833 *
834 * @constructor
835 * @param {Object} [config] Configuration options
836 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
837 * to the top level (e.g., the outermost div) of the element. See the [OOjs UI documentation on MediaWiki][2]
838 * for an example.
839 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#cssExample
840 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
841 * @cfg {string} [text] Text to insert
842 * @cfg {Array} [content] An array of content elements to append (after #text).
843 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
844 * Instances of OO.ui.Element will have their $element appended.
845 * @cfg {jQuery} [$content] Content elements to append (after #text)
846 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
847 * Data can also be specified with the #setData method.
848 */
849 OO.ui.Element = function OoUiElement( config ) {
850 // Configuration initialization
851 config = config || {};
852
853 // Properties
854 this.$ = $;
855 this.visible = true;
856 this.data = config.data;
857 this.$element = config.$element ||
858 $( document.createElement( this.getTagName() ) );
859 this.elementGroup = null;
860 this.debouncedUpdateThemeClassesHandler = this.debouncedUpdateThemeClasses.bind( this );
861 this.updateThemeClassesPending = false;
862
863 // Initialization
864 if ( Array.isArray( config.classes ) ) {
865 this.$element.addClass( config.classes.join( ' ' ) );
866 }
867 if ( config.id ) {
868 this.$element.attr( 'id', config.id );
869 }
870 if ( config.text ) {
871 this.$element.text( config.text );
872 }
873 if ( config.content ) {
874 // The `content` property treats plain strings as text; use an
875 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
876 // appropriate $element appended.
877 this.$element.append( config.content.map( function ( v ) {
878 if ( typeof v === 'string' ) {
879 // Escape string so it is properly represented in HTML.
880 return document.createTextNode( v );
881 } else if ( v instanceof OO.ui.HtmlSnippet ) {
882 // Bypass escaping.
883 return v.toString();
884 } else if ( v instanceof OO.ui.Element ) {
885 return v.$element;
886 }
887 return v;
888 } ) );
889 }
890 if ( config.$content ) {
891 // The `$content` property treats plain strings as HTML.
892 this.$element.append( config.$content );
893 }
894 };
895
896 /* Setup */
897
898 OO.initClass( OO.ui.Element );
899
900 /* Static Properties */
901
902 /**
903 * The name of the HTML tag used by the element.
904 *
905 * The static value may be ignored if the #getTagName method is overridden.
906 *
907 * @static
908 * @inheritable
909 * @property {string}
910 */
911 OO.ui.Element.static.tagName = 'div';
912
913 /* Static Methods */
914
915 /**
916 * Reconstitute a JavaScript object corresponding to a widget created
917 * by the PHP implementation.
918 *
919 * @param {string|HTMLElement|jQuery} idOrNode
920 * A DOM id (if a string) or node for the widget to infuse.
921 * @return {OO.ui.Element}
922 * The `OO.ui.Element` corresponding to this (infusable) document node.
923 * For `Tag` objects emitted on the HTML side (used occasionally for content)
924 * the value returned is a newly-created Element wrapping around the existing
925 * DOM node.
926 */
927 OO.ui.Element.static.infuse = function ( idOrNode ) {
928 var obj = OO.ui.Element.static.unsafeInfuse( idOrNode, true );
929 // Verify that the type matches up.
930 // FIXME: uncomment after T89721 is fixed (see T90929)
931 /*
932 if ( !( obj instanceof this['class'] ) ) {
933 throw new Error( 'Infusion type mismatch!' );
934 }
935 */
936 return obj;
937 };
938
939 /**
940 * Implementation helper for `infuse`; skips the type check and has an
941 * extra property so that only the top-level invocation touches the DOM.
942 * @private
943 * @param {string|HTMLElement|jQuery} idOrNode
944 * @param {boolean} top True only for top-level invocation.
945 * @return {OO.ui.Element}
946 */
947 OO.ui.Element.static.unsafeInfuse = function ( idOrNode, top ) {
948 // look for a cached result of a previous infusion.
949 var id, $elem, data, cls, obj;
950 if ( typeof idOrNode === 'string' ) {
951 id = idOrNode;
952 $elem = $( document.getElementById( id ) );
953 } else {
954 $elem = $( idOrNode );
955 id = $elem.attr( 'id' );
956 }
957 data = $elem.data( 'ooui-infused' );
958 if ( data ) {
959 // cached!
960 if ( data === true ) {
961 throw new Error( 'Circular dependency! ' + id );
962 }
963 return data;
964 }
965 if ( !$elem.length ) {
966 throw new Error( 'Widget not found: ' + id );
967 }
968 data = $elem.attr( 'data-ooui' );
969 if ( !data ) {
970 throw new Error( 'No infusion data found: ' + id );
971 }
972 try {
973 data = $.parseJSON( data );
974 } catch ( _ ) {
975 data = null;
976 }
977 if ( !( data && data._ ) ) {
978 throw new Error( 'No valid infusion data found: ' + id );
979 }
980 if ( data._ === 'Tag' ) {
981 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
982 return new OO.ui.Element( { $element: $elem } );
983 }
984 cls = OO.ui[data._];
985 if ( !cls ) {
986 throw new Error( 'Unknown widget type: ' + id );
987 }
988 $elem.data( 'ooui-infused', true ); // prevent loops
989 data.id = id; // implicit
990 data = OO.copy( data, null, function deserialize( value ) {
991 if ( OO.isPlainObject( value ) ) {
992 if ( value.tag ) {
993 return OO.ui.Element.static.unsafeInfuse( value.tag, false );
994 }
995 if ( value.html ) {
996 return new OO.ui.HtmlSnippet( value.html );
997 }
998 }
999 } );
1000 // jscs:disable requireCapitalizedConstructors
1001 obj = new cls( data ); // rebuild widget
1002 // now replace old DOM with this new DOM.
1003 if ( top ) {
1004 $elem.replaceWith( obj.$element );
1005 }
1006 obj.$element.data( 'ooui-infused', obj );
1007 // set the 'data-ooui' attribute so we can identify infused widgets
1008 obj.$element.attr( 'data-ooui', '' );
1009 return obj;
1010 };
1011
1012 /**
1013 * Get a jQuery function within a specific document.
1014 *
1015 * @static
1016 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
1017 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
1018 * not in an iframe
1019 * @return {Function} Bound jQuery function
1020 */
1021 OO.ui.Element.static.getJQuery = function ( context, $iframe ) {
1022 function wrapper( selector ) {
1023 return $( selector, wrapper.context );
1024 }
1025
1026 wrapper.context = this.getDocument( context );
1027
1028 if ( $iframe ) {
1029 wrapper.$iframe = $iframe;
1030 }
1031
1032 return wrapper;
1033 };
1034
1035 /**
1036 * Get the document of an element.
1037 *
1038 * @static
1039 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
1040 * @return {HTMLDocument|null} Document object
1041 */
1042 OO.ui.Element.static.getDocument = function ( obj ) {
1043 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
1044 return ( obj[ 0 ] && obj[ 0 ].ownerDocument ) ||
1045 // Empty jQuery selections might have a context
1046 obj.context ||
1047 // HTMLElement
1048 obj.ownerDocument ||
1049 // Window
1050 obj.document ||
1051 // HTMLDocument
1052 ( obj.nodeType === 9 && obj ) ||
1053 null;
1054 };
1055
1056 /**
1057 * Get the window of an element or document.
1058 *
1059 * @static
1060 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
1061 * @return {Window} Window object
1062 */
1063 OO.ui.Element.static.getWindow = function ( obj ) {
1064 var doc = this.getDocument( obj );
1065 return doc.parentWindow || doc.defaultView;
1066 };
1067
1068 /**
1069 * Get the direction of an element or document.
1070 *
1071 * @static
1072 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
1073 * @return {string} Text direction, either 'ltr' or 'rtl'
1074 */
1075 OO.ui.Element.static.getDir = function ( obj ) {
1076 var isDoc, isWin;
1077
1078 if ( obj instanceof jQuery ) {
1079 obj = obj[ 0 ];
1080 }
1081 isDoc = obj.nodeType === 9;
1082 isWin = obj.document !== undefined;
1083 if ( isDoc || isWin ) {
1084 if ( isWin ) {
1085 obj = obj.document;
1086 }
1087 obj = obj.body;
1088 }
1089 return $( obj ).css( 'direction' );
1090 };
1091
1092 /**
1093 * Get the offset between two frames.
1094 *
1095 * TODO: Make this function not use recursion.
1096 *
1097 * @static
1098 * @param {Window} from Window of the child frame
1099 * @param {Window} [to=window] Window of the parent frame
1100 * @param {Object} [offset] Offset to start with, used internally
1101 * @return {Object} Offset object, containing left and top properties
1102 */
1103 OO.ui.Element.static.getFrameOffset = function ( from, to, offset ) {
1104 var i, len, frames, frame, rect;
1105
1106 if ( !to ) {
1107 to = window;
1108 }
1109 if ( !offset ) {
1110 offset = { top: 0, left: 0 };
1111 }
1112 if ( from.parent === from ) {
1113 return offset;
1114 }
1115
1116 // Get iframe element
1117 frames = from.parent.document.getElementsByTagName( 'iframe' );
1118 for ( i = 0, len = frames.length; i < len; i++ ) {
1119 if ( frames[ i ].contentWindow === from ) {
1120 frame = frames[ i ];
1121 break;
1122 }
1123 }
1124
1125 // Recursively accumulate offset values
1126 if ( frame ) {
1127 rect = frame.getBoundingClientRect();
1128 offset.left += rect.left;
1129 offset.top += rect.top;
1130 if ( from !== to ) {
1131 this.getFrameOffset( from.parent, offset );
1132 }
1133 }
1134 return offset;
1135 };
1136
1137 /**
1138 * Get the offset between two elements.
1139 *
1140 * The two elements may be in a different frame, but in that case the frame $element is in must
1141 * be contained in the frame $anchor is in.
1142 *
1143 * @static
1144 * @param {jQuery} $element Element whose position to get
1145 * @param {jQuery} $anchor Element to get $element's position relative to
1146 * @return {Object} Translated position coordinates, containing top and left properties
1147 */
1148 OO.ui.Element.static.getRelativePosition = function ( $element, $anchor ) {
1149 var iframe, iframePos,
1150 pos = $element.offset(),
1151 anchorPos = $anchor.offset(),
1152 elementDocument = this.getDocument( $element ),
1153 anchorDocument = this.getDocument( $anchor );
1154
1155 // If $element isn't in the same document as $anchor, traverse up
1156 while ( elementDocument !== anchorDocument ) {
1157 iframe = elementDocument.defaultView.frameElement;
1158 if ( !iframe ) {
1159 throw new Error( '$element frame is not contained in $anchor frame' );
1160 }
1161 iframePos = $( iframe ).offset();
1162 pos.left += iframePos.left;
1163 pos.top += iframePos.top;
1164 elementDocument = iframe.ownerDocument;
1165 }
1166 pos.left -= anchorPos.left;
1167 pos.top -= anchorPos.top;
1168 return pos;
1169 };
1170
1171 /**
1172 * Get element border sizes.
1173 *
1174 * @static
1175 * @param {HTMLElement} el Element to measure
1176 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1177 */
1178 OO.ui.Element.static.getBorders = function ( el ) {
1179 var doc = el.ownerDocument,
1180 win = doc.parentWindow || doc.defaultView,
1181 style = win && win.getComputedStyle ?
1182 win.getComputedStyle( el, null ) :
1183 el.currentStyle,
1184 $el = $( el ),
1185 top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0,
1186 left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0,
1187 bottom = parseFloat( style ? style.borderBottomWidth : $el.css( 'borderBottomWidth' ) ) || 0,
1188 right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0;
1189
1190 return {
1191 top: top,
1192 left: left,
1193 bottom: bottom,
1194 right: right
1195 };
1196 };
1197
1198 /**
1199 * Get dimensions of an element or window.
1200 *
1201 * @static
1202 * @param {HTMLElement|Window} el Element to measure
1203 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1204 */
1205 OO.ui.Element.static.getDimensions = function ( el ) {
1206 var $el, $win,
1207 doc = el.ownerDocument || el.document,
1208 win = doc.parentWindow || doc.defaultView;
1209
1210 if ( win === el || el === doc.documentElement ) {
1211 $win = $( win );
1212 return {
1213 borders: { top: 0, left: 0, bottom: 0, right: 0 },
1214 scroll: {
1215 top: $win.scrollTop(),
1216 left: $win.scrollLeft()
1217 },
1218 scrollbar: { right: 0, bottom: 0 },
1219 rect: {
1220 top: 0,
1221 left: 0,
1222 bottom: $win.innerHeight(),
1223 right: $win.innerWidth()
1224 }
1225 };
1226 } else {
1227 $el = $( el );
1228 return {
1229 borders: this.getBorders( el ),
1230 scroll: {
1231 top: $el.scrollTop(),
1232 left: $el.scrollLeft()
1233 },
1234 scrollbar: {
1235 right: $el.innerWidth() - el.clientWidth,
1236 bottom: $el.innerHeight() - el.clientHeight
1237 },
1238 rect: el.getBoundingClientRect()
1239 };
1240 }
1241 };
1242
1243 /**
1244 * Get scrollable object parent
1245 *
1246 * documentElement can't be used to get or set the scrollTop
1247 * property on Blink. Changing and testing its value lets us
1248 * use 'body' or 'documentElement' based on what is working.
1249 *
1250 * https://code.google.com/p/chromium/issues/detail?id=303131
1251 *
1252 * @static
1253 * @param {HTMLElement} el Element to find scrollable parent for
1254 * @return {HTMLElement} Scrollable parent
1255 */
1256 OO.ui.Element.static.getRootScrollableElement = function ( el ) {
1257 var scrollTop, body;
1258
1259 if ( OO.ui.scrollableElement === undefined ) {
1260 body = el.ownerDocument.body;
1261 scrollTop = body.scrollTop;
1262 body.scrollTop = 1;
1263
1264 if ( body.scrollTop === 1 ) {
1265 body.scrollTop = scrollTop;
1266 OO.ui.scrollableElement = 'body';
1267 } else {
1268 OO.ui.scrollableElement = 'documentElement';
1269 }
1270 }
1271
1272 return el.ownerDocument[ OO.ui.scrollableElement ];
1273 };
1274
1275 /**
1276 * Get closest scrollable container.
1277 *
1278 * Traverses up until either a scrollable element or the root is reached, in which case the window
1279 * will be returned.
1280 *
1281 * @static
1282 * @param {HTMLElement} el Element to find scrollable container for
1283 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1284 * @return {HTMLElement} Closest scrollable container
1285 */
1286 OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) {
1287 var i, val,
1288 props = [ 'overflow' ],
1289 $parent = $( el ).parent();
1290
1291 if ( dimension === 'x' || dimension === 'y' ) {
1292 props.push( 'overflow-' + dimension );
1293 }
1294
1295 while ( $parent.length ) {
1296 if ( $parent[ 0 ] === this.getRootScrollableElement( el ) ) {
1297 return $parent[ 0 ];
1298 }
1299 i = props.length;
1300 while ( i-- ) {
1301 val = $parent.css( props[ i ] );
1302 if ( val === 'auto' || val === 'scroll' ) {
1303 return $parent[ 0 ];
1304 }
1305 }
1306 $parent = $parent.parent();
1307 }
1308 return this.getDocument( el ).body;
1309 };
1310
1311 /**
1312 * Scroll element into view.
1313 *
1314 * @static
1315 * @param {HTMLElement} el Element to scroll into view
1316 * @param {Object} [config] Configuration options
1317 * @param {string} [config.duration] jQuery animation duration value
1318 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1319 * to scroll in both directions
1320 * @param {Function} [config.complete] Function to call when scrolling completes
1321 */
1322 OO.ui.Element.static.scrollIntoView = function ( el, config ) {
1323 // Configuration initialization
1324 config = config || {};
1325
1326 var rel, anim = {},
1327 callback = typeof config.complete === 'function' && config.complete,
1328 sc = this.getClosestScrollableContainer( el, config.direction ),
1329 $sc = $( sc ),
1330 eld = this.getDimensions( el ),
1331 scd = this.getDimensions( sc ),
1332 $win = $( this.getWindow( el ) );
1333
1334 // Compute the distances between the edges of el and the edges of the scroll viewport
1335 if ( $sc.is( 'html, body' ) ) {
1336 // If the scrollable container is the root, this is easy
1337 rel = {
1338 top: eld.rect.top,
1339 bottom: $win.innerHeight() - eld.rect.bottom,
1340 left: eld.rect.left,
1341 right: $win.innerWidth() - eld.rect.right
1342 };
1343 } else {
1344 // Otherwise, we have to subtract el's coordinates from sc's coordinates
1345 rel = {
1346 top: eld.rect.top - ( scd.rect.top + scd.borders.top ),
1347 bottom: scd.rect.bottom - scd.borders.bottom - scd.scrollbar.bottom - eld.rect.bottom,
1348 left: eld.rect.left - ( scd.rect.left + scd.borders.left ),
1349 right: scd.rect.right - scd.borders.right - scd.scrollbar.right - eld.rect.right
1350 };
1351 }
1352
1353 if ( !config.direction || config.direction === 'y' ) {
1354 if ( rel.top < 0 ) {
1355 anim.scrollTop = scd.scroll.top + rel.top;
1356 } else if ( rel.top > 0 && rel.bottom < 0 ) {
1357 anim.scrollTop = scd.scroll.top + Math.min( rel.top, -rel.bottom );
1358 }
1359 }
1360 if ( !config.direction || config.direction === 'x' ) {
1361 if ( rel.left < 0 ) {
1362 anim.scrollLeft = scd.scroll.left + rel.left;
1363 } else if ( rel.left > 0 && rel.right < 0 ) {
1364 anim.scrollLeft = scd.scroll.left + Math.min( rel.left, -rel.right );
1365 }
1366 }
1367 if ( !$.isEmptyObject( anim ) ) {
1368 $sc.stop( true ).animate( anim, config.duration || 'fast' );
1369 if ( callback ) {
1370 $sc.queue( function ( next ) {
1371 callback();
1372 next();
1373 } );
1374 }
1375 } else {
1376 if ( callback ) {
1377 callback();
1378 }
1379 }
1380 };
1381
1382 /**
1383 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1384 * and reserve space for them, because it probably doesn't.
1385 *
1386 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1387 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1388 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1389 * and then reattach (or show) them back.
1390 *
1391 * @static
1392 * @param {HTMLElement} el Element to reconsider the scrollbars on
1393 */
1394 OO.ui.Element.static.reconsiderScrollbars = function ( el ) {
1395 var i, len, nodes = [];
1396 // Detach all children
1397 while ( el.firstChild ) {
1398 nodes.push( el.firstChild );
1399 el.removeChild( el.firstChild );
1400 }
1401 // Force reflow
1402 void el.offsetHeight;
1403 // Reattach all children
1404 for ( i = 0, len = nodes.length; i < len; i++ ) {
1405 el.appendChild( nodes[ i ] );
1406 }
1407 };
1408
1409 /* Methods */
1410
1411 /**
1412 * Toggle visibility of an element.
1413 *
1414 * @param {boolean} [show] Make element visible, omit to toggle visibility
1415 * @fires visible
1416 * @chainable
1417 */
1418 OO.ui.Element.prototype.toggle = function ( show ) {
1419 show = show === undefined ? !this.visible : !!show;
1420
1421 if ( show !== this.isVisible() ) {
1422 this.visible = show;
1423 this.$element.toggleClass( 'oo-ui-element-hidden', !this.visible );
1424 this.emit( 'toggle', show );
1425 }
1426
1427 return this;
1428 };
1429
1430 /**
1431 * Check if element is visible.
1432 *
1433 * @return {boolean} element is visible
1434 */
1435 OO.ui.Element.prototype.isVisible = function () {
1436 return this.visible;
1437 };
1438
1439 /**
1440 * Get element data.
1441 *
1442 * @return {Mixed} Element data
1443 */
1444 OO.ui.Element.prototype.getData = function () {
1445 return this.data;
1446 };
1447
1448 /**
1449 * Set element data.
1450 *
1451 * @param {Mixed} Element data
1452 * @chainable
1453 */
1454 OO.ui.Element.prototype.setData = function ( data ) {
1455 this.data = data;
1456 return this;
1457 };
1458
1459 /**
1460 * Check if element supports one or more methods.
1461 *
1462 * @param {string|string[]} methods Method or list of methods to check
1463 * @return {boolean} All methods are supported
1464 */
1465 OO.ui.Element.prototype.supports = function ( methods ) {
1466 var i, len,
1467 support = 0;
1468
1469 methods = Array.isArray( methods ) ? methods : [ methods ];
1470 for ( i = 0, len = methods.length; i < len; i++ ) {
1471 if ( $.isFunction( this[ methods[ i ] ] ) ) {
1472 support++;
1473 }
1474 }
1475
1476 return methods.length === support;
1477 };
1478
1479 /**
1480 * Update the theme-provided classes.
1481 *
1482 * @localdoc This is called in element mixins and widget classes any time state changes.
1483 * Updating is debounced, minimizing overhead of changing multiple attributes and
1484 * guaranteeing that theme updates do not occur within an element's constructor
1485 */
1486 OO.ui.Element.prototype.updateThemeClasses = function () {
1487 if ( !this.updateThemeClassesPending ) {
1488 this.updateThemeClassesPending = true;
1489 setTimeout( this.debouncedUpdateThemeClassesHandler );
1490 }
1491 };
1492
1493 /**
1494 * @private
1495 */
1496 OO.ui.Element.prototype.debouncedUpdateThemeClasses = function () {
1497 OO.ui.theme.updateElementClasses( this );
1498 this.updateThemeClassesPending = false;
1499 };
1500
1501 /**
1502 * Get the HTML tag name.
1503 *
1504 * Override this method to base the result on instance information.
1505 *
1506 * @return {string} HTML tag name
1507 */
1508 OO.ui.Element.prototype.getTagName = function () {
1509 return this.constructor.static.tagName;
1510 };
1511
1512 /**
1513 * Check if the element is attached to the DOM
1514 * @return {boolean} The element is attached to the DOM
1515 */
1516 OO.ui.Element.prototype.isElementAttached = function () {
1517 return $.contains( this.getElementDocument(), this.$element[ 0 ] );
1518 };
1519
1520 /**
1521 * Get the DOM document.
1522 *
1523 * @return {HTMLDocument} Document object
1524 */
1525 OO.ui.Element.prototype.getElementDocument = function () {
1526 // Don't cache this in other ways either because subclasses could can change this.$element
1527 return OO.ui.Element.static.getDocument( this.$element );
1528 };
1529
1530 /**
1531 * Get the DOM window.
1532 *
1533 * @return {Window} Window object
1534 */
1535 OO.ui.Element.prototype.getElementWindow = function () {
1536 return OO.ui.Element.static.getWindow( this.$element );
1537 };
1538
1539 /**
1540 * Get closest scrollable container.
1541 */
1542 OO.ui.Element.prototype.getClosestScrollableElementContainer = function () {
1543 return OO.ui.Element.static.getClosestScrollableContainer( this.$element[ 0 ] );
1544 };
1545
1546 /**
1547 * Get group element is in.
1548 *
1549 * @return {OO.ui.GroupElement|null} Group element, null if none
1550 */
1551 OO.ui.Element.prototype.getElementGroup = function () {
1552 return this.elementGroup;
1553 };
1554
1555 /**
1556 * Set group element is in.
1557 *
1558 * @param {OO.ui.GroupElement|null} group Group element, null if none
1559 * @chainable
1560 */
1561 OO.ui.Element.prototype.setElementGroup = function ( group ) {
1562 this.elementGroup = group;
1563 return this;
1564 };
1565
1566 /**
1567 * Scroll element into view.
1568 *
1569 * @param {Object} [config] Configuration options
1570 */
1571 OO.ui.Element.prototype.scrollElementIntoView = function ( config ) {
1572 return OO.ui.Element.static.scrollIntoView( this.$element[ 0 ], config );
1573 };
1574
1575 /**
1576 * Container for elements.
1577 *
1578 * @abstract
1579 * @class
1580 * @extends OO.ui.Element
1581 * @mixins OO.EventEmitter
1582 *
1583 * @constructor
1584 * @param {Object} [config] Configuration options
1585 */
1586 OO.ui.Layout = function OoUiLayout( config ) {
1587 // Configuration initialization
1588 config = config || {};
1589
1590 // Parent constructor
1591 OO.ui.Layout.super.call( this, config );
1592
1593 // Mixin constructors
1594 OO.EventEmitter.call( this );
1595
1596 // Initialization
1597 this.$element.addClass( 'oo-ui-layout' );
1598 };
1599
1600 /* Setup */
1601
1602 OO.inheritClass( OO.ui.Layout, OO.ui.Element );
1603 OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
1604
1605 /**
1606 * Widgets are compositions of one or more OOjs UI elements that users can both view
1607 * and interact with. All widgets can be configured and modified via a standard API,
1608 * and their state can change dynamically according to a model.
1609 *
1610 * @abstract
1611 * @class
1612 * @extends OO.ui.Element
1613 * @mixins OO.EventEmitter
1614 *
1615 * @constructor
1616 * @param {Object} [config] Configuration options
1617 * @cfg {boolean} [disabled=false] Disable
1618 */
1619 OO.ui.Widget = function OoUiWidget( config ) {
1620 // Initialize config
1621 config = $.extend( { disabled: false }, config );
1622
1623 // Parent constructor
1624 OO.ui.Widget.super.call( this, config );
1625
1626 // Mixin constructors
1627 OO.EventEmitter.call( this );
1628
1629 // Properties
1630 this.disabled = null;
1631 this.wasDisabled = null;
1632
1633 // Initialization
1634 this.$element.addClass( 'oo-ui-widget' );
1635 this.setDisabled( !!config.disabled );
1636 };
1637
1638 /* Setup */
1639
1640 OO.inheritClass( OO.ui.Widget, OO.ui.Element );
1641 OO.mixinClass( OO.ui.Widget, OO.EventEmitter );
1642
1643 /* Events */
1644
1645 /**
1646 * @event disable
1647 * @param {boolean} disabled Widget is disabled
1648 */
1649
1650 /**
1651 * @event toggle
1652 * @param {boolean} visible Widget is visible
1653 */
1654
1655 /* Methods */
1656
1657 /**
1658 * Check if the widget is disabled.
1659 *
1660 * @return {boolean} Button is disabled
1661 */
1662 OO.ui.Widget.prototype.isDisabled = function () {
1663 return this.disabled;
1664 };
1665
1666 /**
1667 * Set the disabled state of the widget.
1668 *
1669 * This should probably change the widgets' appearance and prevent it from being used.
1670 *
1671 * @param {boolean} disabled Disable widget
1672 * @chainable
1673 */
1674 OO.ui.Widget.prototype.setDisabled = function ( disabled ) {
1675 var isDisabled;
1676
1677 this.disabled = !!disabled;
1678 isDisabled = this.isDisabled();
1679 if ( isDisabled !== this.wasDisabled ) {
1680 this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled );
1681 this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled );
1682 this.$element.attr( 'aria-disabled', isDisabled.toString() );
1683 this.emit( 'disable', isDisabled );
1684 this.updateThemeClasses();
1685 }
1686 this.wasDisabled = isDisabled;
1687
1688 return this;
1689 };
1690
1691 /**
1692 * Update the disabled state, in case of changes in parent widget.
1693 *
1694 * @chainable
1695 */
1696 OO.ui.Widget.prototype.updateDisabled = function () {
1697 this.setDisabled( this.disabled );
1698 return this;
1699 };
1700
1701 /**
1702 * A window is a container for elements that are in a child frame. They are used with
1703 * a window manager (OO.ui.WindowManager), which is used to open and close the window and control
1704 * its presentation. The size of a window is specified using a symbolic name (e.g., ‘small’, ‘medium’,
1705 * ‘large’), which is interpreted by the window manager. If the requested size is not recognized,
1706 * the window manager will choose a sensible fallback.
1707 *
1708 * The lifecycle of a window has three primary stages (opening, opened, and closing) in which
1709 * different processes are executed:
1710 *
1711 * **opening**: The opening stage begins when the window manager's {@link OO.ui.WindowManager#openWindow
1712 * openWindow} or the window's {@link #open open} methods are used, and the window manager begins to open
1713 * the window.
1714 *
1715 * - {@link #getSetupProcess} method is called and its result executed
1716 * - {@link #getReadyProcess} method is called and its result executed
1717 *
1718 * **opened**: The window is now open
1719 *
1720 * **closing**: The closing stage begins when the window manager's
1721 * {@link OO.ui.WindowManager#closeWindow closeWindow}
1722 * or the window's {@link #close} methods are used, and the window manager begins to close the window.
1723 *
1724 * - {@link #getHoldProcess} method is called and its result executed
1725 * - {@link #getTeardownProcess} method is called and its result executed. The window is now closed
1726 *
1727 * Each of the window's processes (setup, ready, hold, and teardown) can be extended in subclasses
1728 * by overriding the window's #getSetupProcess, #getReadyProcess, #getHoldProcess and #getTeardownProcess
1729 * methods. Note that each {@link OO.ui.Process process} is executed in series, so asynchronous
1730 * processing can complete. Always assume window processes are executed asynchronously.
1731 *
1732 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
1733 *
1734 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows
1735 *
1736 * @abstract
1737 * @class
1738 * @extends OO.ui.Element
1739 * @mixins OO.EventEmitter
1740 *
1741 * @constructor
1742 * @param {Object} [config] Configuration options
1743 * @cfg {string} [size] Symbolic name of the dialog size: `small`, `medium`, `large`, `larger` or
1744 * `full`. If omitted, the value of the {@link #static-size static size} property will be used.
1745 */
1746 OO.ui.Window = function OoUiWindow( config ) {
1747 // Configuration initialization
1748 config = config || {};
1749
1750 // Parent constructor
1751 OO.ui.Window.super.call( this, config );
1752
1753 // Mixin constructors
1754 OO.EventEmitter.call( this );
1755
1756 // Properties
1757 this.manager = null;
1758 this.size = config.size || this.constructor.static.size;
1759 this.frame = new OO.ui.PanelLayout( {
1760 expanded: false,
1761 framed: true
1762 } );
1763 this.$frame = this.frame.$element;
1764 this.$overlay = $( '<div>' );
1765 this.$content = $( '<div>' );
1766
1767 // Initialization
1768 this.$overlay.addClass( 'oo-ui-window-overlay' );
1769 this.$content
1770 .addClass( 'oo-ui-window-content' )
1771 .attr( 'tabIndex', 0 );
1772 this.$frame
1773 .addClass( 'oo-ui-window-frame' )
1774 .append( this.$content );
1775
1776 this.$element
1777 .addClass( 'oo-ui-window' )
1778 .append( this.$frame, this.$overlay );
1779
1780 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
1781 // that reference properties not initialized at that time of parent class construction
1782 // TODO: Find a better way to handle post-constructor setup
1783 this.visible = false;
1784 this.$element.addClass( 'oo-ui-element-hidden' );
1785 };
1786
1787 /* Setup */
1788
1789 OO.inheritClass( OO.ui.Window, OO.ui.Element );
1790 OO.mixinClass( OO.ui.Window, OO.EventEmitter );
1791
1792 /* Static Properties */
1793
1794 /**
1795 * Symbolic name of the window size: `small`, `medium`, `large`, `larger` or `full`.
1796 *
1797 * The static size is used if no #size is configured during construction.
1798 *
1799 * @static
1800 * @inheritable
1801 * @property {string}
1802 */
1803 OO.ui.Window.static.size = 'medium';
1804
1805 /* Methods */
1806
1807 /**
1808 * Handle mouse down events.
1809 *
1810 * @private
1811 * @param {jQuery.Event} e Mouse down event
1812 */
1813 OO.ui.Window.prototype.onMouseDown = function ( e ) {
1814 // Prevent clicking on the click-block from stealing focus
1815 if ( e.target === this.$element[ 0 ] ) {
1816 return false;
1817 }
1818 };
1819
1820 /**
1821 * Check if the window has been initialized.
1822 *
1823 * Initialization occurs when a window is added to a manager.
1824 *
1825 * @return {boolean} Window has been initialized
1826 */
1827 OO.ui.Window.prototype.isInitialized = function () {
1828 return !!this.manager;
1829 };
1830
1831 /**
1832 * Check if the window is visible.
1833 *
1834 * @return {boolean} Window is visible
1835 */
1836 OO.ui.Window.prototype.isVisible = function () {
1837 return this.visible;
1838 };
1839
1840 /**
1841 * Check if the window is opening.
1842 *
1843 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpening isOpening}
1844 * method.
1845 *
1846 * @return {boolean} Window is opening
1847 */
1848 OO.ui.Window.prototype.isOpening = function () {
1849 return this.manager.isOpening( this );
1850 };
1851
1852 /**
1853 * Check if the window is closing.
1854 *
1855 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isClosing isClosing} method.
1856 *
1857 * @return {boolean} Window is closing
1858 */
1859 OO.ui.Window.prototype.isClosing = function () {
1860 return this.manager.isClosing( this );
1861 };
1862
1863 /**
1864 * Check if the window is opened.
1865 *
1866 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpened isOpened} method.
1867 *
1868 * @return {boolean} Window is opened
1869 */
1870 OO.ui.Window.prototype.isOpened = function () {
1871 return this.manager.isOpened( this );
1872 };
1873
1874 /**
1875 * Get the window manager.
1876 *
1877 * All windows must be attached to a window manager, which is used to open
1878 * and close the window and control its presentation.
1879 *
1880 * @return {OO.ui.WindowManager} Manager of window
1881 */
1882 OO.ui.Window.prototype.getManager = function () {
1883 return this.manager;
1884 };
1885
1886 /**
1887 * Get the symbolic name of the window size (e.g., `small` or `medium`).
1888 *
1889 * @return {string} Symbolic name of the size: `small`, `medium`, `large`, `larger`, `full`
1890 */
1891 OO.ui.Window.prototype.getSize = function () {
1892 return this.size;
1893 };
1894
1895 /**
1896 * Disable transitions on window's frame for the duration of the callback function, then enable them
1897 * back.
1898 *
1899 * @private
1900 * @param {Function} callback Function to call while transitions are disabled
1901 */
1902 OO.ui.Window.prototype.withoutSizeTransitions = function ( callback ) {
1903 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
1904 // Disable transitions first, otherwise we'll get values from when the window was animating.
1905 var oldTransition,
1906 styleObj = this.$frame[ 0 ].style;
1907 oldTransition = styleObj.transition || styleObj.OTransition || styleObj.MsTransition ||
1908 styleObj.MozTransition || styleObj.WebkitTransition;
1909 styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
1910 styleObj.MozTransition = styleObj.WebkitTransition = 'none';
1911 callback();
1912 // Force reflow to make sure the style changes done inside callback really are not transitioned
1913 this.$frame.height();
1914 styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
1915 styleObj.MozTransition = styleObj.WebkitTransition = oldTransition;
1916 };
1917
1918 /**
1919 * Get the height of the full window contents (i.e., the window head, body and foot together).
1920 *
1921 * What consistitutes the head, body, and foot varies depending on the window type.
1922 * A {@link OO.ui.MessageDialog message dialog} displays a title and message in its body,
1923 * and any actions in the foot. A {@link OO.ui.ProcessDialog process dialog} displays a title
1924 * and special actions in the head, and dialog content in the body.
1925 *
1926 * To get just the height of the dialog body, use the #getBodyHeight method.
1927 *
1928 * @return {number} The height of the window contents (the dialog head, body and foot) in pixels
1929 */
1930 OO.ui.Window.prototype.getContentHeight = function () {
1931 var bodyHeight,
1932 win = this,
1933 bodyStyleObj = this.$body[ 0 ].style,
1934 frameStyleObj = this.$frame[ 0 ].style;
1935
1936 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
1937 // Disable transitions first, otherwise we'll get values from when the window was animating.
1938 this.withoutSizeTransitions( function () {
1939 var oldHeight = frameStyleObj.height,
1940 oldPosition = bodyStyleObj.position;
1941 frameStyleObj.height = '1px';
1942 // Force body to resize to new width
1943 bodyStyleObj.position = 'relative';
1944 bodyHeight = win.getBodyHeight();
1945 frameStyleObj.height = oldHeight;
1946 bodyStyleObj.position = oldPosition;
1947 } );
1948
1949 return (
1950 // Add buffer for border
1951 ( this.$frame.outerHeight() - this.$frame.innerHeight() ) +
1952 // Use combined heights of children
1953 ( this.$head.outerHeight( true ) + bodyHeight + this.$foot.outerHeight( true ) )
1954 );
1955 };
1956
1957 /**
1958 * Get the height of the window body.
1959 *
1960 * To get the height of the full window contents (the window body, head, and foot together),
1961 * use #getContentHeight.
1962 *
1963 * When this function is called, the window will temporarily have been resized
1964 * to height=1px, so .scrollHeight measurements can be taken accurately.
1965 *
1966 * @return {number} Height of the window body in pixels
1967 */
1968 OO.ui.Window.prototype.getBodyHeight = function () {
1969 return this.$body[ 0 ].scrollHeight;
1970 };
1971
1972 /**
1973 * Get the directionality of the frame (right-to-left or left-to-right).
1974 *
1975 * @return {string} Directionality: `'ltr'` or `'rtl'`
1976 */
1977 OO.ui.Window.prototype.getDir = function () {
1978 return this.dir;
1979 };
1980
1981 /**
1982 * Get the 'setup' process.
1983 *
1984 * The setup process is used to set up a window for use in a particular context,
1985 * based on the `data` argument. This method is called during the opening phase of the window’s
1986 * lifecycle.
1987 *
1988 * Override this method to add additional steps to the ‘setup’ process the parent method provides
1989 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
1990 * of OO.ui.Process.
1991 *
1992 * To add window content that persists between openings, you may wish to use the #initialize method
1993 * instead.
1994 *
1995 * @abstract
1996 * @param {Object} [data] Window opening data
1997 * @return {OO.ui.Process} Setup process
1998 */
1999 OO.ui.Window.prototype.getSetupProcess = function () {
2000 return new OO.ui.Process();
2001 };
2002
2003 /**
2004 * Get the ‘ready’ process.
2005 *
2006 * The ready process is used to ready a window for use in a particular
2007 * context, based on the `data` argument. This method is called during the opening phase of
2008 * the window’s lifecycle, after the window has been {@link #getSetupProcess setup}.
2009 *
2010 * Override this method to add additional steps to the ‘ready’ process the parent method
2011 * provides using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next}
2012 * methods of OO.ui.Process.
2013 *
2014 * @abstract
2015 * @param {Object} [data] Window opening data
2016 * @return {OO.ui.Process} Ready process
2017 */
2018 OO.ui.Window.prototype.getReadyProcess = function () {
2019 return new OO.ui.Process();
2020 };
2021
2022 /**
2023 * Get the 'hold' process.
2024 *
2025 * The hold proccess is used to keep a window from being used in a particular context,
2026 * based on the `data` argument. This method is called during the closing phase of the window’s
2027 * lifecycle.
2028 *
2029 * Override this method to add additional steps to the 'hold' process the parent method provides
2030 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2031 * of OO.ui.Process.
2032 *
2033 * @abstract
2034 * @param {Object} [data] Window closing data
2035 * @return {OO.ui.Process} Hold process
2036 */
2037 OO.ui.Window.prototype.getHoldProcess = function () {
2038 return new OO.ui.Process();
2039 };
2040
2041 /**
2042 * Get the ‘teardown’ process.
2043 *
2044 * The teardown process is used to teardown a window after use. During teardown,
2045 * user interactions within the window are conveyed and the window is closed, based on the `data`
2046 * argument. This method is called during the closing phase of the window’s lifecycle.
2047 *
2048 * Override this method to add additional steps to the ‘teardown’ process the parent method provides
2049 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2050 * of OO.ui.Process.
2051 *
2052 * @abstract
2053 * @param {Object} [data] Window closing data
2054 * @return {OO.ui.Process} Teardown process
2055 */
2056 OO.ui.Window.prototype.getTeardownProcess = function () {
2057 return new OO.ui.Process();
2058 };
2059
2060 /**
2061 * Set the window manager.
2062 *
2063 * This will cause the window to initialize. Calling it more than once will cause an error.
2064 *
2065 * @param {OO.ui.WindowManager} manager Manager for this window
2066 * @throws {Error} An error is thrown if the method is called more than once
2067 * @chainable
2068 */
2069 OO.ui.Window.prototype.setManager = function ( manager ) {
2070 if ( this.manager ) {
2071 throw new Error( 'Cannot set window manager, window already has a manager' );
2072 }
2073
2074 this.manager = manager;
2075 this.initialize();
2076
2077 return this;
2078 };
2079
2080 /**
2081 * Set the window size by symbolic name (e.g., 'small' or 'medium')
2082 *
2083 * @param {string} size Symbolic name of size: `small`, `medium`, `large`, `larger` or
2084 * `full`
2085 * @chainable
2086 */
2087 OO.ui.Window.prototype.setSize = function ( size ) {
2088 this.size = size;
2089 this.updateSize();
2090 return this;
2091 };
2092
2093 /**
2094 * Update the window size.
2095 *
2096 * @throws {Error} An error is thrown if the window is not attached to a window manager
2097 * @chainable
2098 */
2099 OO.ui.Window.prototype.updateSize = function () {
2100 if ( !this.manager ) {
2101 throw new Error( 'Cannot update window size, must be attached to a manager' );
2102 }
2103
2104 this.manager.updateWindowSize( this );
2105
2106 return this;
2107 };
2108
2109 /**
2110 * Set window dimensions.
2111 *
2112 * Properties are applied to the frame container.
2113 *
2114 * @param {Object} dim CSS dimension properties
2115 * @param {string|number} [dim.width] Width
2116 * @param {string|number} [dim.minWidth] Minimum width
2117 * @param {string|number} [dim.maxWidth] Maximum width
2118 * @param {string|number} [dim.width] Height, omit to set based on height of contents
2119 * @param {string|number} [dim.minWidth] Minimum height
2120 * @param {string|number} [dim.maxWidth] Maximum height
2121 * @chainable
2122 */
2123 OO.ui.Window.prototype.setDimensions = function ( dim ) {
2124 var height,
2125 win = this,
2126 styleObj = this.$frame[ 0 ].style;
2127
2128 // Calculate the height we need to set using the correct width
2129 if ( dim.height === undefined ) {
2130 this.withoutSizeTransitions( function () {
2131 var oldWidth = styleObj.width;
2132 win.$frame.css( 'width', dim.width || '' );
2133 height = win.getContentHeight();
2134 styleObj.width = oldWidth;
2135 } );
2136 } else {
2137 height = dim.height;
2138 }
2139
2140 this.$frame.css( {
2141 width: dim.width || '',
2142 minWidth: dim.minWidth || '',
2143 maxWidth: dim.maxWidth || '',
2144 height: height || '',
2145 minHeight: dim.minHeight || '',
2146 maxHeight: dim.maxHeight || ''
2147 } );
2148
2149 return this;
2150 };
2151
2152 /**
2153 * Initialize window contents.
2154 *
2155 * Before the window is opened for the first time, #initialize is called so that content that
2156 * persists between openings can be added to the window.
2157 *
2158 * To set up a window with new content each time the window opens, use #getSetupProcess.
2159 *
2160 * @throws {Error} An error is thrown if the window is not attached to a window manager
2161 * @chainable
2162 */
2163 OO.ui.Window.prototype.initialize = function () {
2164 if ( !this.manager ) {
2165 throw new Error( 'Cannot initialize window, must be attached to a manager' );
2166 }
2167
2168 // Properties
2169 this.$head = $( '<div>' );
2170 this.$body = $( '<div>' );
2171 this.$foot = $( '<div>' );
2172 this.dir = OO.ui.Element.static.getDir( this.$content ) || 'ltr';
2173 this.$document = $( this.getElementDocument() );
2174
2175 // Events
2176 this.$element.on( 'mousedown', this.onMouseDown.bind( this ) );
2177
2178 // Initialization
2179 this.$head.addClass( 'oo-ui-window-head' );
2180 this.$body.addClass( 'oo-ui-window-body' );
2181 this.$foot.addClass( 'oo-ui-window-foot' );
2182 this.$content.append( this.$head, this.$body, this.$foot );
2183
2184 return this;
2185 };
2186
2187 /**
2188 * Open the window.
2189 *
2190 * This method is a wrapper around a call to the window manager’s {@link OO.ui.WindowManager#openWindow openWindow}
2191 * method, which returns a promise resolved when the window is done opening.
2192 *
2193 * To customize the window each time it opens, use #getSetupProcess or #getReadyProcess.
2194 *
2195 * @param {Object} [data] Window opening data
2196 * @return {jQuery.Promise} Promise resolved with a value when the window is opened, or rejected
2197 * if the window fails to open. When the promise is resolved successfully, the first argument of the
2198 * value is a new promise, which is resolved when the window begins closing.
2199 * @throws {Error} An error is thrown if the window is not attached to a window manager
2200 */
2201 OO.ui.Window.prototype.open = function ( data ) {
2202 if ( !this.manager ) {
2203 throw new Error( 'Cannot open window, must be attached to a manager' );
2204 }
2205
2206 return this.manager.openWindow( this, data );
2207 };
2208
2209 /**
2210 * Close the window.
2211 *
2212 * This method is a wrapper around a call to the window
2213 * manager’s {@link OO.ui.WindowManager#closeWindow closeWindow} method,
2214 * which returns a closing promise resolved when the window is done closing.
2215 *
2216 * The window's #getHoldProcess and #getTeardownProcess methods are called during the closing
2217 * phase of the window’s lifecycle and can be used to specify closing behavior each time
2218 * the window closes.
2219 *
2220 * @param {Object} [data] Window closing data
2221 * @return {jQuery.Promise} Promise resolved when window is closed
2222 * @throws {Error} An error is thrown if the window is not attached to a window manager
2223 */
2224 OO.ui.Window.prototype.close = function ( data ) {
2225 if ( !this.manager ) {
2226 throw new Error( 'Cannot close window, must be attached to a manager' );
2227 }
2228
2229 return this.manager.closeWindow( this, data );
2230 };
2231
2232 /**
2233 * Setup window.
2234 *
2235 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2236 * by other systems.
2237 *
2238 * @param {Object} [data] Window opening data
2239 * @return {jQuery.Promise} Promise resolved when window is setup
2240 */
2241 OO.ui.Window.prototype.setup = function ( data ) {
2242 var win = this,
2243 deferred = $.Deferred();
2244
2245 this.toggle( true );
2246
2247 this.getSetupProcess( data ).execute().done( function () {
2248 // Force redraw by asking the browser to measure the elements' widths
2249 win.$element.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2250 win.$content.addClass( 'oo-ui-window-content-setup' ).width();
2251 deferred.resolve();
2252 } );
2253
2254 return deferred.promise();
2255 };
2256
2257 /**
2258 * Ready window.
2259 *
2260 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2261 * by other systems.
2262 *
2263 * @param {Object} [data] Window opening data
2264 * @return {jQuery.Promise} Promise resolved when window is ready
2265 */
2266 OO.ui.Window.prototype.ready = function ( data ) {
2267 var win = this,
2268 deferred = $.Deferred();
2269
2270 this.$content.focus();
2271 this.getReadyProcess( data ).execute().done( function () {
2272 // Force redraw by asking the browser to measure the elements' widths
2273 win.$element.addClass( 'oo-ui-window-ready' ).width();
2274 win.$content.addClass( 'oo-ui-window-content-ready' ).width();
2275 deferred.resolve();
2276 } );
2277
2278 return deferred.promise();
2279 };
2280
2281 /**
2282 * Hold window.
2283 *
2284 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2285 * by other systems.
2286 *
2287 * @param {Object} [data] Window closing data
2288 * @return {jQuery.Promise} Promise resolved when window is held
2289 */
2290 OO.ui.Window.prototype.hold = function ( data ) {
2291 var win = this,
2292 deferred = $.Deferred();
2293
2294 this.getHoldProcess( data ).execute().done( function () {
2295 // Get the focused element within the window's content
2296 var $focus = win.$content.find( OO.ui.Element.static.getDocument( win.$content ).activeElement );
2297
2298 // Blur the focused element
2299 if ( $focus.length ) {
2300 $focus[ 0 ].blur();
2301 }
2302
2303 // Force redraw by asking the browser to measure the elements' widths
2304 win.$element.removeClass( 'oo-ui-window-ready' ).width();
2305 win.$content.removeClass( 'oo-ui-window-content-ready' ).width();
2306 deferred.resolve();
2307 } );
2308
2309 return deferred.promise();
2310 };
2311
2312 /**
2313 * Teardown window.
2314 *
2315 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2316 * by other systems.
2317 *
2318 * @param {Object} [data] Window closing data
2319 * @return {jQuery.Promise} Promise resolved when window is torn down
2320 */
2321 OO.ui.Window.prototype.teardown = function ( data ) {
2322 var win = this;
2323
2324 return this.getTeardownProcess( data ).execute()
2325 .done( function () {
2326 // Force redraw by asking the browser to measure the elements' widths
2327 win.$element.removeClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2328 win.$content.removeClass( 'oo-ui-window-content-setup' ).width();
2329 win.toggle( false );
2330 } );
2331 };
2332
2333 /**
2334 * The Dialog class serves as the base class for the other types of dialogs.
2335 * Unless extended to include controls, the rendered dialog box is a simple window
2336 * that users can close by hitting the ‘Esc’ key. Dialog windows are used with OO.ui.WindowManager,
2337 * which opens, closes, and controls the presentation of the window. See the
2338 * [OOjs UI documentation on MediaWiki] [1] for more information.
2339 *
2340 * @example
2341 * // A simple dialog window.
2342 * function MyDialog( config ) {
2343 * MyDialog.super.call( this, config );
2344 * }
2345 * OO.inheritClass( MyDialog, OO.ui.Dialog );
2346 * MyDialog.prototype.initialize = function () {
2347 * MyDialog.super.prototype.initialize.call( this );
2348 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
2349 * this.content.$element.append( '<p>A simple dialog window. Press \'Esc\' to close.</p>' );
2350 * this.$body.append( this.content.$element );
2351 * };
2352 * MyDialog.prototype.getBodyHeight = function () {
2353 * return this.content.$element.outerHeight( true );
2354 * };
2355 * var myDialog = new MyDialog( {
2356 * size: 'medium'
2357 * } );
2358 * // Create and append a window manager, which opens and closes the window.
2359 * var windowManager = new OO.ui.WindowManager();
2360 * $( 'body' ).append( windowManager.$element );
2361 * windowManager.addWindows( [ myDialog ] );
2362 * // Open the window!
2363 * windowManager.openWindow( myDialog );
2364 *
2365 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Dialogs
2366 *
2367 * @abstract
2368 * @class
2369 * @extends OO.ui.Window
2370 * @mixins OO.ui.PendingElement
2371 *
2372 * @constructor
2373 * @param {Object} [config] Configuration options
2374 */
2375 OO.ui.Dialog = function OoUiDialog( config ) {
2376 // Parent constructor
2377 OO.ui.Dialog.super.call( this, config );
2378
2379 // Mixin constructors
2380 OO.ui.PendingElement.call( this );
2381
2382 // Properties
2383 this.actions = new OO.ui.ActionSet();
2384 this.attachedActions = [];
2385 this.currentAction = null;
2386 this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
2387
2388 // Events
2389 this.actions.connect( this, {
2390 click: 'onActionClick',
2391 resize: 'onActionResize',
2392 change: 'onActionsChange'
2393 } );
2394
2395 // Initialization
2396 this.$element
2397 .addClass( 'oo-ui-dialog' )
2398 .attr( 'role', 'dialog' );
2399 };
2400
2401 /* Setup */
2402
2403 OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
2404 OO.mixinClass( OO.ui.Dialog, OO.ui.PendingElement );
2405
2406 /* Static Properties */
2407
2408 /**
2409 * Symbolic name of dialog.
2410 *
2411 * The dialog class must have a symbolic name in order to be registered with OO.Factory.
2412 * Please see the [OOjs UI documentation on MediaWiki] [3] for more information.
2413 *
2414 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
2415 *
2416 * @abstract
2417 * @static
2418 * @inheritable
2419 * @property {string}
2420 */
2421 OO.ui.Dialog.static.name = '';
2422
2423 /**
2424 * The dialog title.
2425 *
2426 * The title can be specified as a plaintext string, a {@link OO.ui.LabelElement Label} node, or a function
2427 * that will produce a Label node or string. The title can also be specified with data passed to the
2428 * constructor (see #getSetupProcess). In this case, the static value will be overriden.
2429 *
2430 * @abstract
2431 * @static
2432 * @inheritable
2433 * @property {jQuery|string|Function}
2434 */
2435 OO.ui.Dialog.static.title = '';
2436
2437 /**
2438 * An array of configured {@link OO.ui.ActionWidget action widgets}.
2439 *
2440 * Actions can also be specified with data passed to the constructor (see #getSetupProcess). In this case, the static
2441 * value will be overriden.
2442 *
2443 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
2444 *
2445 * @static
2446 * @inheritable
2447 * @property {Object[]}
2448 */
2449 OO.ui.Dialog.static.actions = [];
2450
2451 /**
2452 * Close the dialog when the 'Esc' key is pressed.
2453 *
2454 * @static
2455 * @abstract
2456 * @inheritable
2457 * @property {boolean}
2458 */
2459 OO.ui.Dialog.static.escapable = true;
2460
2461 /* Methods */
2462
2463 /**
2464 * Handle frame document key down events.
2465 *
2466 * @private
2467 * @param {jQuery.Event} e Key down event
2468 */
2469 OO.ui.Dialog.prototype.onDocumentKeyDown = function ( e ) {
2470 if ( e.which === OO.ui.Keys.ESCAPE ) {
2471 this.close();
2472 e.preventDefault();
2473 e.stopPropagation();
2474 }
2475 };
2476
2477 /**
2478 * Handle action resized events.
2479 *
2480 * @private
2481 * @param {OO.ui.ActionWidget} action Action that was resized
2482 */
2483 OO.ui.Dialog.prototype.onActionResize = function () {
2484 // Override in subclass
2485 };
2486
2487 /**
2488 * Handle action click events.
2489 *
2490 * @private
2491 * @param {OO.ui.ActionWidget} action Action that was clicked
2492 */
2493 OO.ui.Dialog.prototype.onActionClick = function ( action ) {
2494 if ( !this.isPending() ) {
2495 this.executeAction( action.getAction() );
2496 }
2497 };
2498
2499 /**
2500 * Handle actions change event.
2501 *
2502 * @private
2503 */
2504 OO.ui.Dialog.prototype.onActionsChange = function () {
2505 this.detachActions();
2506 if ( !this.isClosing() ) {
2507 this.attachActions();
2508 }
2509 };
2510
2511 /**
2512 * Get the set of actions used by the dialog.
2513 *
2514 * @return {OO.ui.ActionSet}
2515 */
2516 OO.ui.Dialog.prototype.getActions = function () {
2517 return this.actions;
2518 };
2519
2520 /**
2521 * Get a process for taking action.
2522 *
2523 * When you override this method, you can create a new OO.ui.Process and return it, or add additional
2524 * accept steps to the process the parent method provides using the {@link OO.ui.Process#first 'first'}
2525 * and {@link OO.ui.Process#next 'next'} methods of OO.ui.Process.
2526 *
2527 * @abstract
2528 * @param {string} [action] Symbolic name of action
2529 * @return {OO.ui.Process} Action process
2530 */
2531 OO.ui.Dialog.prototype.getActionProcess = function ( action ) {
2532 return new OO.ui.Process()
2533 .next( function () {
2534 if ( !action ) {
2535 // An empty action always closes the dialog without data, which should always be
2536 // safe and make no changes
2537 this.close();
2538 }
2539 }, this );
2540 };
2541
2542 /**
2543 * @inheritdoc
2544 *
2545 * @param {Object} [data] Dialog opening data
2546 * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use #static-title
2547 * @param {Object[]} [data.actions] List of OO.ui.ActionWidget configuration options for each
2548 * action item, omit to use #static-actions
2549 */
2550 OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
2551 data = data || {};
2552
2553 // Parent method
2554 return OO.ui.Dialog.super.prototype.getSetupProcess.call( this, data )
2555 .next( function () {
2556 var i, len,
2557 items = [],
2558 config = this.constructor.static,
2559 actions = data.actions !== undefined ? data.actions : config.actions;
2560
2561 this.title.setLabel(
2562 data.title !== undefined ? data.title : this.constructor.static.title
2563 );
2564 for ( i = 0, len = actions.length; i < len; i++ ) {
2565 items.push(
2566 new OO.ui.ActionWidget( actions[ i ] )
2567 );
2568 }
2569 this.actions.add( items );
2570
2571 if ( this.constructor.static.escapable ) {
2572 this.$document.on( 'keydown', this.onDocumentKeyDownHandler );
2573 }
2574 }, this );
2575 };
2576
2577 /**
2578 * @inheritdoc
2579 */
2580 OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
2581 // Parent method
2582 return OO.ui.Dialog.super.prototype.getTeardownProcess.call( this, data )
2583 .first( function () {
2584 if ( this.constructor.static.escapable ) {
2585 this.$document.off( 'keydown', this.onDocumentKeyDownHandler );
2586 }
2587
2588 this.actions.clear();
2589 this.currentAction = null;
2590 }, this );
2591 };
2592
2593 /**
2594 * @inheritdoc
2595 */
2596 OO.ui.Dialog.prototype.initialize = function () {
2597 // Parent method
2598 OO.ui.Dialog.super.prototype.initialize.call( this );
2599
2600 // Properties
2601 this.title = new OO.ui.LabelWidget();
2602
2603 // Initialization
2604 this.$content.addClass( 'oo-ui-dialog-content' );
2605 this.setPendingElement( this.$head );
2606 };
2607
2608 /**
2609 * Attach action actions.
2610 *
2611 * @protected
2612 */
2613 OO.ui.Dialog.prototype.attachActions = function () {
2614 // Remember the list of potentially attached actions
2615 this.attachedActions = this.actions.get();
2616 };
2617
2618 /**
2619 * Detach action actions.
2620 *
2621 * @protected
2622 * @chainable
2623 */
2624 OO.ui.Dialog.prototype.detachActions = function () {
2625 var i, len;
2626
2627 // Detach all actions that may have been previously attached
2628 for ( i = 0, len = this.attachedActions.length; i < len; i++ ) {
2629 this.attachedActions[ i ].$element.detach();
2630 }
2631 this.attachedActions = [];
2632 };
2633
2634 /**
2635 * Execute an action.
2636 *
2637 * @param {string} action Symbolic name of action to execute
2638 * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
2639 */
2640 OO.ui.Dialog.prototype.executeAction = function ( action ) {
2641 this.pushPending();
2642 this.currentAction = action;
2643 return this.getActionProcess( action ).execute()
2644 .always( this.popPending.bind( this ) );
2645 };
2646
2647 /**
2648 * Window managers are used to open and close {@link OO.ui.Window windows} and control their presentation.
2649 * Managed windows are mutually exclusive. If a new window is opened while a current window is opening
2650 * or is opened, the current window will be closed and any ongoing {@link OO.ui.Process process} will be cancelled. Windows
2651 * themselves are persistent and—rather than being torn down when closed—can be repopulated with the
2652 * pertinent data and reused.
2653 *
2654 * Over the lifecycle of a window, the window manager makes available three promises: `opening`,
2655 * `opened`, and `closing`, which represent the primary stages of the cycle:
2656 *
2657 * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s
2658 * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window.
2659 *
2660 * - an `opening` event is emitted with an `opening` promise
2661 * - the #getSetupDelay method is called and the returned value is used to time a pause in execution before
2662 * the window’s {@link OO.ui.Window#getSetupProcess getSetupProcess} method is called on the
2663 * window and its result executed
2664 * - a `setup` progress notification is emitted from the `opening` promise
2665 * - the #getReadyDelay method is called the returned value is used to time a pause in execution before
2666 * the window’s {@link OO.ui.Window#getReadyProcess getReadyProcess} method is called on the
2667 * window and its result executed
2668 * - a `ready` progress notification is emitted from the `opening` promise
2669 * - the `opening` promise is resolved with an `opened` promise
2670 *
2671 * **Opened**: the window is now open.
2672 *
2673 * **Closing**: the closing stage begins when the window manager's #closeWindow or the
2674 * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins
2675 * to close the window.
2676 *
2677 * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted
2678 * - the #getHoldDelay method is called and the returned value is used to time a pause in execution before
2679 * the window's {@link OO.ui.Window#getHoldProcess getHoldProces} method is called on the
2680 * window and its result executed
2681 * - a `hold` progress notification is emitted from the `closing` promise
2682 * - the #getTeardownDelay() method is called and the returned value is used to time a pause in execution before
2683 * the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method is called on the
2684 * window and its result executed
2685 * - a `teardown` progress notification is emitted from the `closing` promise
2686 * - the `closing` promise is resolved. The window is now closed
2687 *
2688 * See the [OOjs UI documentation on MediaWiki][1] for more information.
2689 *
2690 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
2691 *
2692 * @class
2693 * @extends OO.ui.Element
2694 * @mixins OO.EventEmitter
2695 *
2696 * @constructor
2697 * @param {Object} [config] Configuration options
2698 * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
2699 * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
2700 */
2701 OO.ui.WindowManager = function OoUiWindowManager( config ) {
2702 // Configuration initialization
2703 config = config || {};
2704
2705 // Parent constructor
2706 OO.ui.WindowManager.super.call( this, config );
2707
2708 // Mixin constructors
2709 OO.EventEmitter.call( this );
2710
2711 // Properties
2712 this.factory = config.factory;
2713 this.modal = config.modal === undefined || !!config.modal;
2714 this.windows = {};
2715 this.opening = null;
2716 this.opened = null;
2717 this.closing = null;
2718 this.preparingToOpen = null;
2719 this.preparingToClose = null;
2720 this.currentWindow = null;
2721 this.globalEvents = false;
2722 this.$ariaHidden = null;
2723 this.onWindowResizeTimeout = null;
2724 this.onWindowResizeHandler = this.onWindowResize.bind( this );
2725 this.afterWindowResizeHandler = this.afterWindowResize.bind( this );
2726
2727 // Initialization
2728 this.$element
2729 .addClass( 'oo-ui-windowManager' )
2730 .toggleClass( 'oo-ui-windowManager-modal', this.modal );
2731 };
2732
2733 /* Setup */
2734
2735 OO.inheritClass( OO.ui.WindowManager, OO.ui.Element );
2736 OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter );
2737
2738 /* Events */
2739
2740 /**
2741 * Window is opening.
2742 *
2743 * Fired when the window begins to be opened.
2744 *
2745 * @event opening
2746 * @param {OO.ui.Window} win Window that's being opened
2747 * @param {jQuery.Promise} opening Promise resolved when window is opened; when the promise is
2748 * resolved the first argument will be a promise which will be resolved when the window begins
2749 * closing, the second argument will be the opening data; progress notifications will be fired on
2750 * the promise for `setup` and `ready` when those processes are completed respectively.
2751 * @param {Object} data Window opening data
2752 */
2753
2754 /**
2755 * Window is closing.
2756 *
2757 * Fired when the window begins to be closed.
2758 *
2759 * @event closing
2760 * @param {OO.ui.Window} win Window that's being closed
2761 * @param {jQuery.Promise} closing Promise resolved when window is closed; when the promise
2762 * is resolved the first argument will be a the closing data; progress notifications will be fired
2763 * on the promise for `hold` and `teardown` when those processes are completed respectively.
2764 * @param {Object} data Window closing data
2765 */
2766
2767 /**
2768 * Window was resized.
2769 *
2770 * @event resize
2771 * @param {OO.ui.Window} win Window that was resized
2772 */
2773
2774 /* Static Properties */
2775
2776 /**
2777 * Map of symbolic size names and CSS properties.
2778 *
2779 * @static
2780 * @inheritable
2781 * @property {Object}
2782 */
2783 OO.ui.WindowManager.static.sizes = {
2784 small: {
2785 width: 300
2786 },
2787 medium: {
2788 width: 500
2789 },
2790 large: {
2791 width: 700
2792 },
2793 larger: {
2794 width: 900
2795 },
2796 full: {
2797 // These can be non-numeric because they are never used in calculations
2798 width: '100%',
2799 height: '100%'
2800 }
2801 };
2802
2803 /**
2804 * Symbolic name of default size.
2805 *
2806 * Default size is used if the window's requested size is not recognized.
2807 *
2808 * @static
2809 * @inheritable
2810 * @property {string}
2811 */
2812 OO.ui.WindowManager.static.defaultSize = 'medium';
2813
2814 /* Methods */
2815
2816 /**
2817 * Handle window resize events.
2818 *
2819 * @param {jQuery.Event} e Window resize event
2820 */
2821 OO.ui.WindowManager.prototype.onWindowResize = function () {
2822 clearTimeout( this.onWindowResizeTimeout );
2823 this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 );
2824 };
2825
2826 /**
2827 * Handle window resize events.
2828 *
2829 * @param {jQuery.Event} e Window resize event
2830 */
2831 OO.ui.WindowManager.prototype.afterWindowResize = function () {
2832 if ( this.currentWindow ) {
2833 this.updateWindowSize( this.currentWindow );
2834 }
2835 };
2836
2837 /**
2838 * Check if window is opening.
2839 *
2840 * @return {boolean} Window is opening
2841 */
2842 OO.ui.WindowManager.prototype.isOpening = function ( win ) {
2843 return win === this.currentWindow && !!this.opening && this.opening.state() === 'pending';
2844 };
2845
2846 /**
2847 * Check if window is closing.
2848 *
2849 * @return {boolean} Window is closing
2850 */
2851 OO.ui.WindowManager.prototype.isClosing = function ( win ) {
2852 return win === this.currentWindow && !!this.closing && this.closing.state() === 'pending';
2853 };
2854
2855 /**
2856 * Check if window is opened.
2857 *
2858 * @return {boolean} Window is opened
2859 */
2860 OO.ui.WindowManager.prototype.isOpened = function ( win ) {
2861 return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending';
2862 };
2863
2864 /**
2865 * Check if a window is being managed.
2866 *
2867 * @param {OO.ui.Window} win Window to check
2868 * @return {boolean} Window is being managed
2869 */
2870 OO.ui.WindowManager.prototype.hasWindow = function ( win ) {
2871 var name;
2872
2873 for ( name in this.windows ) {
2874 if ( this.windows[ name ] === win ) {
2875 return true;
2876 }
2877 }
2878
2879 return false;
2880 };
2881
2882 /**
2883 * Get the number of milliseconds to wait between beginning opening and executing setup process.
2884 *
2885 * @param {OO.ui.Window} win Window being opened
2886 * @param {Object} [data] Window opening data
2887 * @return {number} Milliseconds to wait
2888 */
2889 OO.ui.WindowManager.prototype.getSetupDelay = function () {
2890 return 0;
2891 };
2892
2893 /**
2894 * Get the number of milliseconds to wait between finishing setup and executing ready process.
2895 *
2896 * @param {OO.ui.Window} win Window being opened
2897 * @param {Object} [data] Window opening data
2898 * @return {number} Milliseconds to wait
2899 */
2900 OO.ui.WindowManager.prototype.getReadyDelay = function () {
2901 return 0;
2902 };
2903
2904 /**
2905 * Get the number of milliseconds to wait between beginning closing and executing hold process.
2906 *
2907 * @param {OO.ui.Window} win Window being closed
2908 * @param {Object} [data] Window closing data
2909 * @return {number} Milliseconds to wait
2910 */
2911 OO.ui.WindowManager.prototype.getHoldDelay = function () {
2912 return 0;
2913 };
2914
2915 /**
2916 * Get the number of milliseconds to wait between finishing hold and executing teardown process.
2917 *
2918 * @param {OO.ui.Window} win Window being closed
2919 * @param {Object} [data] Window closing data
2920 * @return {number} Milliseconds to wait
2921 */
2922 OO.ui.WindowManager.prototype.getTeardownDelay = function () {
2923 return this.modal ? 250 : 0;
2924 };
2925
2926 /**
2927 * Get managed window by symbolic name.
2928 *
2929 * If window is not yet instantiated, it will be instantiated and added automatically.
2930 *
2931 * @param {string} name Symbolic window name
2932 * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
2933 * @throws {Error} If the symbolic name is unrecognized by the factory
2934 * @throws {Error} If the symbolic name unrecognized as a managed window
2935 */
2936 OO.ui.WindowManager.prototype.getWindow = function ( name ) {
2937 var deferred = $.Deferred(),
2938 win = this.windows[ name ];
2939
2940 if ( !( win instanceof OO.ui.Window ) ) {
2941 if ( this.factory ) {
2942 if ( !this.factory.lookup( name ) ) {
2943 deferred.reject( new OO.ui.Error(
2944 'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
2945 ) );
2946 } else {
2947 win = this.factory.create( name );
2948 this.addWindows( [ win ] );
2949 deferred.resolve( win );
2950 }
2951 } else {
2952 deferred.reject( new OO.ui.Error(
2953 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
2954 ) );
2955 }
2956 } else {
2957 deferred.resolve( win );
2958 }
2959
2960 return deferred.promise();
2961 };
2962
2963 /**
2964 * Get current window.
2965 *
2966 * @return {OO.ui.Window|null} Currently opening/opened/closing window
2967 */
2968 OO.ui.WindowManager.prototype.getCurrentWindow = function () {
2969 return this.currentWindow;
2970 };
2971
2972 /**
2973 * Open a window.
2974 *
2975 * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
2976 * @param {Object} [data] Window opening data
2977 * @return {jQuery.Promise} Promise resolved when window is done opening; see {@link #event-opening}
2978 * for more details about the `opening` promise
2979 * @fires opening
2980 */
2981 OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
2982 var manager = this,
2983 opening = $.Deferred();
2984
2985 // Argument handling
2986 if ( typeof win === 'string' ) {
2987 return this.getWindow( win ).then( function ( win ) {
2988 return manager.openWindow( win, data );
2989 } );
2990 }
2991
2992 // Error handling
2993 if ( !this.hasWindow( win ) ) {
2994 opening.reject( new OO.ui.Error(
2995 'Cannot open window: window is not attached to manager'
2996 ) );
2997 } else if ( this.preparingToOpen || this.opening || this.opened ) {
2998 opening.reject( new OO.ui.Error(
2999 'Cannot open window: another window is opening or open'
3000 ) );
3001 }
3002
3003 // Window opening
3004 if ( opening.state() !== 'rejected' ) {
3005 // If a window is currently closing, wait for it to complete
3006 this.preparingToOpen = $.when( this.closing );
3007 // Ensure handlers get called after preparingToOpen is set
3008 this.preparingToOpen.done( function () {
3009 if ( manager.modal ) {
3010 manager.toggleGlobalEvents( true );
3011 manager.toggleAriaIsolation( true );
3012 }
3013 manager.currentWindow = win;
3014 manager.opening = opening;
3015 manager.preparingToOpen = null;
3016 manager.emit( 'opening', win, opening, data );
3017 setTimeout( function () {
3018 win.setup( data ).then( function () {
3019 manager.updateWindowSize( win );
3020 manager.opening.notify( { state: 'setup' } );
3021 setTimeout( function () {
3022 win.ready( data ).then( function () {
3023 manager.opening.notify( { state: 'ready' } );
3024 manager.opening = null;
3025 manager.opened = $.Deferred();
3026 opening.resolve( manager.opened.promise(), data );
3027 } );
3028 }, manager.getReadyDelay() );
3029 } );
3030 }, manager.getSetupDelay() );
3031 } );
3032 }
3033
3034 return opening.promise();
3035 };
3036
3037 /**
3038 * Close a window.
3039 *
3040 * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
3041 * @param {Object} [data] Window closing data
3042 * @return {jQuery.Promise} Promise resolved when window is done closing; see {@link #event-closing}
3043 * for more details about the `closing` promise
3044 * @throws {Error} If no window by that name is being managed
3045 * @fires closing
3046 */
3047 OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
3048 var manager = this,
3049 closing = $.Deferred(),
3050 opened;
3051
3052 // Argument handling
3053 if ( typeof win === 'string' ) {
3054 win = this.windows[ win ];
3055 } else if ( !this.hasWindow( win ) ) {
3056 win = null;
3057 }
3058
3059 // Error handling
3060 if ( !win ) {
3061 closing.reject( new OO.ui.Error(
3062 'Cannot close window: window is not attached to manager'
3063 ) );
3064 } else if ( win !== this.currentWindow ) {
3065 closing.reject( new OO.ui.Error(
3066 'Cannot close window: window already closed with different data'
3067 ) );
3068 } else if ( this.preparingToClose || this.closing ) {
3069 closing.reject( new OO.ui.Error(
3070 'Cannot close window: window already closing with different data'
3071 ) );
3072 }
3073
3074 // Window closing
3075 if ( closing.state() !== 'rejected' ) {
3076 // If the window is currently opening, close it when it's done
3077 this.preparingToClose = $.when( this.opening );
3078 // Ensure handlers get called after preparingToClose is set
3079 this.preparingToClose.done( function () {
3080 manager.closing = closing;
3081 manager.preparingToClose = null;
3082 manager.emit( 'closing', win, closing, data );
3083 opened = manager.opened;
3084 manager.opened = null;
3085 opened.resolve( closing.promise(), data );
3086 setTimeout( function () {
3087 win.hold( data ).then( function () {
3088 closing.notify( { state: 'hold' } );
3089 setTimeout( function () {
3090 win.teardown( data ).then( function () {
3091 closing.notify( { state: 'teardown' } );
3092 if ( manager.modal ) {
3093 manager.toggleGlobalEvents( false );
3094 manager.toggleAriaIsolation( false );
3095 }
3096 manager.closing = null;
3097 manager.currentWindow = null;
3098 closing.resolve( data );
3099 } );
3100 }, manager.getTeardownDelay() );
3101 } );
3102 }, manager.getHoldDelay() );
3103 } );
3104 }
3105
3106 return closing.promise();
3107 };
3108
3109 /**
3110 * Add windows.
3111 *
3112 * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows Windows to add
3113 * @throws {Error} If one of the windows being added without an explicit symbolic name does not have
3114 * a statically configured symbolic name
3115 */
3116 OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
3117 var i, len, win, name, list;
3118
3119 if ( Array.isArray( windows ) ) {
3120 // Convert to map of windows by looking up symbolic names from static configuration
3121 list = {};
3122 for ( i = 0, len = windows.length; i < len; i++ ) {
3123 name = windows[ i ].constructor.static.name;
3124 if ( typeof name !== 'string' ) {
3125 throw new Error( 'Cannot add window' );
3126 }
3127 list[ name ] = windows[ i ];
3128 }
3129 } else if ( OO.isPlainObject( windows ) ) {
3130 list = windows;
3131 }
3132
3133 // Add windows
3134 for ( name in list ) {
3135 win = list[ name ];
3136 this.windows[ name ] = win.toggle( false );
3137 this.$element.append( win.$element );
3138 win.setManager( this );
3139 }
3140 };
3141
3142 /**
3143 * Remove windows.
3144 *
3145 * Windows will be closed before they are removed.
3146 *
3147 * @param {string[]} names Symbolic names of windows to remove
3148 * @return {jQuery.Promise} Promise resolved when window is closed and removed
3149 * @throws {Error} If windows being removed are not being managed
3150 */
3151 OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
3152 var i, len, win, name, cleanupWindow,
3153 manager = this,
3154 promises = [],
3155 cleanup = function ( name, win ) {
3156 delete manager.windows[ name ];
3157 win.$element.detach();
3158 };
3159
3160 for ( i = 0, len = names.length; i < len; i++ ) {
3161 name = names[ i ];
3162 win = this.windows[ name ];
3163 if ( !win ) {
3164 throw new Error( 'Cannot remove window' );
3165 }
3166 cleanupWindow = cleanup.bind( null, name, win );
3167 promises.push( this.closeWindow( name ).then( cleanupWindow, cleanupWindow ) );
3168 }
3169
3170 return $.when.apply( $, promises );
3171 };
3172
3173 /**
3174 * Remove all windows.
3175 *
3176 * Windows will be closed before they are removed.
3177 *
3178 * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
3179 */
3180 OO.ui.WindowManager.prototype.clearWindows = function () {
3181 return this.removeWindows( Object.keys( this.windows ) );
3182 };
3183
3184 /**
3185 * Set dialog size.
3186 *
3187 * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
3188 *
3189 * @chainable
3190 */
3191 OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) {
3192 // Bypass for non-current, and thus invisible, windows
3193 if ( win !== this.currentWindow ) {
3194 return;
3195 }
3196
3197 var viewport = OO.ui.Element.static.getDimensions( win.getElementWindow() ),
3198 sizes = this.constructor.static.sizes,
3199 size = win.getSize();
3200
3201 if ( !sizes[ size ] ) {
3202 size = this.constructor.static.defaultSize;
3203 }
3204 if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[ size ].width ) {
3205 size = 'full';
3206 }
3207
3208 this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', size === 'full' );
3209 this.$element.toggleClass( 'oo-ui-windowManager-floating', size !== 'full' );
3210 win.setDimensions( sizes[ size ] );
3211
3212 this.emit( 'resize', win );
3213
3214 return this;
3215 };
3216
3217 /**
3218 * Bind or unbind global events for scrolling.
3219 *
3220 * @param {boolean} [on] Bind global events
3221 * @chainable
3222 */
3223 OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) {
3224 on = on === undefined ? !!this.globalEvents : !!on;
3225
3226 var $body = $( this.getElementDocument().body ),
3227 // We could have multiple window managers open to only modify
3228 // the body class at the bottom of the stack
3229 stackDepth = $body.data( 'windowManagerGlobalEvents' ) || 0 ;
3230
3231 if ( on ) {
3232 if ( !this.globalEvents ) {
3233 $( this.getElementWindow() ).on( {
3234 // Start listening for top-level window dimension changes
3235 'orientationchange resize': this.onWindowResizeHandler
3236 } );
3237 if ( stackDepth === 0 ) {
3238 $body.css( 'overflow', 'hidden' );
3239 }
3240 stackDepth++;
3241 this.globalEvents = true;
3242 }
3243 } else if ( this.globalEvents ) {
3244 $( this.getElementWindow() ).off( {
3245 // Stop listening for top-level window dimension changes
3246 'orientationchange resize': this.onWindowResizeHandler
3247 } );
3248 stackDepth--;
3249 if ( stackDepth === 0 ) {
3250 $( this.getElementDocument().body ).css( 'overflow', '' );
3251 }
3252 this.globalEvents = false;
3253 }
3254 $body.data( 'windowManagerGlobalEvents', stackDepth );
3255
3256 return this;
3257 };
3258
3259 /**
3260 * Toggle screen reader visibility of content other than the window manager.
3261 *
3262 * @param {boolean} [isolate] Make only the window manager visible to screen readers
3263 * @chainable
3264 */
3265 OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) {
3266 isolate = isolate === undefined ? !this.$ariaHidden : !!isolate;
3267
3268 if ( isolate ) {
3269 if ( !this.$ariaHidden ) {
3270 // Hide everything other than the window manager from screen readers
3271 this.$ariaHidden = $( 'body' )
3272 .children()
3273 .not( this.$element.parentsUntil( 'body' ).last() )
3274 .attr( 'aria-hidden', '' );
3275 }
3276 } else if ( this.$ariaHidden ) {
3277 // Restore screen reader visibility
3278 this.$ariaHidden.removeAttr( 'aria-hidden' );
3279 this.$ariaHidden = null;
3280 }
3281
3282 return this;
3283 };
3284
3285 /**
3286 * Destroy window manager.
3287 */
3288 OO.ui.WindowManager.prototype.destroy = function () {
3289 this.toggleGlobalEvents( false );
3290 this.toggleAriaIsolation( false );
3291 this.clearWindows();
3292 this.$element.remove();
3293 };
3294
3295 /**
3296 * @class
3297 *
3298 * @constructor
3299 * @param {string|jQuery} message Description of error
3300 * @param {Object} [config] Configuration options
3301 * @cfg {boolean} [recoverable=true] Error is recoverable
3302 * @cfg {boolean} [warning=false] Whether this error is a warning or not.
3303 */
3304 OO.ui.Error = function OoUiError( message, config ) {
3305 // Allow passing positional parameters inside the config object
3306 if ( OO.isPlainObject( message ) && config === undefined ) {
3307 config = message;
3308 message = config.message;
3309 }
3310
3311 // Configuration initialization
3312 config = config || {};
3313
3314 // Properties
3315 this.message = message instanceof jQuery ? message : String( message );
3316 this.recoverable = config.recoverable === undefined || !!config.recoverable;
3317 this.warning = !!config.warning;
3318 };
3319
3320 /* Setup */
3321
3322 OO.initClass( OO.ui.Error );
3323
3324 /* Methods */
3325
3326 /**
3327 * Check if error can be recovered from.
3328 *
3329 * @return {boolean} Error is recoverable
3330 */
3331 OO.ui.Error.prototype.isRecoverable = function () {
3332 return this.recoverable;
3333 };
3334
3335 /**
3336 * Check if the error is a warning
3337 *
3338 * @return {boolean} Error is warning
3339 */
3340 OO.ui.Error.prototype.isWarning = function () {
3341 return this.warning;
3342 };
3343
3344 /**
3345 * Get error message as DOM nodes.
3346 *
3347 * @return {jQuery} Error message in DOM nodes
3348 */
3349 OO.ui.Error.prototype.getMessage = function () {
3350 return this.message instanceof jQuery ?
3351 this.message.clone() :
3352 $( '<div>' ).text( this.message ).contents();
3353 };
3354
3355 /**
3356 * Get error message as text.
3357 *
3358 * @return {string} Error message
3359 */
3360 OO.ui.Error.prototype.getMessageText = function () {
3361 return this.message instanceof jQuery ? this.message.text() : this.message;
3362 };
3363
3364 /**
3365 * Wraps an HTML snippet for use with configuration values which default
3366 * to strings. This bypasses the default html-escaping done to string
3367 * values.
3368 *
3369 * @class
3370 *
3371 * @constructor
3372 * @param {string} [content] HTML content
3373 */
3374 OO.ui.HtmlSnippet = function OoUiHtmlSnippet( content ) {
3375 // Properties
3376 this.content = content;
3377 };
3378
3379 /* Setup */
3380
3381 OO.initClass( OO.ui.HtmlSnippet );
3382
3383 /* Methods */
3384
3385 /**
3386 * Render into HTML.
3387 *
3388 * @return {string} Unchanged HTML snippet.
3389 */
3390 OO.ui.HtmlSnippet.prototype.toString = function () {
3391 return this.content;
3392 };
3393
3394 /**
3395 * A list of functions, called in sequence.
3396 *
3397 * If a function added to a process returns boolean false the process will stop; if it returns an
3398 * object with a `promise` method the process will use the promise to either continue to the next
3399 * step when the promise is resolved or stop when the promise is rejected.
3400 *
3401 * @class
3402 *
3403 * @constructor
3404 * @param {number|jQuery.Promise|Function} step Time to wait, promise to wait for or function to
3405 * call, see #createStep for more information
3406 * @param {Object} [context=null] Context to call the step function in, ignored if step is a number
3407 * or a promise
3408 * @return {Object} Step object, with `callback` and `context` properties
3409 */
3410 OO.ui.Process = function ( step, context ) {
3411 // Properties
3412 this.steps = [];
3413
3414 // Initialization
3415 if ( step !== undefined ) {
3416 this.next( step, context );
3417 }
3418 };
3419
3420 /* Setup */
3421
3422 OO.initClass( OO.ui.Process );
3423
3424 /* Methods */
3425
3426 /**
3427 * Start the process.
3428 *
3429 * @return {jQuery.Promise} Promise that is resolved when all steps have completed or rejected when
3430 * any of the steps return boolean false or a promise which gets rejected; upon stopping the
3431 * process, the remaining steps will not be taken
3432 */
3433 OO.ui.Process.prototype.execute = function () {
3434 var i, len, promise;
3435
3436 /**
3437 * Continue execution.
3438 *
3439 * @ignore
3440 * @param {Array} step A function and the context it should be called in
3441 * @return {Function} Function that continues the process
3442 */
3443 function proceed( step ) {
3444 return function () {
3445 // Execute step in the correct context
3446 var deferred,
3447 result = step.callback.call( step.context );
3448
3449 if ( result === false ) {
3450 // Use rejected promise for boolean false results
3451 return $.Deferred().reject( [] ).promise();
3452 }
3453 if ( typeof result === 'number' ) {
3454 if ( result < 0 ) {
3455 throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
3456 }
3457 // Use a delayed promise for numbers, expecting them to be in milliseconds
3458 deferred = $.Deferred();
3459 setTimeout( deferred.resolve, result );
3460 return deferred.promise();
3461 }
3462 if ( result instanceof OO.ui.Error ) {
3463 // Use rejected promise for error
3464 return $.Deferred().reject( [ result ] ).promise();
3465 }
3466 if ( Array.isArray( result ) && result.length && result[ 0 ] instanceof OO.ui.Error ) {
3467 // Use rejected promise for list of errors
3468 return $.Deferred().reject( result ).promise();
3469 }
3470 // Duck-type the object to see if it can produce a promise
3471 if ( result && $.isFunction( result.promise ) ) {
3472 // Use a promise generated from the result
3473 return result.promise();
3474 }
3475 // Use resolved promise for other results
3476 return $.Deferred().resolve().promise();
3477 };
3478 }
3479
3480 if ( this.steps.length ) {
3481 // Generate a chain reaction of promises
3482 promise = proceed( this.steps[ 0 ] )();
3483 for ( i = 1, len = this.steps.length; i < len; i++ ) {
3484 promise = promise.then( proceed( this.steps[ i ] ) );
3485 }
3486 } else {
3487 promise = $.Deferred().resolve().promise();
3488 }
3489
3490 return promise;
3491 };
3492
3493 /**
3494 * Create a process step.
3495 *
3496 * @private
3497 * @param {number|jQuery.Promise|Function} step
3498 *
3499 * - Number of milliseconds to wait; or
3500 * - Promise to wait to be resolved; or
3501 * - Function to execute
3502 * - If it returns boolean false the process will stop
3503 * - If it returns an object with a `promise` method the process will use the promise to either
3504 * continue to the next step when the promise is resolved or stop when the promise is rejected
3505 * - If it returns a number, the process will wait for that number of milliseconds before
3506 * proceeding
3507 * @param {Object} [context=null] Context to call the step function in, ignored if step is a number
3508 * or a promise
3509 * @return {Object} Step object, with `callback` and `context` properties
3510 */
3511 OO.ui.Process.prototype.createStep = function ( step, context ) {
3512 if ( typeof step === 'number' || $.isFunction( step.promise ) ) {
3513 return {
3514 callback: function () {
3515 return step;
3516 },
3517 context: null
3518 };
3519 }
3520 if ( $.isFunction( step ) ) {
3521 return {
3522 callback: step,
3523 context: context
3524 };
3525 }
3526 throw new Error( 'Cannot create process step: number, promise or function expected' );
3527 };
3528
3529 /**
3530 * Add step to the beginning of the process.
3531 *
3532 * @inheritdoc #createStep
3533 * @return {OO.ui.Process} this
3534 * @chainable
3535 */
3536 OO.ui.Process.prototype.first = function ( step, context ) {
3537 this.steps.unshift( this.createStep( step, context ) );
3538 return this;
3539 };
3540
3541 /**
3542 * Add step to the end of the process.
3543 *
3544 * @inheritdoc #createStep
3545 * @return {OO.ui.Process} this
3546 * @chainable
3547 */
3548 OO.ui.Process.prototype.next = function ( step, context ) {
3549 this.steps.push( this.createStep( step, context ) );
3550 return this;
3551 };
3552
3553 /**
3554 * Factory for tools.
3555 *
3556 * @class
3557 * @extends OO.Factory
3558 * @constructor
3559 */
3560 OO.ui.ToolFactory = function OoUiToolFactory() {
3561 // Parent constructor
3562 OO.ui.ToolFactory.super.call( this );
3563 };
3564
3565 /* Setup */
3566
3567 OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
3568
3569 /* Methods */
3570
3571 /**
3572 * Get tools from the factory
3573 *
3574 * @param {Array} include Included tools
3575 * @param {Array} exclude Excluded tools
3576 * @param {Array} promote Promoted tools
3577 * @param {Array} demote Demoted tools
3578 * @return {string[]} List of tools
3579 */
3580 OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
3581 var i, len, included, promoted, demoted,
3582 auto = [],
3583 used = {};
3584
3585 // Collect included and not excluded tools
3586 included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
3587
3588 // Promotion
3589 promoted = this.extract( promote, used );
3590 demoted = this.extract( demote, used );
3591
3592 // Auto
3593 for ( i = 0, len = included.length; i < len; i++ ) {
3594 if ( !used[ included[ i ] ] ) {
3595 auto.push( included[ i ] );
3596 }
3597 }
3598
3599 return promoted.concat( auto ).concat( demoted );
3600 };
3601
3602 /**
3603 * Get a flat list of names from a list of names or groups.
3604 *
3605 * Tools can be specified in the following ways:
3606 *
3607 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
3608 * - All tools in a group: `{ group: 'group-name' }`
3609 * - All tools: `'*'`
3610 *
3611 * @private
3612 * @param {Array|string} collection List of tools
3613 * @param {Object} [used] Object with names that should be skipped as properties; extracted
3614 * names will be added as properties
3615 * @return {string[]} List of extracted names
3616 */
3617 OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
3618 var i, len, item, name, tool,
3619 names = [];
3620
3621 if ( collection === '*' ) {
3622 for ( name in this.registry ) {
3623 tool = this.registry[ name ];
3624 if (
3625 // Only add tools by group name when auto-add is enabled
3626 tool.static.autoAddToCatchall &&
3627 // Exclude already used tools
3628 ( !used || !used[ name ] )
3629 ) {
3630 names.push( name );
3631 if ( used ) {
3632 used[ name ] = true;
3633 }
3634 }
3635 }
3636 } else if ( Array.isArray( collection ) ) {
3637 for ( i = 0, len = collection.length; i < len; i++ ) {
3638 item = collection[ i ];
3639 // Allow plain strings as shorthand for named tools
3640 if ( typeof item === 'string' ) {
3641 item = { name: item };
3642 }
3643 if ( OO.isPlainObject( item ) ) {
3644 if ( item.group ) {
3645 for ( name in this.registry ) {
3646 tool = this.registry[ name ];
3647 if (
3648 // Include tools with matching group
3649 tool.static.group === item.group &&
3650 // Only add tools by group name when auto-add is enabled
3651 tool.static.autoAddToGroup &&
3652 // Exclude already used tools
3653 ( !used || !used[ name ] )
3654 ) {
3655 names.push( name );
3656 if ( used ) {
3657 used[ name ] = true;
3658 }
3659 }
3660 }
3661 // Include tools with matching name and exclude already used tools
3662 } else if ( item.name && ( !used || !used[ item.name ] ) ) {
3663 names.push( item.name );
3664 if ( used ) {
3665 used[ item.name ] = true;
3666 }
3667 }
3668 }
3669 }
3670 }
3671 return names;
3672 };
3673
3674 /**
3675 * Factory for tool groups.
3676 *
3677 * @class
3678 * @extends OO.Factory
3679 * @constructor
3680 */
3681 OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() {
3682 // Parent constructor
3683 OO.Factory.call( this );
3684
3685 var i, l,
3686 defaultClasses = this.constructor.static.getDefaultClasses();
3687
3688 // Register default toolgroups
3689 for ( i = 0, l = defaultClasses.length; i < l; i++ ) {
3690 this.register( defaultClasses[ i ] );
3691 }
3692 };
3693
3694 /* Setup */
3695
3696 OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory );
3697
3698 /* Static Methods */
3699
3700 /**
3701 * Get a default set of classes to be registered on construction
3702 *
3703 * @return {Function[]} Default classes
3704 */
3705 OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
3706 return [
3707 OO.ui.BarToolGroup,
3708 OO.ui.ListToolGroup,
3709 OO.ui.MenuToolGroup
3710 ];
3711 };
3712
3713 /**
3714 * Theme logic.
3715 *
3716 * @abstract
3717 * @class
3718 *
3719 * @constructor
3720 * @param {Object} [config] Configuration options
3721 */
3722 OO.ui.Theme = function OoUiTheme( config ) {
3723 // Configuration initialization
3724 config = config || {};
3725 };
3726
3727 /* Setup */
3728
3729 OO.initClass( OO.ui.Theme );
3730
3731 /* Methods */
3732
3733 /**
3734 * Get a list of classes to be applied to a widget.
3735 *
3736 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
3737 * otherwise state transitions will not work properly.
3738 *
3739 * @param {OO.ui.Element} element Element for which to get classes
3740 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
3741 */
3742 OO.ui.Theme.prototype.getElementClasses = function ( /* element */ ) {
3743 return { on: [], off: [] };
3744 };
3745
3746 /**
3747 * Update CSS classes provided by the theme.
3748 *
3749 * For elements with theme logic hooks, this should be called any time there's a state change.
3750 *
3751 * @param {OO.ui.Element} element Element for which to update classes
3752 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
3753 */
3754 OO.ui.Theme.prototype.updateElementClasses = function ( element ) {
3755 var classes = this.getElementClasses( element );
3756
3757 element.$element
3758 .removeClass( classes.off.join( ' ' ) )
3759 .addClass( classes.on.join( ' ' ) );
3760 };
3761
3762 /**
3763 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
3764 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
3765 * order in which users will navigate through the focusable elements via the "tab" key.
3766 *
3767 * @example
3768 * // TabIndexedElement is mixed into the ButtonWidget class
3769 * // to provide a tabIndex property.
3770 * var button1 = new OO.ui.ButtonWidget( {
3771 * label : 'fourth',
3772 * tabIndex : 4
3773 * } );
3774 * var button2 = new OO.ui.ButtonWidget( {
3775 * label : 'second',
3776 * tabIndex : 2
3777 * } );
3778 * var button3 = new OO.ui.ButtonWidget( {
3779 * label : 'third',
3780 * tabIndex : 3
3781 * } );
3782 * var button4 = new OO.ui.ButtonWidget( {
3783 * label : 'first',
3784 * tabIndex : 1
3785 * } );
3786 * $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
3787 *
3788 * @abstract
3789 * @class
3790 *
3791 * @constructor
3792 * @param {Object} [config] Configuration options
3793 * @cfg {jQuery} [$tabIndexed] tabIndexed node, assigned to #$tabIndexed, omit to use #$element
3794 * @cfg {number|null} [tabIndex=0] Tab index value. Use 0 to use default ordering, use -1 to
3795 * prevent tab focusing, use null to suppress the `tabindex` attribute.
3796 */
3797 OO.ui.TabIndexedElement = function OoUiTabIndexedElement( config ) {
3798 // Configuration initialization
3799 config = $.extend( { tabIndex: 0 }, config );
3800
3801 // Properties
3802 this.$tabIndexed = null;
3803 this.tabIndex = null;
3804
3805 // Events
3806 this.connect( this, { disable: 'onDisable' } );
3807
3808 // Initialization
3809 this.setTabIndex( config.tabIndex );
3810 this.setTabIndexedElement( config.$tabIndexed || this.$element );
3811 };
3812
3813 /* Setup */
3814
3815 OO.initClass( OO.ui.TabIndexedElement );
3816
3817 /* Methods */
3818
3819 /**
3820 * Set the element with `tabindex` attribute.
3821 *
3822 * If an element is already set, it will be cleaned up before setting up the new element.
3823 *
3824 * @param {jQuery} $tabIndexed Element to set tab index on
3825 * @chainable
3826 */
3827 OO.ui.TabIndexedElement.prototype.setTabIndexedElement = function ( $tabIndexed ) {
3828 var tabIndex = this.tabIndex;
3829 // Remove attributes from old $tabIndexed
3830 this.setTabIndex( null );
3831 // Force update of new $tabIndexed
3832 this.$tabIndexed = $tabIndexed;
3833 this.tabIndex = tabIndex;
3834 return this.updateTabIndex();
3835 };
3836
3837 /**
3838 * Set tab index value.
3839 *
3840 * @param {number|null} tabIndex Tab index value or null for no tab index
3841 * @chainable
3842 */
3843 OO.ui.TabIndexedElement.prototype.setTabIndex = function ( tabIndex ) {
3844 tabIndex = typeof tabIndex === 'number' ? tabIndex : null;
3845
3846 if ( this.tabIndex !== tabIndex ) {
3847 this.tabIndex = tabIndex;
3848 this.updateTabIndex();
3849 }
3850
3851 return this;
3852 };
3853
3854 /**
3855 * Update the `tabindex` attribute, in case of changes to tab index or
3856 * disabled state.
3857 *
3858 * @chainable
3859 */
3860 OO.ui.TabIndexedElement.prototype.updateTabIndex = function () {
3861 if ( this.$tabIndexed ) {
3862 if ( this.tabIndex !== null ) {
3863 // Do not index over disabled elements
3864 this.$tabIndexed.attr( {
3865 tabindex: this.isDisabled() ? -1 : this.tabIndex,
3866 // ChromeVox and NVDA do not seem to inherit this from parent elements
3867 'aria-disabled': this.isDisabled().toString()
3868 } );
3869 } else {
3870 this.$tabIndexed.removeAttr( 'tabindex aria-disabled' );
3871 }
3872 }
3873 return this;
3874 };
3875
3876 /**
3877 * Handle disable events.
3878 *
3879 * @private
3880 * @param {boolean} disabled Element is disabled
3881 */
3882 OO.ui.TabIndexedElement.prototype.onDisable = function () {
3883 this.updateTabIndex();
3884 };
3885
3886 /**
3887 * Get tab index value.
3888 *
3889 * @return {number|null} Tab index value
3890 */
3891 OO.ui.TabIndexedElement.prototype.getTabIndex = function () {
3892 return this.tabIndex;
3893 };
3894
3895 /**
3896 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
3897 * interface element that can be configured with access keys for accessibility.
3898 * See the [OOjs UI documentation on MediaWiki] [1] for examples.
3899 *
3900 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
3901 * @abstract
3902 * @class
3903 *
3904 * @constructor
3905 * @param {Object} [config] Configuration options
3906 * @cfg {jQuery} [$button] Button node, assigned to #$button, omit to use a generated `<a>`
3907 * @cfg {boolean} [framed=true] Render button with a frame
3908 * @cfg {string} [accessKey] Button's access key
3909 */
3910 OO.ui.ButtonElement = function OoUiButtonElement( config ) {
3911 // Configuration initialization
3912 config = config || {};
3913
3914 // Properties
3915 this.$button = null;
3916 this.framed = null;
3917 this.accessKey = null;
3918 this.active = false;
3919 this.onMouseUpHandler = this.onMouseUp.bind( this );
3920 this.onMouseDownHandler = this.onMouseDown.bind( this );
3921 this.onKeyDownHandler = this.onKeyDown.bind( this );
3922 this.onKeyUpHandler = this.onKeyUp.bind( this );
3923 this.onClickHandler = this.onClick.bind( this );
3924 this.onKeyPressHandler = this.onKeyPress.bind( this );
3925
3926 // Initialization
3927 this.$element.addClass( 'oo-ui-buttonElement' );
3928 this.toggleFramed( config.framed === undefined || config.framed );
3929 this.setAccessKey( config.accessKey );
3930 this.setButtonElement( config.$button || $( '<a>' ) );
3931 };
3932
3933 /* Setup */
3934
3935 OO.initClass( OO.ui.ButtonElement );
3936
3937 /* Static Properties */
3938
3939 /**
3940 * Cancel mouse down events.
3941 *
3942 * @static
3943 * @inheritable
3944 * @property {boolean}
3945 */
3946 OO.ui.ButtonElement.static.cancelButtonMouseDownEvents = true;
3947
3948 /* Events */
3949
3950 /**
3951 * @event click
3952 */
3953
3954 /* Methods */
3955
3956 /**
3957 * Set the button element.
3958 *
3959 * If an element is already set, it will be cleaned up before setting up the new element.
3960 *
3961 * @param {jQuery} $button Element to use as button
3962 */
3963 OO.ui.ButtonElement.prototype.setButtonElement = function ( $button ) {
3964 if ( this.$button ) {
3965 this.$button
3966 .removeClass( 'oo-ui-buttonElement-button' )
3967 .removeAttr( 'role accesskey' )
3968 .off( {
3969 mousedown: this.onMouseDownHandler,
3970 keydown: this.onKeyDownHandler,
3971 click: this.onClickHandler,
3972 keypress: this.onKeyPressHandler
3973 } );
3974 }
3975
3976 this.$button = $button
3977 .addClass( 'oo-ui-buttonElement-button' )
3978 .attr( { role: 'button', accesskey: this.accessKey } )
3979 .on( {
3980 mousedown: this.onMouseDownHandler,
3981 keydown: this.onKeyDownHandler,
3982 click: this.onClickHandler,
3983 keypress: this.onKeyPressHandler
3984 } );
3985 };
3986
3987 /**
3988 * Handles mouse down events.
3989 *
3990 * @protected
3991 * @param {jQuery.Event} e Mouse down event
3992 */
3993 OO.ui.ButtonElement.prototype.onMouseDown = function ( e ) {
3994 if ( this.isDisabled() || e.which !== 1 ) {
3995 return;
3996 }
3997 this.$element.addClass( 'oo-ui-buttonElement-pressed' );
3998 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
3999 // reliably remove the pressed class
4000 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
4001 // Prevent change of focus unless specifically configured otherwise
4002 if ( this.constructor.static.cancelButtonMouseDownEvents ) {
4003 return false;
4004 }
4005 };
4006
4007 /**
4008 * Handles mouse up events.
4009 *
4010 * @protected
4011 * @param {jQuery.Event} e Mouse up event
4012 */
4013 OO.ui.ButtonElement.prototype.onMouseUp = function ( e ) {
4014 if ( this.isDisabled() || e.which !== 1 ) {
4015 return;
4016 }
4017 this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
4018 // Stop listening for mouseup, since we only needed this once
4019 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
4020 };
4021
4022 /**
4023 * Handles mouse click events.
4024 *
4025 * @protected
4026 * @param {jQuery.Event} e Mouse click event
4027 * @fires click
4028 */
4029 OO.ui.ButtonElement.prototype.onClick = function ( e ) {
4030 if ( !this.isDisabled() && e.which === 1 ) {
4031 this.emit( 'click' );
4032 }
4033 return false;
4034 };
4035
4036 /**
4037 * Handles key down events.
4038 *
4039 * @protected
4040 * @param {jQuery.Event} e Key down event
4041 */
4042 OO.ui.ButtonElement.prototype.onKeyDown = function ( e ) {
4043 if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
4044 return;
4045 }
4046 this.$element.addClass( 'oo-ui-buttonElement-pressed' );
4047 // Run the keyup handler no matter where the key is when the button is let go, so we can
4048 // reliably remove the pressed class
4049 this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler, true );
4050 };
4051
4052 /**
4053 * Handles key up events.
4054 *
4055 * @protected
4056 * @param {jQuery.Event} e Key up event
4057 */
4058 OO.ui.ButtonElement.prototype.onKeyUp = function ( e ) {
4059 if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
4060 return;
4061 }
4062 this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
4063 // Stop listening for keyup, since we only needed this once
4064 this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler, true );
4065 };
4066
4067 /**
4068 * Handles key press events.
4069 *
4070 * @protected
4071 * @param {jQuery.Event} e Key press event
4072 * @fires click
4073 */
4074 OO.ui.ButtonElement.prototype.onKeyPress = function ( e ) {
4075 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
4076 this.emit( 'click' );
4077 return false;
4078 }
4079 };
4080
4081 /**
4082 * Check if button has a frame.
4083 *
4084 * @return {boolean} Button is framed
4085 */
4086 OO.ui.ButtonElement.prototype.isFramed = function () {
4087 return this.framed;
4088 };
4089
4090 /**
4091 * Toggle frame.
4092 *
4093 * @param {boolean} [framed] Make button framed, omit to toggle
4094 * @chainable
4095 */
4096 OO.ui.ButtonElement.prototype.toggleFramed = function ( framed ) {
4097 framed = framed === undefined ? !this.framed : !!framed;
4098 if ( framed !== this.framed ) {
4099 this.framed = framed;
4100 this.$element
4101 .toggleClass( 'oo-ui-buttonElement-frameless', !framed )
4102 .toggleClass( 'oo-ui-buttonElement-framed', framed );
4103 this.updateThemeClasses();
4104 }
4105
4106 return this;
4107 };
4108
4109 /**
4110 * Set access key.
4111 *
4112 * @param {string} accessKey Button's access key, use empty string to remove
4113 * @chainable
4114 */
4115 OO.ui.ButtonElement.prototype.setAccessKey = function ( accessKey ) {
4116 accessKey = typeof accessKey === 'string' && accessKey.length ? accessKey : null;
4117
4118 if ( this.accessKey !== accessKey ) {
4119 if ( this.$button ) {
4120 if ( accessKey !== null ) {
4121 this.$button.attr( 'accesskey', accessKey );
4122 } else {
4123 this.$button.removeAttr( 'accesskey' );
4124 }
4125 }
4126 this.accessKey = accessKey;
4127 }
4128
4129 return this;
4130 };
4131
4132 /**
4133 * Set active state.
4134 *
4135 * @param {boolean} [value] Make button active
4136 * @chainable
4137 */
4138 OO.ui.ButtonElement.prototype.setActive = function ( value ) {
4139 this.$element.toggleClass( 'oo-ui-buttonElement-active', !!value );
4140 return this;
4141 };
4142
4143 /**
4144 * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
4145 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
4146 * items from the group is done through the interface the class provides.
4147 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
4148 *
4149 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
4150 *
4151 * @abstract
4152 * @class
4153 *
4154 * @constructor
4155 * @param {Object} [config] Configuration options
4156 * @cfg {jQuery} [$group] Container node, assigned to #$group, omit to use a generated `<div>`
4157 */
4158 OO.ui.GroupElement = function OoUiGroupElement( config ) {
4159 // Configuration initialization
4160 config = config || {};
4161
4162 // Properties
4163 this.$group = null;
4164 this.items = [];
4165 this.aggregateItemEvents = {};
4166
4167 // Initialization
4168 this.setGroupElement( config.$group || $( '<div>' ) );
4169 };
4170
4171 /* Methods */
4172
4173 /**
4174 * Set the group element.
4175 *
4176 * If an element is already set, items will be moved to the new element.
4177 *
4178 * @param {jQuery} $group Element to use as group
4179 */
4180 OO.ui.GroupElement.prototype.setGroupElement = function ( $group ) {
4181 var i, len;
4182
4183 this.$group = $group;
4184 for ( i = 0, len = this.items.length; i < len; i++ ) {
4185 this.$group.append( this.items[ i ].$element );
4186 }
4187 };
4188
4189 /**
4190 * Check if there are no items.
4191 *
4192 * @return {boolean} Group is empty
4193 */
4194 OO.ui.GroupElement.prototype.isEmpty = function () {
4195 return !this.items.length;
4196 };
4197
4198 /**
4199 * Get items.
4200 *
4201 * @return {OO.ui.Element[]} Items
4202 */
4203 OO.ui.GroupElement.prototype.getItems = function () {
4204 return this.items.slice( 0 );
4205 };
4206
4207 /**
4208 * Get an item by its data.
4209 *
4210 * Data is compared by a hash of its value. Only the first item with matching data will be returned.
4211 *
4212 * @param {Object} data Item data to search for
4213 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
4214 */
4215 OO.ui.GroupElement.prototype.getItemFromData = function ( data ) {
4216 var i, len, item,
4217 hash = OO.getHash( data );
4218
4219 for ( i = 0, len = this.items.length; i < len; i++ ) {
4220 item = this.items[ i ];
4221 if ( hash === OO.getHash( item.getData() ) ) {
4222 return item;
4223 }
4224 }
4225
4226 return null;
4227 };
4228
4229 /**
4230 * Get items by their data.
4231 *
4232 * Data is compared by a hash of its value. All items with matching data will be returned.
4233 *
4234 * @param {Object} data Item data to search for
4235 * @return {OO.ui.Element[]} Items with equivalent data
4236 */
4237 OO.ui.GroupElement.prototype.getItemsFromData = function ( data ) {
4238 var i, len, item,
4239 hash = OO.getHash( data ),
4240 items = [];
4241
4242 for ( i = 0, len = this.items.length; i < len; i++ ) {
4243 item = this.items[ i ];
4244 if ( hash === OO.getHash( item.getData() ) ) {
4245 items.push( item );
4246 }
4247 }
4248
4249 return items;
4250 };
4251
4252 /**
4253 * Add an aggregate item event.
4254 *
4255 * Aggregated events are listened to on each item and then emitted by the group under a new name,
4256 * and with an additional leading parameter containing the item that emitted the original event.
4257 * Other arguments that were emitted from the original event are passed through.
4258 *
4259 * @param {Object.<string,string|null>} events Aggregate events emitted by group, keyed by item
4260 * event, use null value to remove aggregation
4261 * @throws {Error} If aggregation already exists
4262 */
4263 OO.ui.GroupElement.prototype.aggregate = function ( events ) {
4264 var i, len, item, add, remove, itemEvent, groupEvent;
4265
4266 for ( itemEvent in events ) {
4267 groupEvent = events[ itemEvent ];
4268
4269 // Remove existing aggregated event
4270 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4271 // Don't allow duplicate aggregations
4272 if ( groupEvent ) {
4273 throw new Error( 'Duplicate item event aggregation for ' + itemEvent );
4274 }
4275 // Remove event aggregation from existing items
4276 for ( i = 0, len = this.items.length; i < len; i++ ) {
4277 item = this.items[ i ];
4278 if ( item.connect && item.disconnect ) {
4279 remove = {};
4280 remove[ itemEvent ] = [ 'emit', groupEvent, item ];
4281 item.disconnect( this, remove );
4282 }
4283 }
4284 // Prevent future items from aggregating event
4285 delete this.aggregateItemEvents[ itemEvent ];
4286 }
4287
4288 // Add new aggregate event
4289 if ( groupEvent ) {
4290 // Make future items aggregate event
4291 this.aggregateItemEvents[ itemEvent ] = groupEvent;
4292 // Add event aggregation to existing items
4293 for ( i = 0, len = this.items.length; i < len; i++ ) {
4294 item = this.items[ i ];
4295 if ( item.connect && item.disconnect ) {
4296 add = {};
4297 add[ itemEvent ] = [ 'emit', groupEvent, item ];
4298 item.connect( this, add );
4299 }
4300 }
4301 }
4302 }
4303 };
4304
4305 /**
4306 * Add items.
4307 *
4308 * Adding an existing item will move it.
4309 *
4310 * @param {OO.ui.Element[]} items Items
4311 * @param {number} [index] Index to insert items at
4312 * @chainable
4313 */
4314 OO.ui.GroupElement.prototype.addItems = function ( items, index ) {
4315 var i, len, item, event, events, currentIndex,
4316 itemElements = [];
4317
4318 for ( i = 0, len = items.length; i < len; i++ ) {
4319 item = items[ i ];
4320
4321 // Check if item exists then remove it first, effectively "moving" it
4322 currentIndex = $.inArray( item, this.items );
4323 if ( currentIndex >= 0 ) {
4324 this.removeItems( [ item ] );
4325 // Adjust index to compensate for removal
4326 if ( currentIndex < index ) {
4327 index--;
4328 }
4329 }
4330 // Add the item
4331 if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
4332 events = {};
4333 for ( event in this.aggregateItemEvents ) {
4334 events[ event ] = [ 'emit', this.aggregateItemEvents[ event ], item ];
4335 }
4336 item.connect( this, events );
4337 }
4338 item.setElementGroup( this );
4339 itemElements.push( item.$element.get( 0 ) );
4340 }
4341
4342 if ( index === undefined || index < 0 || index >= this.items.length ) {
4343 this.$group.append( itemElements );
4344 this.items.push.apply( this.items, items );
4345 } else if ( index === 0 ) {
4346 this.$group.prepend( itemElements );
4347 this.items.unshift.apply( this.items, items );
4348 } else {
4349 this.items[ index ].$element.before( itemElements );
4350 this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
4351 }
4352
4353 return this;
4354 };
4355
4356 /**
4357 * Remove items.
4358 *
4359 * Items will be detached, not removed, so they can be used later.
4360 *
4361 * @param {OO.ui.Element[]} items Items to remove
4362 * @chainable
4363 */
4364 OO.ui.GroupElement.prototype.removeItems = function ( items ) {
4365 var i, len, item, index, remove, itemEvent;
4366
4367 // Remove specific items
4368 for ( i = 0, len = items.length; i < len; i++ ) {
4369 item = items[ i ];
4370 index = $.inArray( item, this.items );
4371 if ( index !== -1 ) {
4372 if (
4373 item.connect && item.disconnect &&
4374 !$.isEmptyObject( this.aggregateItemEvents )
4375 ) {
4376 remove = {};
4377 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4378 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
4379 }
4380 item.disconnect( this, remove );
4381 }
4382 item.setElementGroup( null );
4383 this.items.splice( index, 1 );
4384 item.$element.detach();
4385 }
4386 }
4387
4388 return this;
4389 };
4390
4391 /**
4392 * Clear all items.
4393 *
4394 * Items will be detached, not removed, so they can be used later.
4395 *
4396 * @chainable
4397 */
4398 OO.ui.GroupElement.prototype.clearItems = function () {
4399 var i, len, item, remove, itemEvent;
4400
4401 // Remove all items
4402 for ( i = 0, len = this.items.length; i < len; i++ ) {
4403 item = this.items[ i ];
4404 if (
4405 item.connect && item.disconnect &&
4406 !$.isEmptyObject( this.aggregateItemEvents )
4407 ) {
4408 remove = {};
4409 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4410 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
4411 }
4412 item.disconnect( this, remove );
4413 }
4414 item.setElementGroup( null );
4415 item.$element.detach();
4416 }
4417
4418 this.items = [];
4419 return this;
4420 };
4421
4422 /**
4423 * DraggableElement is a mixin class used to create elements that can be clicked
4424 * and dragged by a mouse to a new position within a group. This class must be used
4425 * in conjunction with OO.ui.DraggableGroupElement, which provides a container for
4426 * the draggable elements.
4427 *
4428 * @abstract
4429 * @class
4430 *
4431 * @constructor
4432 */
4433 OO.ui.DraggableElement = function OoUiDraggableElement() {
4434 // Properties
4435 this.index = null;
4436
4437 // Initialize and events
4438 this.$element
4439 .attr( 'draggable', true )
4440 .addClass( 'oo-ui-draggableElement' )
4441 .on( {
4442 dragstart: this.onDragStart.bind( this ),
4443 dragover: this.onDragOver.bind( this ),
4444 dragend: this.onDragEnd.bind( this ),
4445 drop: this.onDrop.bind( this )
4446 } );
4447 };
4448
4449 OO.initClass( OO.ui.DraggableElement );
4450
4451 /* Events */
4452
4453 /**
4454 * @event dragstart
4455 *
4456 * A dragstart event is emitted when the user clicks and begins dragging an item.
4457 * @param {OO.ui.DraggableElement} item The item the user has clicked and is dragging with the mouse.
4458 */
4459
4460 /**
4461 * @event dragend
4462 * A dragend event is emitted when the user drags an item and releases the mouse,
4463 * thus terminating the drag operation.
4464 */
4465
4466 /**
4467 * @event drop
4468 * A drop event is emitted when the user drags an item and then releases the mouse button
4469 * over a valid target.
4470 */
4471
4472 /* Static Properties */
4473
4474 /**
4475 * @inheritdoc OO.ui.ButtonElement
4476 */
4477 OO.ui.DraggableElement.static.cancelButtonMouseDownEvents = false;
4478
4479 /* Methods */
4480
4481 /**
4482 * Respond to dragstart event.
4483 *
4484 * @private
4485 * @param {jQuery.Event} event jQuery event
4486 * @fires dragstart
4487 */
4488 OO.ui.DraggableElement.prototype.onDragStart = function ( e ) {
4489 var dataTransfer = e.originalEvent.dataTransfer;
4490 // Define drop effect
4491 dataTransfer.dropEffect = 'none';
4492 dataTransfer.effectAllowed = 'move';
4493 // We must set up a dataTransfer data property or Firefox seems to
4494 // ignore the fact the element is draggable.
4495 try {
4496 dataTransfer.setData( 'application-x/OOjs-UI-draggable', this.getIndex() );
4497 } catch ( err ) {
4498 // The above is only for firefox. No need to set a catch clause
4499 // if it fails, move on.
4500 }
4501 // Add dragging class
4502 this.$element.addClass( 'oo-ui-draggableElement-dragging' );
4503 // Emit event
4504 this.emit( 'dragstart', this );
4505 return true;
4506 };
4507
4508 /**
4509 * Respond to dragend event.
4510 *
4511 * @private
4512 * @fires dragend
4513 */
4514 OO.ui.DraggableElement.prototype.onDragEnd = function () {
4515 this.$element.removeClass( 'oo-ui-draggableElement-dragging' );
4516 this.emit( 'dragend' );
4517 };
4518
4519 /**
4520 * Handle drop event.
4521 *
4522 * @private
4523 * @param {jQuery.Event} event jQuery event
4524 * @fires drop
4525 */
4526 OO.ui.DraggableElement.prototype.onDrop = function ( e ) {
4527 e.preventDefault();
4528 this.emit( 'drop', e );
4529 };
4530
4531 /**
4532 * In order for drag/drop to work, the dragover event must
4533 * return false and stop propogation.
4534 *
4535 * @private
4536 */
4537 OO.ui.DraggableElement.prototype.onDragOver = function ( e ) {
4538 e.preventDefault();
4539 };
4540
4541 /**
4542 * Set item index.
4543 * Store it in the DOM so we can access from the widget drag event
4544 *
4545 * @private
4546 * @param {number} Item index
4547 */
4548 OO.ui.DraggableElement.prototype.setIndex = function ( index ) {
4549 if ( this.index !== index ) {
4550 this.index = index;
4551 this.$element.data( 'index', index );
4552 }
4553 };
4554
4555 /**
4556 * Get item index
4557 *
4558 * @private
4559 * @return {number} Item index
4560 */
4561 OO.ui.DraggableElement.prototype.getIndex = function () {
4562 return this.index;
4563 };
4564
4565 /**
4566 * DraggableGroupElement is a mixin class used to create a group element to
4567 * contain draggable elements, which are items that can be clicked and dragged by a mouse.
4568 * The class is used with OO.ui.DraggableElement.
4569 *
4570 * @abstract
4571 * @class
4572 *
4573 * @constructor
4574 * @param {Object} [config] Configuration options
4575 * @cfg {jQuery} [$group] Container node, assigned to #$group, omit to use a generated `<div>`
4576 * @cfg {string} [orientation] Item orientation, 'horizontal' or 'vertical'. Defaults to 'vertical'
4577 */
4578 OO.ui.DraggableGroupElement = function OoUiDraggableGroupElement( config ) {
4579 // Configuration initialization
4580 config = config || {};
4581
4582 // Parent constructor
4583 OO.ui.GroupElement.call( this, config );
4584
4585 // Properties
4586 this.orientation = config.orientation || 'vertical';
4587 this.dragItem = null;
4588 this.itemDragOver = null;
4589 this.itemKeys = {};
4590 this.sideInsertion = '';
4591
4592 // Events
4593 this.aggregate( {
4594 dragstart: 'itemDragStart',
4595 dragend: 'itemDragEnd',
4596 drop: 'itemDrop'
4597 } );
4598 this.connect( this, {
4599 itemDragStart: 'onItemDragStart',
4600 itemDrop: 'onItemDrop',
4601 itemDragEnd: 'onItemDragEnd'
4602 } );
4603 this.$element.on( {
4604 dragover: $.proxy( this.onDragOver, this ),
4605 dragleave: $.proxy( this.onDragLeave, this )
4606 } );
4607
4608 // Initialize
4609 if ( Array.isArray( config.items ) ) {
4610 this.addItems( config.items );
4611 }
4612 this.$placeholder = $( '<div>' )
4613 .addClass( 'oo-ui-draggableGroupElement-placeholder' );
4614 this.$element
4615 .addClass( 'oo-ui-draggableGroupElement' )
4616 .append( this.$status )
4617 .toggleClass( 'oo-ui-draggableGroupElement-horizontal', this.orientation === 'horizontal' )
4618 .prepend( this.$placeholder );
4619 };
4620
4621 /* Setup */
4622 OO.mixinClass( OO.ui.DraggableGroupElement, OO.ui.GroupElement );
4623
4624 /* Events */
4625
4626 /**
4627 * @event reorder
4628 * @param {OO.ui.DraggableElement} item Reordered item
4629 * @param {number} [newIndex] New index for the item
4630 */
4631
4632 /* Methods */
4633
4634 /**
4635 * Respond to item drag start event
4636 * @param {OO.ui.DraggableElement} item Dragged item
4637 */
4638 OO.ui.DraggableGroupElement.prototype.onItemDragStart = function ( item ) {
4639 var i, len;
4640
4641 // Map the index of each object
4642 for ( i = 0, len = this.items.length; i < len; i++ ) {
4643 this.items[ i ].setIndex( i );
4644 }
4645
4646 if ( this.orientation === 'horizontal' ) {
4647 // Set the height of the indicator
4648 this.$placeholder.css( {
4649 height: item.$element.outerHeight(),
4650 width: 2
4651 } );
4652 } else {
4653 // Set the width of the indicator
4654 this.$placeholder.css( {
4655 height: 2,
4656 width: item.$element.outerWidth()
4657 } );
4658 }
4659 this.setDragItem( item );
4660 };
4661
4662 /**
4663 * Respond to item drag end event
4664 */
4665 OO.ui.DraggableGroupElement.prototype.onItemDragEnd = function () {
4666 this.unsetDragItem();
4667 return false;
4668 };
4669
4670 /**
4671 * Handle drop event and switch the order of the items accordingly
4672 * @param {OO.ui.DraggableElement} item Dropped item
4673 * @fires reorder
4674 */
4675 OO.ui.DraggableGroupElement.prototype.onItemDrop = function ( item ) {
4676 var toIndex = item.getIndex();
4677 // Check if the dropped item is from the current group
4678 // TODO: Figure out a way to configure a list of legally droppable
4679 // elements even if they are not yet in the list
4680 if ( this.getDragItem() ) {
4681 // If the insertion point is 'after', the insertion index
4682 // is shifted to the right (or to the left in RTL, hence 'after')
4683 if ( this.sideInsertion === 'after' ) {
4684 toIndex++;
4685 }
4686 // Emit change event
4687 this.emit( 'reorder', this.getDragItem(), toIndex );
4688 }
4689 this.unsetDragItem();
4690 // Return false to prevent propogation
4691 return false;
4692 };
4693
4694 /**
4695 * Handle dragleave event.
4696 */
4697 OO.ui.DraggableGroupElement.prototype.onDragLeave = function () {
4698 // This means the item was dragged outside the widget
4699 this.$placeholder
4700 .css( 'left', 0 )
4701 .addClass( 'oo-ui-element-hidden' );
4702 };
4703
4704 /**
4705 * Respond to dragover event
4706 * @param {jQuery.Event} event Event details
4707 */
4708 OO.ui.DraggableGroupElement.prototype.onDragOver = function ( e ) {
4709 var dragOverObj, $optionWidget, itemOffset, itemMidpoint, itemBoundingRect,
4710 itemSize, cssOutput, dragPosition, itemIndex, itemPosition,
4711 clientX = e.originalEvent.clientX,
4712 clientY = e.originalEvent.clientY;
4713
4714 // Get the OptionWidget item we are dragging over
4715 dragOverObj = this.getElementDocument().elementFromPoint( clientX, clientY );
4716 $optionWidget = $( dragOverObj ).closest( '.oo-ui-draggableElement' );
4717 if ( $optionWidget[ 0 ] ) {
4718 itemOffset = $optionWidget.offset();
4719 itemBoundingRect = $optionWidget[ 0 ].getBoundingClientRect();
4720 itemPosition = $optionWidget.position();
4721 itemIndex = $optionWidget.data( 'index' );
4722 }
4723
4724 if (
4725 itemOffset &&
4726 this.isDragging() &&
4727 itemIndex !== this.getDragItem().getIndex()
4728 ) {
4729 if ( this.orientation === 'horizontal' ) {
4730 // Calculate where the mouse is relative to the item width
4731 itemSize = itemBoundingRect.width;
4732 itemMidpoint = itemBoundingRect.left + itemSize / 2;
4733 dragPosition = clientX;
4734 // Which side of the item we hover over will dictate
4735 // where the placeholder will appear, on the left or
4736 // on the right
4737 cssOutput = {
4738 left: dragPosition < itemMidpoint ? itemPosition.left : itemPosition.left + itemSize,
4739 top: itemPosition.top
4740 };
4741 } else {
4742 // Calculate where the mouse is relative to the item height
4743 itemSize = itemBoundingRect.height;
4744 itemMidpoint = itemBoundingRect.top + itemSize / 2;
4745 dragPosition = clientY;
4746 // Which side of the item we hover over will dictate
4747 // where the placeholder will appear, on the top or
4748 // on the bottom
4749 cssOutput = {
4750 top: dragPosition < itemMidpoint ? itemPosition.top : itemPosition.top + itemSize,
4751 left: itemPosition.left
4752 };
4753 }
4754 // Store whether we are before or after an item to rearrange
4755 // For horizontal layout, we need to account for RTL, as this is flipped
4756 if ( this.orientation === 'horizontal' && this.$element.css( 'direction' ) === 'rtl' ) {
4757 this.sideInsertion = dragPosition < itemMidpoint ? 'after' : 'before';
4758 } else {
4759 this.sideInsertion = dragPosition < itemMidpoint ? 'before' : 'after';
4760 }
4761 // Add drop indicator between objects
4762 this.$placeholder
4763 .css( cssOutput )
4764 .removeClass( 'oo-ui-element-hidden' );
4765 } else {
4766 // This means the item was dragged outside the widget
4767 this.$placeholder
4768 .css( 'left', 0 )
4769 .addClass( 'oo-ui-element-hidden' );
4770 }
4771 // Prevent default
4772 e.preventDefault();
4773 };
4774
4775 /**
4776 * Set a dragged item
4777 * @param {OO.ui.DraggableElement} item Dragged item
4778 */
4779 OO.ui.DraggableGroupElement.prototype.setDragItem = function ( item ) {
4780 this.dragItem = item;
4781 };
4782
4783 /**
4784 * Unset the current dragged item
4785 */
4786 OO.ui.DraggableGroupElement.prototype.unsetDragItem = function () {
4787 this.dragItem = null;
4788 this.itemDragOver = null;
4789 this.$placeholder.addClass( 'oo-ui-element-hidden' );
4790 this.sideInsertion = '';
4791 };
4792
4793 /**
4794 * Get the current dragged item
4795 * @return {OO.ui.DraggableElement|null} item Dragged item or null if no item is dragged
4796 */
4797 OO.ui.DraggableGroupElement.prototype.getDragItem = function () {
4798 return this.dragItem;
4799 };
4800
4801 /**
4802 * Check if there's an item being dragged.
4803 * @return {Boolean} Item is being dragged
4804 */
4805 OO.ui.DraggableGroupElement.prototype.isDragging = function () {
4806 return this.getDragItem() !== null;
4807 };
4808
4809 /**
4810 * IconElement is often mixed into other classes to generate an icon.
4811 * Icons are graphics, about the size of normal text. They are used to aid the user
4812 * in locating a control or to convey information in a space-efficient way. See the
4813 * [OOjs UI documentation on MediaWiki] [1] for a list of icons
4814 * included in the library.
4815 *
4816 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
4817 *
4818 * @abstract
4819 * @class
4820 *
4821 * @constructor
4822 * @param {Object} [config] Configuration options
4823 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
4824 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
4825 * the icon element be set to an existing icon instead of the one generated by this class, set a
4826 * value using a jQuery selection. For example:
4827 *
4828 * // Use a <div> tag instead of a <span>
4829 * $icon: $("<div>")
4830 * // Use an existing icon element instead of the one generated by the class
4831 * $icon: this.$element
4832 * // Use an icon element from a child widget
4833 * $icon: this.childwidget.$element
4834 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
4835 * symbolic names. A map is used for i18n purposes and contains a `default` icon
4836 * name and additional names keyed by language code. The `default` name is used when no icon is keyed
4837 * by the user's language.
4838 *
4839 * Example of an i18n map:
4840 *
4841 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
4842 * See the [OOjs UI documentation on MediaWiki] [2] for a list of icons included in the library.
4843 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
4844 * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
4845 * text. The icon title is displayed when users move the mouse over the icon.
4846 */
4847 OO.ui.IconElement = function OoUiIconElement( config ) {
4848 // Configuration initialization
4849 config = config || {};
4850
4851 // Properties
4852 this.$icon = null;
4853 this.icon = null;
4854 this.iconTitle = null;
4855
4856 // Initialization
4857 this.setIcon( config.icon || this.constructor.static.icon );
4858 this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle );
4859 this.setIconElement( config.$icon || $( '<span>' ) );
4860 };
4861
4862 /* Setup */
4863
4864 OO.initClass( OO.ui.IconElement );
4865
4866 /* Static Properties */
4867
4868 /**
4869 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
4870 * for i18n purposes and contains a `default` icon name and additional names keyed by
4871 * language code. The `default` name is used when no icon is keyed by the user's language.
4872 *
4873 * Example of an i18n map:
4874 *
4875 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
4876 *
4877 * Note: the static property will be overridden if the #icon configuration is used.
4878 *
4879 * @static
4880 * @inheritable
4881 * @property {Object|string}
4882 */
4883 OO.ui.IconElement.static.icon = null;
4884
4885 /**
4886 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
4887 * function that returns title text, or `null` for no title.
4888 *
4889 * The static property will be overridden if the #iconTitle configuration is used.
4890 *
4891 * @static
4892 * @inheritable
4893 * @property {string|Function|null}
4894 */
4895 OO.ui.IconElement.static.iconTitle = null;
4896
4897 /* Methods */
4898
4899 /**
4900 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
4901 * applies to the specified icon element instead of the one created by the class. If an icon
4902 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
4903 * and mixin methods will no longer affect the element.
4904 *
4905 * @param {jQuery} $icon Element to use as icon
4906 */
4907 OO.ui.IconElement.prototype.setIconElement = function ( $icon ) {
4908 if ( this.$icon ) {
4909 this.$icon
4910 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon )
4911 .removeAttr( 'title' );
4912 }
4913
4914 this.$icon = $icon
4915 .addClass( 'oo-ui-iconElement-icon' )
4916 .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon );
4917 if ( this.iconTitle !== null ) {
4918 this.$icon.attr( 'title', this.iconTitle );
4919 }
4920 };
4921
4922 /**
4923 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
4924 * The icon parameter can also be set to a map of icon names. See the #icon config setting
4925 * for an example.
4926 *
4927 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
4928 * by language code, or `null` to remove the icon.
4929 * @chainable
4930 */
4931 OO.ui.IconElement.prototype.setIcon = function ( icon ) {
4932 icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
4933 icon = typeof icon === 'string' && icon.trim().length ? icon.trim() : null;
4934
4935 if ( this.icon !== icon ) {
4936 if ( this.$icon ) {
4937 if ( this.icon !== null ) {
4938 this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
4939 }
4940 if ( icon !== null ) {
4941 this.$icon.addClass( 'oo-ui-icon-' + icon );
4942 }
4943 }
4944 this.icon = icon;
4945 }
4946
4947 this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon );
4948 this.updateThemeClasses();
4949
4950 return this;
4951 };
4952
4953 /**
4954 * Set the icon title. Use `null` to remove the title.
4955 *
4956 * @param {string|Function|null} iconTitle A text string used as the icon title,
4957 * a function that returns title text, or `null` for no title.
4958 * @chainable
4959 */
4960 OO.ui.IconElement.prototype.setIconTitle = function ( iconTitle ) {
4961 iconTitle = typeof iconTitle === 'function' ||
4962 ( typeof iconTitle === 'string' && iconTitle.length ) ?
4963 OO.ui.resolveMsg( iconTitle ) : null;
4964
4965 if ( this.iconTitle !== iconTitle ) {
4966 this.iconTitle = iconTitle;
4967 if ( this.$icon ) {
4968 if ( this.iconTitle !== null ) {
4969 this.$icon.attr( 'title', iconTitle );
4970 } else {
4971 this.$icon.removeAttr( 'title' );
4972 }
4973 }
4974 }
4975
4976 return this;
4977 };
4978
4979 /**
4980 * Get the symbolic name of the icon.
4981 *
4982 * @return {string} Icon name
4983 */
4984 OO.ui.IconElement.prototype.getIcon = function () {
4985 return this.icon;
4986 };
4987
4988 /**
4989 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
4990 *
4991 * @return {string} Icon title text
4992 */
4993 OO.ui.IconElement.prototype.getIconTitle = function () {
4994 return this.iconTitle;
4995 };
4996
4997 /**
4998 * IndicatorElement is often mixed into other classes to generate an indicator.
4999 * Indicators are small graphics that are generally used in two ways:
5000 *
5001 * - To draw attention to the status of an item. For example, an indicator might be
5002 * used to show that an item in a list has errors that need to be resolved.
5003 * - To clarify the function of a control that acts in an exceptional way (a button
5004 * that opens a menu instead of performing an action directly, for example).
5005 *
5006 * For a list of indicators included in the library, please see the
5007 * [OOjs UI documentation on MediaWiki] [1].
5008 *
5009 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
5010 *
5011 * @abstract
5012 * @class
5013 *
5014 * @constructor
5015 * @param {Object} [config] Configuration options
5016 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
5017 * configuration is omitted, the indicator element will use a generated `<span>`.
5018 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
5019 * See the [OOjs UI documentation on MediaWiki][2] for a list of indicators included
5020 * in the library.
5021 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
5022 * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
5023 * or a function that returns title text. The indicator title is displayed when users move
5024 * the mouse over the indicator.
5025 */
5026 OO.ui.IndicatorElement = function OoUiIndicatorElement( config ) {
5027 // Configuration initialization
5028 config = config || {};
5029
5030 // Properties
5031 this.$indicator = null;
5032 this.indicator = null;
5033 this.indicatorTitle = null;
5034
5035 // Initialization
5036 this.setIndicator( config.indicator || this.constructor.static.indicator );
5037 this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle );
5038 this.setIndicatorElement( config.$indicator || $( '<span>' ) );
5039 };
5040
5041 /* Setup */
5042
5043 OO.initClass( OO.ui.IndicatorElement );
5044
5045 /* Static Properties */
5046
5047 /**
5048 * Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
5049 * The static property will be overridden if the #indicator configuration is used.
5050 *
5051 * @static
5052 * @inheritable
5053 * @property {string|null}
5054 */
5055 OO.ui.IndicatorElement.static.indicator = null;
5056
5057 /**
5058 * A text string used as the indicator title, a function that returns title text, or `null`
5059 * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
5060 *
5061 * @static
5062 * @inheritable
5063 * @property {string|Function|null}
5064 */
5065 OO.ui.IndicatorElement.static.indicatorTitle = null;
5066
5067 /* Methods */
5068
5069 /**
5070 * Set the indicator element.
5071 *
5072 * If an element is already set, it will be cleaned up before setting up the new element.
5073 *
5074 * @param {jQuery} $indicator Element to use as indicator
5075 */
5076 OO.ui.IndicatorElement.prototype.setIndicatorElement = function ( $indicator ) {
5077 if ( this.$indicator ) {
5078 this.$indicator
5079 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator )
5080 .removeAttr( 'title' );
5081 }
5082
5083 this.$indicator = $indicator
5084 .addClass( 'oo-ui-indicatorElement-indicator' )
5085 .toggleClass( 'oo-ui-indicator-' + this.indicator, !!this.indicator );
5086 if ( this.indicatorTitle !== null ) {
5087 this.$indicator.attr( 'title', this.indicatorTitle );
5088 }
5089 };
5090
5091 /**
5092 * Set indicator name.
5093 *
5094 * @param {string|null} indicator Symbolic name of indicator to use or null for no indicator
5095 * @chainable
5096 */
5097 OO.ui.IndicatorElement.prototype.setIndicator = function ( indicator ) {
5098 indicator = typeof indicator === 'string' && indicator.length ? indicator.trim() : null;
5099
5100 if ( this.indicator !== indicator ) {
5101 if ( this.$indicator ) {
5102 if ( this.indicator !== null ) {
5103 this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
5104 }
5105 if ( indicator !== null ) {
5106 this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
5107 }
5108 }
5109 this.indicator = indicator;
5110 }
5111
5112 this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator );
5113 this.updateThemeClasses();
5114
5115 return this;
5116 };
5117
5118 /**
5119 * Set indicator title.
5120 *
5121 * @param {string|Function|null} indicator Indicator title text, a function that returns text or
5122 * null for no indicator title
5123 * @chainable
5124 */
5125 OO.ui.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
5126 indicatorTitle = typeof indicatorTitle === 'function' ||
5127 ( typeof indicatorTitle === 'string' && indicatorTitle.length ) ?
5128 OO.ui.resolveMsg( indicatorTitle ) : null;
5129
5130 if ( this.indicatorTitle !== indicatorTitle ) {
5131 this.indicatorTitle = indicatorTitle;
5132 if ( this.$indicator ) {
5133 if ( this.indicatorTitle !== null ) {
5134 this.$indicator.attr( 'title', indicatorTitle );
5135 } else {
5136 this.$indicator.removeAttr( 'title' );
5137 }
5138 }
5139 }
5140
5141 return this;
5142 };
5143
5144 /**
5145 * Get indicator name.
5146 *
5147 * @return {string} Symbolic name of indicator
5148 */
5149 OO.ui.IndicatorElement.prototype.getIndicator = function () {
5150 return this.indicator;
5151 };
5152
5153 /**
5154 * Get indicator title.
5155 *
5156 * @return {string} Indicator title text
5157 */
5158 OO.ui.IndicatorElement.prototype.getIndicatorTitle = function () {
5159 return this.indicatorTitle;
5160 };
5161
5162 /**
5163 * LabelElement is often mixed into other classes to generate a label, which
5164 * helps identify the function of an interface element.
5165 * See the [OOjs UI documentation on MediaWiki] [1] for more information.
5166 *
5167 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
5168 *
5169 * @abstract
5170 * @class
5171 *
5172 * @constructor
5173 * @param {Object} [config] Configuration options
5174 * @cfg {jQuery} [$label] The label element created by the class. If this
5175 * configuration is omitted, the label element will use a generated `<span>`.
5176 * @cfg {jQuery|string|Function} [label] The label text. The label can be specified as a plaintext string,
5177 * a jQuery selection of elements, or a function that will produce a string in the future. See the
5178 * [OOjs UI documentation on MediaWiki] [2] for examples.
5179 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
5180 * @cfg {boolean} [autoFitLabel=true] Fit the label to the width of the parent element.
5181 * The label will be truncated to fit if necessary.
5182 */
5183 OO.ui.LabelElement = function OoUiLabelElement( config ) {
5184 // Configuration initialization
5185 config = config || {};
5186
5187 // Properties
5188 this.$label = null;
5189 this.label = null;
5190 this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel;
5191
5192 // Initialization
5193 this.setLabel( config.label || this.constructor.static.label );
5194 this.setLabelElement( config.$label || $( '<span>' ) );
5195 };
5196
5197 /* Setup */
5198
5199 OO.initClass( OO.ui.LabelElement );
5200
5201 /* Events */
5202
5203 /**
5204 * @event labelChange
5205 * @param {string} value
5206 */
5207
5208 /* Static Properties */
5209
5210 /**
5211 * The label text. The label can be specified as a plaintext string, a function that will
5212 * produce a string in the future, or `null` for no label. The static value will
5213 * be overridden if a label is specified with the #label config option.
5214 *
5215 * @static
5216 * @inheritable
5217 * @property {string|Function|null}
5218 */
5219 OO.ui.LabelElement.static.label = null;
5220
5221 /* Methods */
5222
5223 /**
5224 * Set the label element.
5225 *
5226 * If an element is already set, it will be cleaned up before setting up the new element.
5227 *
5228 * @param {jQuery} $label Element to use as label
5229 */
5230 OO.ui.LabelElement.prototype.setLabelElement = function ( $label ) {
5231 if ( this.$label ) {
5232 this.$label.removeClass( 'oo-ui-labelElement-label' ).empty();
5233 }
5234
5235 this.$label = $label.addClass( 'oo-ui-labelElement-label' );
5236 this.setLabelContent( this.label );
5237 };
5238
5239 /**
5240 * Set the label.
5241 *
5242 * An empty string will result in the label being hidden. A string containing only whitespace will
5243 * be converted to a single `&nbsp;`.
5244 *
5245 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
5246 * text; or null for no label
5247 * @chainable
5248 */
5249 OO.ui.LabelElement.prototype.setLabel = function ( label ) {
5250 label = typeof label === 'function' ? OO.ui.resolveMsg( label ) : label;
5251 label = ( ( typeof label === 'string' && label.length ) || label instanceof jQuery || label instanceof OO.ui.HtmlSnippet ) ? label : null;
5252
5253 this.$element.toggleClass( 'oo-ui-labelElement', !!label );
5254
5255 if ( this.label !== label ) {
5256 if ( this.$label ) {
5257 this.setLabelContent( label );
5258 }
5259 this.label = label;
5260 this.emit( 'labelChange' );
5261 }
5262
5263 return this;
5264 };
5265
5266 /**
5267 * Get the label.
5268 *
5269 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
5270 * text; or null for no label
5271 */
5272 OO.ui.LabelElement.prototype.getLabel = function () {
5273 return this.label;
5274 };
5275
5276 /**
5277 * Fit the label.
5278 *
5279 * @chainable
5280 */
5281 OO.ui.LabelElement.prototype.fitLabel = function () {
5282 if ( this.$label && this.$label.autoEllipsis && this.autoFitLabel ) {
5283 this.$label.autoEllipsis( { hasSpan: false, tooltip: true } );
5284 }
5285
5286 return this;
5287 };
5288
5289 /**
5290 * Set the content of the label.
5291 *
5292 * Do not call this method until after the label element has been set by #setLabelElement.
5293 *
5294 * @private
5295 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
5296 * text; or null for no label
5297 */
5298 OO.ui.LabelElement.prototype.setLabelContent = function ( label ) {
5299 if ( typeof label === 'string' ) {
5300 if ( label.match( /^\s*$/ ) ) {
5301 // Convert whitespace only string to a single non-breaking space
5302 this.$label.html( '&nbsp;' );
5303 } else {
5304 this.$label.text( label );
5305 }
5306 } else if ( label instanceof OO.ui.HtmlSnippet ) {
5307 this.$label.html( label.toString() );
5308 } else if ( label instanceof jQuery ) {
5309 this.$label.empty().append( label );
5310 } else {
5311 this.$label.empty();
5312 }
5313 };
5314
5315 /**
5316 * Mixin that adds a menu showing suggested values for a OO.ui.TextInputWidget.
5317 *
5318 * Subclasses that set the value of #lookupInput from #onLookupMenuItemChoose should
5319 * be aware that this will cause new suggestions to be looked up for the new value. If this is
5320 * not desired, disable lookups with #setLookupsDisabled, then set the value, then re-enable lookups.
5321 *
5322 * @class
5323 * @abstract
5324 *
5325 * @constructor
5326 * @param {Object} [config] Configuration options
5327 * @cfg {jQuery} [$overlay] Overlay for dropdown; defaults to relative positioning
5328 * @cfg {jQuery} [$container=this.$element] Element to render menu under
5329 */
5330 OO.ui.LookupElement = function OoUiLookupElement( config ) {
5331 // Configuration initialization
5332 config = config || {};
5333
5334 // Properties
5335 this.$overlay = config.$overlay || this.$element;
5336 this.lookupMenu = new OO.ui.TextInputMenuSelectWidget( this, {
5337 widget: this,
5338 input: this,
5339 $container: config.$container
5340 } );
5341 this.lookupCache = {};
5342 this.lookupQuery = null;
5343 this.lookupRequest = null;
5344 this.lookupsDisabled = false;
5345 this.lookupInputFocused = false;
5346
5347 // Events
5348 this.$input.on( {
5349 focus: this.onLookupInputFocus.bind( this ),
5350 blur: this.onLookupInputBlur.bind( this ),
5351 mousedown: this.onLookupInputMouseDown.bind( this )
5352 } );
5353 this.connect( this, { change: 'onLookupInputChange' } );
5354 this.lookupMenu.connect( this, {
5355 toggle: 'onLookupMenuToggle',
5356 choose: 'onLookupMenuItemChoose'
5357 } );
5358
5359 // Initialization
5360 this.$element.addClass( 'oo-ui-lookupElement' );
5361 this.lookupMenu.$element.addClass( 'oo-ui-lookupElement-menu' );
5362 this.$overlay.append( this.lookupMenu.$element );
5363 };
5364
5365 /* Methods */
5366
5367 /**
5368 * Handle input focus event.
5369 *
5370 * @param {jQuery.Event} e Input focus event
5371 */
5372 OO.ui.LookupElement.prototype.onLookupInputFocus = function () {
5373 this.lookupInputFocused = true;
5374 this.populateLookupMenu();
5375 };
5376
5377 /**
5378 * Handle input blur event.
5379 *
5380 * @param {jQuery.Event} e Input blur event
5381 */
5382 OO.ui.LookupElement.prototype.onLookupInputBlur = function () {
5383 this.closeLookupMenu();
5384 this.lookupInputFocused = false;
5385 };
5386
5387 /**
5388 * Handle input mouse down event.
5389 *
5390 * @param {jQuery.Event} e Input mouse down event
5391 */
5392 OO.ui.LookupElement.prototype.onLookupInputMouseDown = function () {
5393 // Only open the menu if the input was already focused.
5394 // This way we allow the user to open the menu again after closing it with Esc
5395 // by clicking in the input. Opening (and populating) the menu when initially
5396 // clicking into the input is handled by the focus handler.
5397 if ( this.lookupInputFocused && !this.lookupMenu.isVisible() ) {
5398 this.populateLookupMenu();
5399 }
5400 };
5401
5402 /**
5403 * Handle input change event.
5404 *
5405 * @param {string} value New input value
5406 */
5407 OO.ui.LookupElement.prototype.onLookupInputChange = function () {
5408 if ( this.lookupInputFocused ) {
5409 this.populateLookupMenu();
5410 }
5411 };
5412
5413 /**
5414 * Handle the lookup menu being shown/hidden.
5415 *
5416 * @param {boolean} visible Whether the lookup menu is now visible.
5417 */
5418 OO.ui.LookupElement.prototype.onLookupMenuToggle = function ( visible ) {
5419 if ( !visible ) {
5420 // When the menu is hidden, abort any active request and clear the menu.
5421 // This has to be done here in addition to closeLookupMenu(), because
5422 // MenuSelectWidget will close itself when the user presses Esc.
5423 this.abortLookupRequest();
5424 this.lookupMenu.clearItems();
5425 }
5426 };
5427
5428 /**
5429 * Handle menu item 'choose' event, updating the text input value to the value of the clicked item.
5430 *
5431 * @param {OO.ui.MenuOptionWidget|null} item Selected item
5432 */
5433 OO.ui.LookupElement.prototype.onLookupMenuItemChoose = function ( item ) {
5434 if ( item ) {
5435 this.setValue( item.getData() );
5436 }
5437 };
5438
5439 /**
5440 * Get lookup menu.
5441 *
5442 * @return {OO.ui.TextInputMenuSelectWidget}
5443 */
5444 OO.ui.LookupElement.prototype.getLookupMenu = function () {
5445 return this.lookupMenu;
5446 };
5447
5448 /**
5449 * Disable or re-enable lookups.
5450 *
5451 * When lookups are disabled, calls to #populateLookupMenu will be ignored.
5452 *
5453 * @param {boolean} disabled Disable lookups
5454 */
5455 OO.ui.LookupElement.prototype.setLookupsDisabled = function ( disabled ) {
5456 this.lookupsDisabled = !!disabled;
5457 };
5458
5459 /**
5460 * Open the menu. If there are no entries in the menu, this does nothing.
5461 *
5462 * @chainable
5463 */
5464 OO.ui.LookupElement.prototype.openLookupMenu = function () {
5465 if ( !this.lookupMenu.isEmpty() ) {
5466 this.lookupMenu.toggle( true );
5467 }
5468 return this;
5469 };
5470
5471 /**
5472 * Close the menu, empty it, and abort any pending request.
5473 *
5474 * @chainable
5475 */
5476 OO.ui.LookupElement.prototype.closeLookupMenu = function () {
5477 this.lookupMenu.toggle( false );
5478 this.abortLookupRequest();
5479 this.lookupMenu.clearItems();
5480 return this;
5481 };
5482
5483 /**
5484 * Request menu items based on the input's current value, and when they arrive,
5485 * populate the menu with these items and show the menu.
5486 *
5487 * If lookups have been disabled with #setLookupsDisabled, this function does nothing.
5488 *
5489 * @chainable
5490 */
5491 OO.ui.LookupElement.prototype.populateLookupMenu = function () {
5492 var widget = this,
5493 value = this.getValue();
5494
5495 if ( this.lookupsDisabled ) {
5496 return;
5497 }
5498
5499 // If the input is empty, clear the menu
5500 if ( value === '' ) {
5501 this.closeLookupMenu();
5502 // Skip population if there is already a request pending for the current value
5503 } else if ( value !== this.lookupQuery ) {
5504 this.getLookupMenuItems()
5505 .done( function ( items ) {
5506 widget.lookupMenu.clearItems();
5507 if ( items.length ) {
5508 widget.lookupMenu
5509 .addItems( items )
5510 .toggle( true );
5511 widget.initializeLookupMenuSelection();
5512 } else {
5513 widget.lookupMenu.toggle( false );
5514 }
5515 } )
5516 .fail( function () {
5517 widget.lookupMenu.clearItems();
5518 } );
5519 }
5520
5521 return this;
5522 };
5523
5524 /**
5525 * Select and highlight the first selectable item in the menu.
5526 *
5527 * @chainable
5528 */
5529 OO.ui.LookupElement.prototype.initializeLookupMenuSelection = function () {
5530 if ( !this.lookupMenu.getSelectedItem() ) {
5531 this.lookupMenu.selectItem( this.lookupMenu.getFirstSelectableItem() );
5532 }
5533 this.lookupMenu.highlightItem( this.lookupMenu.getSelectedItem() );
5534 };
5535
5536 /**
5537 * Get lookup menu items for the current query.
5538 *
5539 * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument of
5540 * the done event. If the request was aborted to make way for a subsequent request, this promise
5541 * will not be rejected: it will remain pending forever.
5542 */
5543 OO.ui.LookupElement.prototype.getLookupMenuItems = function () {
5544 var widget = this,
5545 value = this.getValue(),
5546 deferred = $.Deferred(),
5547 ourRequest;
5548
5549 this.abortLookupRequest();
5550 if ( Object.prototype.hasOwnProperty.call( this.lookupCache, value ) ) {
5551 deferred.resolve( this.getLookupMenuOptionsFromData( this.lookupCache[ value ] ) );
5552 } else {
5553 this.pushPending();
5554 this.lookupQuery = value;
5555 ourRequest = this.lookupRequest = this.getLookupRequest();
5556 ourRequest
5557 .always( function () {
5558 // We need to pop pending even if this is an old request, otherwise
5559 // the widget will remain pending forever.
5560 // TODO: this assumes that an aborted request will fail or succeed soon after
5561 // being aborted, or at least eventually. It would be nice if we could popPending()
5562 // at abort time, but only if we knew that we hadn't already called popPending()
5563 // for that request.
5564 widget.popPending();
5565 } )
5566 .done( function ( data ) {
5567 // If this is an old request (and aborting it somehow caused it to still succeed),
5568 // ignore its success completely
5569 if ( ourRequest === widget.lookupRequest ) {
5570 widget.lookupQuery = null;
5571 widget.lookupRequest = null;
5572 widget.lookupCache[ value ] = widget.getLookupCacheDataFromResponse( data );
5573 deferred.resolve( widget.getLookupMenuOptionsFromData( widget.lookupCache[ value ] ) );
5574 }
5575 } )
5576 .fail( function () {
5577 // If this is an old request (or a request failing because it's being aborted),
5578 // ignore its failure completely
5579 if ( ourRequest === widget.lookupRequest ) {
5580 widget.lookupQuery = null;
5581 widget.lookupRequest = null;
5582 deferred.reject();
5583 }
5584 } );
5585 }
5586 return deferred.promise();
5587 };
5588
5589 /**
5590 * Abort the currently pending lookup request, if any.
5591 */
5592 OO.ui.LookupElement.prototype.abortLookupRequest = function () {
5593 var oldRequest = this.lookupRequest;
5594 if ( oldRequest ) {
5595 // First unset this.lookupRequest to the fail handler will notice
5596 // that the request is no longer current
5597 this.lookupRequest = null;
5598 this.lookupQuery = null;
5599 oldRequest.abort();
5600 }
5601 };
5602
5603 /**
5604 * Get a new request object of the current lookup query value.
5605 *
5606 * @abstract
5607 * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
5608 */
5609 OO.ui.LookupElement.prototype.getLookupRequest = function () {
5610 // Stub, implemented in subclass
5611 return null;
5612 };
5613
5614 /**
5615 * Pre-process data returned by the request from #getLookupRequest.
5616 *
5617 * The return value of this function will be cached, and any further queries for the given value
5618 * will use the cache rather than doing API requests.
5619 *
5620 * @abstract
5621 * @param {Mixed} data Response from server
5622 * @return {Mixed} Cached result data
5623 */
5624 OO.ui.LookupElement.prototype.getLookupCacheDataFromResponse = function () {
5625 // Stub, implemented in subclass
5626 return [];
5627 };
5628
5629 /**
5630 * Get a list of menu option widgets from the (possibly cached) data returned by
5631 * #getLookupCacheDataFromResponse.
5632 *
5633 * @abstract
5634 * @param {Mixed} data Cached result data, usually an array
5635 * @return {OO.ui.MenuOptionWidget[]} Menu items
5636 */
5637 OO.ui.LookupElement.prototype.getLookupMenuOptionsFromData = function () {
5638 // Stub, implemented in subclass
5639 return [];
5640 };
5641
5642 /**
5643 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
5644 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
5645 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
5646 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
5647 *
5648 * @abstract
5649 * @class
5650 *
5651 * @constructor
5652 * @param {Object} [config] Configuration options
5653 * @cfg {Object} [popup] Configuration to pass to popup
5654 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
5655 */
5656 OO.ui.PopupElement = function OoUiPopupElement( config ) {
5657 // Configuration initialization
5658 config = config || {};
5659
5660 // Properties
5661 this.popup = new OO.ui.PopupWidget( $.extend(
5662 { autoClose: true },
5663 config.popup,
5664 { $autoCloseIgnore: this.$element }
5665 ) );
5666 };
5667
5668 /* Methods */
5669
5670 /**
5671 * Get popup.
5672 *
5673 * @return {OO.ui.PopupWidget} Popup widget
5674 */
5675 OO.ui.PopupElement.prototype.getPopup = function () {
5676 return this.popup;
5677 };
5678
5679 /**
5680 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
5681 * additional functionality to an element created by another class. The class provides
5682 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
5683 * which are used to customize the look and feel of a widget to better describe its
5684 * importance and functionality.
5685 *
5686 * The library currently contains the following styling flags for general use:
5687 *
5688 * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
5689 * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
5690 * - **constructive**: Constructive styling is applied to convey that the widget will create something.
5691 *
5692 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
5693 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
5694 *
5695 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
5696 *
5697 * @abstract
5698 * @class
5699 *
5700 * @constructor
5701 * @param {Object} [config] Configuration options
5702 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'constructive' or 'primary') to apply.
5703 * Please see the [OOjs UI documentation on MediaWiki] [2] for more information about available flags.
5704 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
5705 * @cfg {jQuery} [$flagged] Flagged node, assigned to $flagged, omit to use $element
5706 */
5707 OO.ui.FlaggedElement = function OoUiFlaggedElement( config ) {
5708 // Configuration initialization
5709 config = config || {};
5710
5711 // Properties
5712 this.flags = {};
5713 this.$flagged = null;
5714
5715 // Initialization
5716 this.setFlags( config.flags );
5717 this.setFlaggedElement( config.$flagged || this.$element );
5718 };
5719
5720 /* Events */
5721
5722 /**
5723 * @event flag
5724 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
5725 * parameter contains the name of each modified flag and indicates whether it was
5726 * added or removed.
5727 *
5728 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
5729 * that the flag was added, `false` that the flag was removed.
5730 */
5731
5732 /* Methods */
5733
5734 /**
5735 * Set the flagged element.
5736 *
5737 * If an element is already set, it will be cleaned up before setting up the new element.
5738 *
5739 * @param {jQuery} $flagged Element to add flags to
5740 */
5741 OO.ui.FlaggedElement.prototype.setFlaggedElement = function ( $flagged ) {
5742 var classNames = Object.keys( this.flags ).map( function ( flag ) {
5743 return 'oo-ui-flaggedElement-' + flag;
5744 } ).join( ' ' );
5745
5746 if ( this.$flagged ) {
5747 this.$flagged.removeClass( classNames );
5748 }
5749
5750 this.$flagged = $flagged.addClass( classNames );
5751 };
5752
5753 /**
5754 * Check if a flag is set.
5755 *
5756 * @param {string} flag Name of flag
5757 * @return {boolean} Has flag
5758 */
5759 OO.ui.FlaggedElement.prototype.hasFlag = function ( flag ) {
5760 return flag in this.flags;
5761 };
5762
5763 /**
5764 * Get the names of all flags set.
5765 *
5766 * @return {string[]} Flag names
5767 */
5768 OO.ui.FlaggedElement.prototype.getFlags = function () {
5769 return Object.keys( this.flags );
5770 };
5771
5772 /**
5773 * Clear all flags.
5774 *
5775 * @chainable
5776 * @fires flag
5777 */
5778 OO.ui.FlaggedElement.prototype.clearFlags = function () {
5779 var flag, className,
5780 changes = {},
5781 remove = [],
5782 classPrefix = 'oo-ui-flaggedElement-';
5783
5784 for ( flag in this.flags ) {
5785 className = classPrefix + flag;
5786 changes[ flag ] = false;
5787 delete this.flags[ flag ];
5788 remove.push( className );
5789 }
5790
5791 if ( this.$flagged ) {
5792 this.$flagged.removeClass( remove.join( ' ' ) );
5793 }
5794
5795 this.updateThemeClasses();
5796 this.emit( 'flag', changes );
5797
5798 return this;
5799 };
5800
5801 /**
5802 * Add one or more flags.
5803 *
5804 * @param {string|string[]|Object.<string, boolean>} flags One or more flags to add, or an object
5805 * keyed by flag name containing boolean set/remove instructions.
5806 * @chainable
5807 * @fires flag
5808 */
5809 OO.ui.FlaggedElement.prototype.setFlags = function ( flags ) {
5810 var i, len, flag, className,
5811 changes = {},
5812 add = [],
5813 remove = [],
5814 classPrefix = 'oo-ui-flaggedElement-';
5815
5816 if ( typeof flags === 'string' ) {
5817 className = classPrefix + flags;
5818 // Set
5819 if ( !this.flags[ flags ] ) {
5820 this.flags[ flags ] = true;
5821 add.push( className );
5822 }
5823 } else if ( Array.isArray( flags ) ) {
5824 for ( i = 0, len = flags.length; i < len; i++ ) {
5825 flag = flags[ i ];
5826 className = classPrefix + flag;
5827 // Set
5828 if ( !this.flags[ flag ] ) {
5829 changes[ flag ] = true;
5830 this.flags[ flag ] = true;
5831 add.push( className );
5832 }
5833 }
5834 } else if ( OO.isPlainObject( flags ) ) {
5835 for ( flag in flags ) {
5836 className = classPrefix + flag;
5837 if ( flags[ flag ] ) {
5838 // Set
5839 if ( !this.flags[ flag ] ) {
5840 changes[ flag ] = true;
5841 this.flags[ flag ] = true;
5842 add.push( className );
5843 }
5844 } else {
5845 // Remove
5846 if ( this.flags[ flag ] ) {
5847 changes[ flag ] = false;
5848 delete this.flags[ flag ];
5849 remove.push( className );
5850 }
5851 }
5852 }
5853 }
5854
5855 if ( this.$flagged ) {
5856 this.$flagged
5857 .addClass( add.join( ' ' ) )
5858 .removeClass( remove.join( ' ' ) );
5859 }
5860
5861 this.updateThemeClasses();
5862 this.emit( 'flag', changes );
5863
5864 return this;
5865 };
5866
5867 /**
5868 * TitledElement is mixed into other classes to provide a `title` attribute.
5869 * Titles are rendered by the browser and are made visible when the user moves
5870 * the mouse over the element. Titles are not visible on touch devices.
5871 *
5872 * @example
5873 * // TitledElement provides a 'title' attribute to the
5874 * // ButtonWidget class
5875 * var button = new OO.ui.ButtonWidget( {
5876 * label : 'Button with Title',
5877 * title : 'I am a button'
5878 * } );
5879 * $( 'body' ).append( button.$element );
5880 *
5881 * @abstract
5882 * @class
5883 *
5884 * @constructor
5885 * @param {Object} [config] Configuration options
5886 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
5887 * If this config is omitted, the title functionality is applied to $element, the
5888 * element created by the class.
5889 * @cfg {string|Function} [title] The title text or a function that returns text. If
5890 * this config is omitted, the value of the static `title` property is used.
5891 */
5892 OO.ui.TitledElement = function OoUiTitledElement( config ) {
5893 // Configuration initialization
5894 config = config || {};
5895
5896 // Properties
5897 this.$titled = null;
5898 this.title = null;
5899
5900 // Initialization
5901 this.setTitle( config.title || this.constructor.static.title );
5902 this.setTitledElement( config.$titled || this.$element );
5903 };
5904
5905 /* Setup */
5906
5907 OO.initClass( OO.ui.TitledElement );
5908
5909 /* Static Properties */
5910
5911 /**
5912 * The title text, a function that returns text, or `null` for no title. The value of the static property
5913 * is overridden if the #title config option is used.
5914 *
5915 * @static
5916 * @inheritable
5917 * @property {string|Function|null}
5918 */
5919 OO.ui.TitledElement.static.title = null;
5920
5921 /* Methods */
5922
5923 /**
5924 * Set the titled element.
5925 *
5926 * If an element is already set, it will be cleaned up before setting up the new element.
5927 *
5928 * @param {jQuery} $titled Element to set title on
5929 */
5930 OO.ui.TitledElement.prototype.setTitledElement = function ( $titled ) {
5931 if ( this.$titled ) {
5932 this.$titled.removeAttr( 'title' );
5933 }
5934
5935 this.$titled = $titled;
5936 if ( this.title ) {
5937 this.$titled.attr( 'title', this.title );
5938 }
5939 };
5940
5941 /**
5942 * Set title.
5943 *
5944 * @param {string|Function|null} title Title text, a function that returns text or null for no title
5945 * @chainable
5946 */
5947 OO.ui.TitledElement.prototype.setTitle = function ( title ) {
5948 title = typeof title === 'string' ? OO.ui.resolveMsg( title ) : null;
5949
5950 if ( this.title !== title ) {
5951 if ( this.$titled ) {
5952 if ( title !== null ) {
5953 this.$titled.attr( 'title', title );
5954 } else {
5955 this.$titled.removeAttr( 'title' );
5956 }
5957 }
5958 this.title = title;
5959 }
5960
5961 return this;
5962 };
5963
5964 /**
5965 * Get title.
5966 *
5967 * @return {string} Title string
5968 */
5969 OO.ui.TitledElement.prototype.getTitle = function () {
5970 return this.title;
5971 };
5972
5973 /**
5974 * Element that can be automatically clipped to visible boundaries.
5975 *
5976 * Whenever the element's natural height changes, you have to call
5977 * #clip to make sure it's still clipping correctly.
5978 *
5979 * @abstract
5980 * @class
5981 *
5982 * @constructor
5983 * @param {Object} [config] Configuration options
5984 * @cfg {jQuery} [$clippable] Nodes to clip, assigned to #$clippable, omit to use #$element
5985 */
5986 OO.ui.ClippableElement = function OoUiClippableElement( config ) {
5987 // Configuration initialization
5988 config = config || {};
5989
5990 // Properties
5991 this.$clippable = null;
5992 this.clipping = false;
5993 this.clippedHorizontally = false;
5994 this.clippedVertically = false;
5995 this.$clippableContainer = null;
5996 this.$clippableScroller = null;
5997 this.$clippableWindow = null;
5998 this.idealWidth = null;
5999 this.idealHeight = null;
6000 this.onClippableContainerScrollHandler = this.clip.bind( this );
6001 this.onClippableWindowResizeHandler = this.clip.bind( this );
6002
6003 // Initialization
6004 this.setClippableElement( config.$clippable || this.$element );
6005 };
6006
6007 /* Methods */
6008
6009 /**
6010 * Set clippable element.
6011 *
6012 * If an element is already set, it will be cleaned up before setting up the new element.
6013 *
6014 * @param {jQuery} $clippable Element to make clippable
6015 */
6016 OO.ui.ClippableElement.prototype.setClippableElement = function ( $clippable ) {
6017 if ( this.$clippable ) {
6018 this.$clippable.removeClass( 'oo-ui-clippableElement-clippable' );
6019 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
6020 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
6021 }
6022
6023 this.$clippable = $clippable.addClass( 'oo-ui-clippableElement-clippable' );
6024 this.clip();
6025 };
6026
6027 /**
6028 * Toggle clipping.
6029 *
6030 * Do not turn clipping on until after the element is attached to the DOM and visible.
6031 *
6032 * @param {boolean} [clipping] Enable clipping, omit to toggle
6033 * @chainable
6034 */
6035 OO.ui.ClippableElement.prototype.toggleClipping = function ( clipping ) {
6036 clipping = clipping === undefined ? !this.clipping : !!clipping;
6037
6038 if ( this.clipping !== clipping ) {
6039 this.clipping = clipping;
6040 if ( clipping ) {
6041 this.$clippableContainer = $( this.getClosestScrollableElementContainer() );
6042 // If the clippable container is the root, we have to listen to scroll events and check
6043 // jQuery.scrollTop on the window because of browser inconsistencies
6044 this.$clippableScroller = this.$clippableContainer.is( 'html, body' ) ?
6045 $( OO.ui.Element.static.getWindow( this.$clippableContainer ) ) :
6046 this.$clippableContainer;
6047 this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler );
6048 this.$clippableWindow = $( this.getElementWindow() )
6049 .on( 'resize', this.onClippableWindowResizeHandler );
6050 // Initial clip after visible
6051 this.clip();
6052 } else {
6053 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
6054 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
6055
6056 this.$clippableContainer = null;
6057 this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler );
6058 this.$clippableScroller = null;
6059 this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
6060 this.$clippableWindow = null;
6061 }
6062 }
6063
6064 return this;
6065 };
6066
6067 /**
6068 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
6069 *
6070 * @return {boolean} Element will be clipped to the visible area
6071 */
6072 OO.ui.ClippableElement.prototype.isClipping = function () {
6073 return this.clipping;
6074 };
6075
6076 /**
6077 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
6078 *
6079 * @return {boolean} Part of the element is being clipped
6080 */
6081 OO.ui.ClippableElement.prototype.isClipped = function () {
6082 return this.clippedHorizontally || this.clippedVertically;
6083 };
6084
6085 /**
6086 * Check if the right of the element is being clipped by the nearest scrollable container.
6087 *
6088 * @return {boolean} Part of the element is being clipped
6089 */
6090 OO.ui.ClippableElement.prototype.isClippedHorizontally = function () {
6091 return this.clippedHorizontally;
6092 };
6093
6094 /**
6095 * Check if the bottom of the element is being clipped by the nearest scrollable container.
6096 *
6097 * @return {boolean} Part of the element is being clipped
6098 */
6099 OO.ui.ClippableElement.prototype.isClippedVertically = function () {
6100 return this.clippedVertically;
6101 };
6102
6103 /**
6104 * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
6105 *
6106 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
6107 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
6108 */
6109 OO.ui.ClippableElement.prototype.setIdealSize = function ( width, height ) {
6110 this.idealWidth = width;
6111 this.idealHeight = height;
6112
6113 if ( !this.clipping ) {
6114 // Update dimensions
6115 this.$clippable.css( { width: width, height: height } );
6116 }
6117 // While clipping, idealWidth and idealHeight are not considered
6118 };
6119
6120 /**
6121 * Clip element to visible boundaries and allow scrolling when needed. Call this method when
6122 * the element's natural height changes.
6123 *
6124 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
6125 * overlapped by, the visible area of the nearest scrollable container.
6126 *
6127 * @chainable
6128 */
6129 OO.ui.ClippableElement.prototype.clip = function () {
6130 if ( !this.clipping ) {
6131 // this.$clippableContainer and this.$clippableWindow are null, so the below will fail
6132 return this;
6133 }
6134
6135 var buffer = 7, // Chosen by fair dice roll
6136 cOffset = this.$clippable.offset(),
6137 $container = this.$clippableContainer.is( 'html, body' ) ?
6138 this.$clippableWindow : this.$clippableContainer,
6139 ccOffset = $container.offset() || { top: 0, left: 0 },
6140 ccHeight = $container.innerHeight() - buffer,
6141 ccWidth = $container.innerWidth() - buffer,
6142 cHeight = this.$clippable.outerHeight() + buffer,
6143 cWidth = this.$clippable.outerWidth() + buffer,
6144 scrollTop = this.$clippableScroller.scrollTop(),
6145 scrollLeft = this.$clippableScroller.scrollLeft(),
6146 desiredWidth = cOffset.left < 0 ?
6147 cWidth + cOffset.left :
6148 ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left,
6149 desiredHeight = cOffset.top < 0 ?
6150 cHeight + cOffset.top :
6151 ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top,
6152 naturalWidth = this.$clippable.prop( 'scrollWidth' ),
6153 naturalHeight = this.$clippable.prop( 'scrollHeight' ),
6154 clipWidth = desiredWidth < naturalWidth,
6155 clipHeight = desiredHeight < naturalHeight;
6156
6157 if ( clipWidth ) {
6158 this.$clippable.css( { overflowX: 'scroll', width: desiredWidth } );
6159 } else {
6160 this.$clippable.css( { width: this.idealWidth || '', overflowX: '' } );
6161 }
6162 if ( clipHeight ) {
6163 this.$clippable.css( { overflowY: 'scroll', height: desiredHeight } );
6164 } else {
6165 this.$clippable.css( { height: this.idealHeight || '', overflowY: '' } );
6166 }
6167
6168 // If we stopped clipping in at least one of the dimensions
6169 if ( !clipWidth || !clipHeight ) {
6170 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
6171 }
6172
6173 this.clippedHorizontally = clipWidth;
6174 this.clippedVertically = clipHeight;
6175
6176 return this;
6177 };
6178
6179 /**
6180 * Generic toolbar tool.
6181 *
6182 * @abstract
6183 * @class
6184 * @extends OO.ui.Widget
6185 * @mixins OO.ui.IconElement
6186 * @mixins OO.ui.FlaggedElement
6187 *
6188 * @constructor
6189 * @param {OO.ui.ToolGroup} toolGroup
6190 * @param {Object} [config] Configuration options
6191 * @cfg {string|Function} [title] Title text or a function that returns text
6192 */
6193 OO.ui.Tool = function OoUiTool( toolGroup, config ) {
6194 // Allow passing positional parameters inside the config object
6195 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
6196 config = toolGroup;
6197 toolGroup = config.toolGroup;
6198 }
6199
6200 // Configuration initialization
6201 config = config || {};
6202
6203 // Parent constructor
6204 OO.ui.Tool.super.call( this, config );
6205
6206 // Mixin constructors
6207 OO.ui.IconElement.call( this, config );
6208 OO.ui.FlaggedElement.call( this, config );
6209
6210 // Properties
6211 this.toolGroup = toolGroup;
6212 this.toolbar = this.toolGroup.getToolbar();
6213 this.active = false;
6214 this.$title = $( '<span>' );
6215 this.$accel = $( '<span>' );
6216 this.$link = $( '<a>' );
6217 this.title = null;
6218
6219 // Events
6220 this.toolbar.connect( this, { updateState: 'onUpdateState' } );
6221
6222 // Initialization
6223 this.$title.addClass( 'oo-ui-tool-title' );
6224 this.$accel
6225 .addClass( 'oo-ui-tool-accel' )
6226 .prop( {
6227 // This may need to be changed if the key names are ever localized,
6228 // but for now they are essentially written in English
6229 dir: 'ltr',
6230 lang: 'en'
6231 } );
6232 this.$link
6233 .addClass( 'oo-ui-tool-link' )
6234 .append( this.$icon, this.$title, this.$accel )
6235 .prop( 'tabIndex', 0 )
6236 .attr( 'role', 'button' );
6237 this.$element
6238 .data( 'oo-ui-tool', this )
6239 .addClass(
6240 'oo-ui-tool ' + 'oo-ui-tool-name-' +
6241 this.constructor.static.name.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' )
6242 )
6243 .append( this.$link );
6244 this.setTitle( config.title || this.constructor.static.title );
6245 };
6246
6247 /* Setup */
6248
6249 OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
6250 OO.mixinClass( OO.ui.Tool, OO.ui.IconElement );
6251 OO.mixinClass( OO.ui.Tool, OO.ui.FlaggedElement );
6252
6253 /* Events */
6254
6255 /**
6256 * @event select
6257 */
6258
6259 /* Static Properties */
6260
6261 /**
6262 * @static
6263 * @inheritdoc
6264 */
6265 OO.ui.Tool.static.tagName = 'span';
6266
6267 /**
6268 * Symbolic name of tool.
6269 *
6270 * @abstract
6271 * @static
6272 * @inheritable
6273 * @property {string}
6274 */
6275 OO.ui.Tool.static.name = '';
6276
6277 /**
6278 * Tool group.
6279 *
6280 * @abstract
6281 * @static
6282 * @inheritable
6283 * @property {string}
6284 */
6285 OO.ui.Tool.static.group = '';
6286
6287 /**
6288 * Tool title.
6289 *
6290 * Title is used as a tooltip when the tool is part of a bar tool group, or a label when the tool
6291 * is part of a list or menu tool group. If a trigger is associated with an action by the same name
6292 * as the tool, a description of its keyboard shortcut for the appropriate platform will be
6293 * appended to the title if the tool is part of a bar tool group.
6294 *
6295 * @abstract
6296 * @static
6297 * @inheritable
6298 * @property {string|Function} Title text or a function that returns text
6299 */
6300 OO.ui.Tool.static.title = '';
6301
6302 /**
6303 * Tool can be automatically added to catch-all groups.
6304 *
6305 * @static
6306 * @inheritable
6307 * @property {boolean}
6308 */
6309 OO.ui.Tool.static.autoAddToCatchall = true;
6310
6311 /**
6312 * Tool can be automatically added to named groups.
6313 *
6314 * @static
6315 * @property {boolean}
6316 * @inheritable
6317 */
6318 OO.ui.Tool.static.autoAddToGroup = true;
6319
6320 /**
6321 * Check if this tool is compatible with given data.
6322 *
6323 * @static
6324 * @inheritable
6325 * @param {Mixed} data Data to check
6326 * @return {boolean} Tool can be used with data
6327 */
6328 OO.ui.Tool.static.isCompatibleWith = function () {
6329 return false;
6330 };
6331
6332 /* Methods */
6333
6334 /**
6335 * Handle the toolbar state being updated.
6336 *
6337 * This is an abstract method that must be overridden in a concrete subclass.
6338 *
6339 * @abstract
6340 */
6341 OO.ui.Tool.prototype.onUpdateState = function () {
6342 throw new Error(
6343 'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor
6344 );
6345 };
6346
6347 /**
6348 * Handle the tool being selected.
6349 *
6350 * This is an abstract method that must be overridden in a concrete subclass.
6351 *
6352 * @abstract
6353 */
6354 OO.ui.Tool.prototype.onSelect = function () {
6355 throw new Error(
6356 'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor
6357 );
6358 };
6359
6360 /**
6361 * Check if the button is active.
6362 *
6363 * @return {boolean} Button is active
6364 */
6365 OO.ui.Tool.prototype.isActive = function () {
6366 return this.active;
6367 };
6368
6369 /**
6370 * Make the button appear active or inactive.
6371 *
6372 * @param {boolean} state Make button appear active
6373 */
6374 OO.ui.Tool.prototype.setActive = function ( state ) {
6375 this.active = !!state;
6376 if ( this.active ) {
6377 this.$element.addClass( 'oo-ui-tool-active' );
6378 } else {
6379 this.$element.removeClass( 'oo-ui-tool-active' );
6380 }
6381 };
6382
6383 /**
6384 * Get the tool title.
6385 *
6386 * @param {string|Function} title Title text or a function that returns text
6387 * @chainable
6388 */
6389 OO.ui.Tool.prototype.setTitle = function ( title ) {
6390 this.title = OO.ui.resolveMsg( title );
6391 this.updateTitle();
6392 return this;
6393 };
6394
6395 /**
6396 * Get the tool title.
6397 *
6398 * @return {string} Title text
6399 */
6400 OO.ui.Tool.prototype.getTitle = function () {
6401 return this.title;
6402 };
6403
6404 /**
6405 * Get the tool's symbolic name.
6406 *
6407 * @return {string} Symbolic name of tool
6408 */
6409 OO.ui.Tool.prototype.getName = function () {
6410 return this.constructor.static.name;
6411 };
6412
6413 /**
6414 * Update the title.
6415 */
6416 OO.ui.Tool.prototype.updateTitle = function () {
6417 var titleTooltips = this.toolGroup.constructor.static.titleTooltips,
6418 accelTooltips = this.toolGroup.constructor.static.accelTooltips,
6419 accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
6420 tooltipParts = [];
6421
6422 this.$title.text( this.title );
6423 this.$accel.text( accel );
6424
6425 if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
6426 tooltipParts.push( this.title );
6427 }
6428 if ( accelTooltips && typeof accel === 'string' && accel.length ) {
6429 tooltipParts.push( accel );
6430 }
6431 if ( tooltipParts.length ) {
6432 this.$link.attr( 'title', tooltipParts.join( ' ' ) );
6433 } else {
6434 this.$link.removeAttr( 'title' );
6435 }
6436 };
6437
6438 /**
6439 * Destroy tool.
6440 */
6441 OO.ui.Tool.prototype.destroy = function () {
6442 this.toolbar.disconnect( this );
6443 this.$element.remove();
6444 };
6445
6446 /**
6447 * Collection of tool groups.
6448 *
6449 * @class
6450 * @extends OO.ui.Element
6451 * @mixins OO.EventEmitter
6452 * @mixins OO.ui.GroupElement
6453 *
6454 * @constructor
6455 * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
6456 * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating tool groups
6457 * @param {Object} [config] Configuration options
6458 * @cfg {boolean} [actions] Add an actions section opposite to the tools
6459 * @cfg {boolean} [shadow] Add a shadow below the toolbar
6460 */
6461 OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
6462 // Allow passing positional parameters inside the config object
6463 if ( OO.isPlainObject( toolFactory ) && config === undefined ) {
6464 config = toolFactory;
6465 toolFactory = config.toolFactory;
6466 toolGroupFactory = config.toolGroupFactory;
6467 }
6468
6469 // Configuration initialization
6470 config = config || {};
6471
6472 // Parent constructor
6473 OO.ui.Toolbar.super.call( this, config );
6474
6475 // Mixin constructors
6476 OO.EventEmitter.call( this );
6477 OO.ui.GroupElement.call( this, config );
6478
6479 // Properties
6480 this.toolFactory = toolFactory;
6481 this.toolGroupFactory = toolGroupFactory;
6482 this.groups = [];
6483 this.tools = {};
6484 this.$bar = $( '<div>' );
6485 this.$actions = $( '<div>' );
6486 this.initialized = false;
6487 this.onWindowResizeHandler = this.onWindowResize.bind( this );
6488
6489 // Events
6490 this.$element
6491 .add( this.$bar ).add( this.$group ).add( this.$actions )
6492 .on( 'mousedown', this.onPointerDown.bind( this ) );
6493
6494 // Initialization
6495 this.$group.addClass( 'oo-ui-toolbar-tools' );
6496 if ( config.actions ) {
6497 this.$bar.append( this.$actions.addClass( 'oo-ui-toolbar-actions' ) );
6498 }
6499 this.$bar
6500 .addClass( 'oo-ui-toolbar-bar' )
6501 .append( this.$group, '<div style="clear:both"></div>' );
6502 if ( config.shadow ) {
6503 this.$bar.append( '<div class="oo-ui-toolbar-shadow"></div>' );
6504 }
6505 this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar );
6506 };
6507
6508 /* Setup */
6509
6510 OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
6511 OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
6512 OO.mixinClass( OO.ui.Toolbar, OO.ui.GroupElement );
6513
6514 /* Methods */
6515
6516 /**
6517 * Get the tool factory.
6518 *
6519 * @return {OO.ui.ToolFactory} Tool factory
6520 */
6521 OO.ui.Toolbar.prototype.getToolFactory = function () {
6522 return this.toolFactory;
6523 };
6524
6525 /**
6526 * Get the tool group factory.
6527 *
6528 * @return {OO.Factory} Tool group factory
6529 */
6530 OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
6531 return this.toolGroupFactory;
6532 };
6533
6534 /**
6535 * Handles mouse down events.
6536 *
6537 * @param {jQuery.Event} e Mouse down event
6538 */
6539 OO.ui.Toolbar.prototype.onPointerDown = function ( e ) {
6540 var $closestWidgetToEvent = $( e.target ).closest( '.oo-ui-widget' ),
6541 $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
6542 if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[ 0 ] === $closestWidgetToToolbar[ 0 ] ) {
6543 return false;
6544 }
6545 };
6546
6547 /**
6548 * Handle window resize event.
6549 *
6550 * @private
6551 * @param {jQuery.Event} e Window resize event
6552 */
6553 OO.ui.Toolbar.prototype.onWindowResize = function () {
6554 this.$element.toggleClass(
6555 'oo-ui-toolbar-narrow',
6556 this.$bar.width() <= this.narrowThreshold
6557 );
6558 };
6559
6560 /**
6561 * Sets up handles and preloads required information for the toolbar to work.
6562 * This must be called after it is attached to a visible document and before doing anything else.
6563 */
6564 OO.ui.Toolbar.prototype.initialize = function () {
6565 this.initialized = true;
6566 this.narrowThreshold = this.$group.width() + this.$actions.width();
6567 $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
6568 this.onWindowResize();
6569 };
6570
6571 /**
6572 * Setup toolbar.
6573 *
6574 * Tools can be specified in the following ways:
6575 *
6576 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
6577 * - All tools in a group: `{ group: 'group-name' }`
6578 * - All tools: `'*'` - Using this will make the group a list with a "More" label by default
6579 *
6580 * @param {Object.<string,Array>} groups List of tool group configurations
6581 * @param {Array|string} [groups.include] Tools to include
6582 * @param {Array|string} [groups.exclude] Tools to exclude
6583 * @param {Array|string} [groups.promote] Tools to promote to the beginning
6584 * @param {Array|string} [groups.demote] Tools to demote to the end
6585 */
6586 OO.ui.Toolbar.prototype.setup = function ( groups ) {
6587 var i, len, type, group,
6588 items = [],
6589 defaultType = 'bar';
6590
6591 // Cleanup previous groups
6592 this.reset();
6593
6594 // Build out new groups
6595 for ( i = 0, len = groups.length; i < len; i++ ) {
6596 group = groups[ i ];
6597 if ( group.include === '*' ) {
6598 // Apply defaults to catch-all groups
6599 if ( group.type === undefined ) {
6600 group.type = 'list';
6601 }
6602 if ( group.label === undefined ) {
6603 group.label = OO.ui.msg( 'ooui-toolbar-more' );
6604 }
6605 }
6606 // Check type has been registered
6607 type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType;
6608 items.push(
6609 this.getToolGroupFactory().create( type, this, group )
6610 );
6611 }
6612 this.addItems( items );
6613 };
6614
6615 /**
6616 * Remove all tools and groups from the toolbar.
6617 */
6618 OO.ui.Toolbar.prototype.reset = function () {
6619 var i, len;
6620
6621 this.groups = [];
6622 this.tools = {};
6623 for ( i = 0, len = this.items.length; i < len; i++ ) {
6624 this.items[ i ].destroy();
6625 }
6626 this.clearItems();
6627 };
6628
6629 /**
6630 * Destroys toolbar, removing event handlers and DOM elements.
6631 *
6632 * Call this whenever you are done using a toolbar.
6633 */
6634 OO.ui.Toolbar.prototype.destroy = function () {
6635 $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
6636 this.reset();
6637 this.$element.remove();
6638 };
6639
6640 /**
6641 * Check if tool has not been used yet.
6642 *
6643 * @param {string} name Symbolic name of tool
6644 * @return {boolean} Tool is available
6645 */
6646 OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
6647 return !this.tools[ name ];
6648 };
6649
6650 /**
6651 * Prevent tool from being used again.
6652 *
6653 * @param {OO.ui.Tool} tool Tool to reserve
6654 */
6655 OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
6656 this.tools[ tool.getName() ] = tool;
6657 };
6658
6659 /**
6660 * Allow tool to be used again.
6661 *
6662 * @param {OO.ui.Tool} tool Tool to release
6663 */
6664 OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
6665 delete this.tools[ tool.getName() ];
6666 };
6667
6668 /**
6669 * Get accelerator label for tool.
6670 *
6671 * This is a stub that should be overridden to provide access to accelerator information.
6672 *
6673 * @param {string} name Symbolic name of tool
6674 * @return {string|undefined} Tool accelerator label if available
6675 */
6676 OO.ui.Toolbar.prototype.getToolAccelerator = function () {
6677 return undefined;
6678 };
6679
6680 /**
6681 * Collection of tools.
6682 *
6683 * Tools can be specified in the following ways:
6684 *
6685 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
6686 * - All tools in a group: `{ group: 'group-name' }`
6687 * - All tools: `'*'`
6688 *
6689 * @abstract
6690 * @class
6691 * @extends OO.ui.Widget
6692 * @mixins OO.ui.GroupElement
6693 *
6694 * @constructor
6695 * @param {OO.ui.Toolbar} toolbar
6696 * @param {Object} [config] Configuration options
6697 * @cfg {Array|string} [include=[]] List of tools to include
6698 * @cfg {Array|string} [exclude=[]] List of tools to exclude
6699 * @cfg {Array|string} [promote=[]] List of tools to promote to the beginning
6700 * @cfg {Array|string} [demote=[]] List of tools to demote to the end
6701 */
6702 OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
6703 // Allow passing positional parameters inside the config object
6704 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
6705 config = toolbar;
6706 toolbar = config.toolbar;
6707 }
6708
6709 // Configuration initialization
6710 config = config || {};
6711
6712 // Parent constructor
6713 OO.ui.ToolGroup.super.call( this, config );
6714
6715 // Mixin constructors
6716 OO.ui.GroupElement.call( this, config );
6717
6718 // Properties
6719 this.toolbar = toolbar;
6720 this.tools = {};
6721 this.pressed = null;
6722 this.autoDisabled = false;
6723 this.include = config.include || [];
6724 this.exclude = config.exclude || [];
6725 this.promote = config.promote || [];
6726 this.demote = config.demote || [];
6727 this.onCapturedMouseUpHandler = this.onCapturedMouseUp.bind( this );
6728
6729 // Events
6730 this.$element.on( {
6731 mousedown: this.onPointerDown.bind( this ),
6732 mouseup: this.onPointerUp.bind( this ),
6733 mouseover: this.onMouseOver.bind( this ),
6734 mouseout: this.onMouseOut.bind( this )
6735 } );
6736 this.toolbar.getToolFactory().connect( this, { register: 'onToolFactoryRegister' } );
6737 this.aggregate( { disable: 'itemDisable' } );
6738 this.connect( this, { itemDisable: 'updateDisabled' } );
6739
6740 // Initialization
6741 this.$group.addClass( 'oo-ui-toolGroup-tools' );
6742 this.$element
6743 .addClass( 'oo-ui-toolGroup' )
6744 .append( this.$group );
6745 this.populate();
6746 };
6747
6748 /* Setup */
6749
6750 OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget );
6751 OO.mixinClass( OO.ui.ToolGroup, OO.ui.GroupElement );
6752
6753 /* Events */
6754
6755 /**
6756 * @event update
6757 */
6758
6759 /* Static Properties */
6760
6761 /**
6762 * Show labels in tooltips.
6763 *
6764 * @static
6765 * @inheritable
6766 * @property {boolean}
6767 */
6768 OO.ui.ToolGroup.static.titleTooltips = false;
6769
6770 /**
6771 * Show acceleration labels in tooltips.
6772 *
6773 * @static
6774 * @inheritable
6775 * @property {boolean}
6776 */
6777 OO.ui.ToolGroup.static.accelTooltips = false;
6778
6779 /**
6780 * Automatically disable the toolgroup when all tools are disabled
6781 *
6782 * @static
6783 * @inheritable
6784 * @property {boolean}
6785 */
6786 OO.ui.ToolGroup.static.autoDisable = true;
6787
6788 /* Methods */
6789
6790 /**
6791 * @inheritdoc
6792 */
6793 OO.ui.ToolGroup.prototype.isDisabled = function () {
6794 return this.autoDisabled || OO.ui.ToolGroup.super.prototype.isDisabled.apply( this, arguments );
6795 };
6796
6797 /**
6798 * @inheritdoc
6799 */
6800 OO.ui.ToolGroup.prototype.updateDisabled = function () {
6801 var i, item, allDisabled = true;
6802
6803 if ( this.constructor.static.autoDisable ) {
6804 for ( i = this.items.length - 1; i >= 0; i-- ) {
6805 item = this.items[ i ];
6806 if ( !item.isDisabled() ) {
6807 allDisabled = false;
6808 break;
6809 }
6810 }
6811 this.autoDisabled = allDisabled;
6812 }
6813 OO.ui.ToolGroup.super.prototype.updateDisabled.apply( this, arguments );
6814 };
6815
6816 /**
6817 * Handle mouse down events.
6818 *
6819 * @param {jQuery.Event} e Mouse down event
6820 */
6821 OO.ui.ToolGroup.prototype.onPointerDown = function ( e ) {
6822 if ( !this.isDisabled() && e.which === 1 ) {
6823 this.pressed = this.getTargetTool( e );
6824 if ( this.pressed ) {
6825 this.pressed.setActive( true );
6826 this.getElementDocument().addEventListener(
6827 'mouseup', this.onCapturedMouseUpHandler, true
6828 );
6829 }
6830 }
6831 return false;
6832 };
6833
6834 /**
6835 * Handle captured mouse up events.
6836 *
6837 * @param {Event} e Mouse up event
6838 */
6839 OO.ui.ToolGroup.prototype.onCapturedMouseUp = function ( e ) {
6840 this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseUpHandler, true );
6841 // onPointerUp may be called a second time, depending on where the mouse is when the button is
6842 // released, but since `this.pressed` will no longer be true, the second call will be ignored.
6843 this.onPointerUp( e );
6844 };
6845
6846 /**
6847 * Handle mouse up events.
6848 *
6849 * @param {jQuery.Event} e Mouse up event
6850 */
6851 OO.ui.ToolGroup.prototype.onPointerUp = function ( e ) {
6852 var tool = this.getTargetTool( e );
6853
6854 if ( !this.isDisabled() && e.which === 1 && this.pressed && this.pressed === tool ) {
6855 this.pressed.onSelect();
6856 }
6857
6858 this.pressed = null;
6859 return false;
6860 };
6861
6862 /**
6863 * Handle mouse over events.
6864 *
6865 * @param {jQuery.Event} e Mouse over event
6866 */
6867 OO.ui.ToolGroup.prototype.onMouseOver = function ( e ) {
6868 var tool = this.getTargetTool( e );
6869
6870 if ( this.pressed && this.pressed === tool ) {
6871 this.pressed.setActive( true );
6872 }
6873 };
6874
6875 /**
6876 * Handle mouse out events.
6877 *
6878 * @param {jQuery.Event} e Mouse out event
6879 */
6880 OO.ui.ToolGroup.prototype.onMouseOut = function ( e ) {
6881 var tool = this.getTargetTool( e );
6882
6883 if ( this.pressed && this.pressed === tool ) {
6884 this.pressed.setActive( false );
6885 }
6886 };
6887
6888 /**
6889 * Get the closest tool to a jQuery.Event.
6890 *
6891 * Only tool links are considered, which prevents other elements in the tool such as popups from
6892 * triggering tool group interactions.
6893 *
6894 * @private
6895 * @param {jQuery.Event} e
6896 * @return {OO.ui.Tool|null} Tool, `null` if none was found
6897 */
6898 OO.ui.ToolGroup.prototype.getTargetTool = function ( e ) {
6899 var tool,
6900 $item = $( e.target ).closest( '.oo-ui-tool-link' );
6901
6902 if ( $item.length ) {
6903 tool = $item.parent().data( 'oo-ui-tool' );
6904 }
6905
6906 return tool && !tool.isDisabled() ? tool : null;
6907 };
6908
6909 /**
6910 * Handle tool registry register events.
6911 *
6912 * If a tool is registered after the group is created, we must repopulate the list to account for:
6913 *
6914 * - a tool being added that may be included
6915 * - a tool already included being overridden
6916 *
6917 * @param {string} name Symbolic name of tool
6918 */
6919 OO.ui.ToolGroup.prototype.onToolFactoryRegister = function () {
6920 this.populate();
6921 };
6922
6923 /**
6924 * Get the toolbar this group is in.
6925 *
6926 * @return {OO.ui.Toolbar} Toolbar of group
6927 */
6928 OO.ui.ToolGroup.prototype.getToolbar = function () {
6929 return this.toolbar;
6930 };
6931
6932 /**
6933 * Add and remove tools based on configuration.
6934 */
6935 OO.ui.ToolGroup.prototype.populate = function () {
6936 var i, len, name, tool,
6937 toolFactory = this.toolbar.getToolFactory(),
6938 names = {},
6939 add = [],
6940 remove = [],
6941 list = this.toolbar.getToolFactory().getTools(
6942 this.include, this.exclude, this.promote, this.demote
6943 );
6944
6945 // Build a list of needed tools
6946 for ( i = 0, len = list.length; i < len; i++ ) {
6947 name = list[ i ];
6948 if (
6949 // Tool exists
6950 toolFactory.lookup( name ) &&
6951 // Tool is available or is already in this group
6952 ( this.toolbar.isToolAvailable( name ) || this.tools[ name ] )
6953 ) {
6954 // Hack to prevent infinite recursion via ToolGroupTool. We need to reserve the tool before
6955 // creating it, but we can't call reserveTool() yet because we haven't created the tool.
6956 this.toolbar.tools[ name ] = true;
6957 tool = this.tools[ name ];
6958 if ( !tool ) {
6959 // Auto-initialize tools on first use
6960 this.tools[ name ] = tool = toolFactory.create( name, this );
6961 tool.updateTitle();
6962 }
6963 this.toolbar.reserveTool( tool );
6964 add.push( tool );
6965 names[ name ] = true;
6966 }
6967 }
6968 // Remove tools that are no longer needed
6969 for ( name in this.tools ) {
6970 if ( !names[ name ] ) {
6971 this.tools[ name ].destroy();
6972 this.toolbar.releaseTool( this.tools[ name ] );
6973 remove.push( this.tools[ name ] );
6974 delete this.tools[ name ];
6975 }
6976 }
6977 if ( remove.length ) {
6978 this.removeItems( remove );
6979 }
6980 // Update emptiness state
6981 if ( add.length ) {
6982 this.$element.removeClass( 'oo-ui-toolGroup-empty' );
6983 } else {
6984 this.$element.addClass( 'oo-ui-toolGroup-empty' );
6985 }
6986 // Re-add tools (moving existing ones to new locations)
6987 this.addItems( add );
6988 // Disabled state may depend on items
6989 this.updateDisabled();
6990 };
6991
6992 /**
6993 * Destroy tool group.
6994 */
6995 OO.ui.ToolGroup.prototype.destroy = function () {
6996 var name;
6997
6998 this.clearItems();
6999 this.toolbar.getToolFactory().disconnect( this );
7000 for ( name in this.tools ) {
7001 this.toolbar.releaseTool( this.tools[ name ] );
7002 this.tools[ name ].disconnect( this ).destroy();
7003 delete this.tools[ name ];
7004 }
7005 this.$element.remove();
7006 };
7007
7008 /**
7009 * MessageDialogs display a confirmation or alert message. By default, the rendered dialog box
7010 * consists of a header that contains the dialog title, a body with the message, and a footer that
7011 * contains any {@link OO.ui.ActionWidget action widgets}. The MessageDialog class is the only type
7012 * of {@link OO.ui.Dialog dialog} that is usually instantiated directly.
7013 *
7014 * There are two basic types of message dialogs, confirmation and alert:
7015 *
7016 * - **confirmation**: the dialog title describes what a progressive action will do and the message provides
7017 * more details about the consequences.
7018 * - **alert**: the dialog title describes which event occurred and the message provides more information
7019 * about why the event occurred.
7020 *
7021 * The MessageDialog class specifies two actions: ‘accept’, the primary
7022 * action (e.g., ‘ok’) and ‘reject,’ the safe action (e.g., ‘cancel’). Both will close the window,
7023 * passing along the selected action.
7024 *
7025 * For more information and examples, please see the [OOjs UI documentation on MediaWiki][1].
7026 *
7027 * @example
7028 * // Example: Creating and opening a message dialog window.
7029 * var messageDialog = new OO.ui.MessageDialog();
7030 *
7031 * // Create and append a window manager.
7032 * var windowManager = new OO.ui.WindowManager();
7033 * $( 'body' ).append( windowManager.$element );
7034 * windowManager.addWindows( [ messageDialog ] );
7035 * // Open the window.
7036 * windowManager.openWindow( messageDialog, {
7037 * title: 'Basic message dialog',
7038 * message: 'This is the message'
7039 * } );
7040 *
7041 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Message_Dialogs
7042 *
7043 * @class
7044 * @extends OO.ui.Dialog
7045 *
7046 * @constructor
7047 * @param {Object} [config] Configuration options
7048 */
7049 OO.ui.MessageDialog = function OoUiMessageDialog( config ) {
7050 // Parent constructor
7051 OO.ui.MessageDialog.super.call( this, config );
7052
7053 // Properties
7054 this.verticalActionLayout = null;
7055
7056 // Initialization
7057 this.$element.addClass( 'oo-ui-messageDialog' );
7058 };
7059
7060 /* Inheritance */
7061
7062 OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog );
7063
7064 /* Static Properties */
7065
7066 OO.ui.MessageDialog.static.name = 'message';
7067
7068 OO.ui.MessageDialog.static.size = 'small';
7069
7070 OO.ui.MessageDialog.static.verbose = false;
7071
7072 /**
7073 * Dialog title.
7074 *
7075 * The title of a confirmation dialog describes what a progressive action will do. The
7076 * title of an alert dialog describes which event occurred.
7077 *
7078 * @static
7079 * @inheritable
7080 * @property {jQuery|string|Function|null}
7081 */
7082 OO.ui.MessageDialog.static.title = null;
7083
7084 /**
7085 * The message displayed in the dialog body.
7086 *
7087 * A confirmation message describes the consequences of a progressive action. An alert
7088 * message describes why an event occurred.
7089 *
7090 * @static
7091 * @inheritable
7092 * @property {jQuery|string|Function|null}
7093 */
7094 OO.ui.MessageDialog.static.message = null;
7095
7096 OO.ui.MessageDialog.static.actions = [
7097 { action: 'accept', label: OO.ui.deferMsg( 'ooui-dialog-message-accept' ), flags: 'primary' },
7098 { action: 'reject', label: OO.ui.deferMsg( 'ooui-dialog-message-reject' ), flags: 'safe' }
7099 ];
7100
7101 /* Methods */
7102
7103 /**
7104 * @inheritdoc
7105 */
7106 OO.ui.MessageDialog.prototype.setManager = function ( manager ) {
7107 OO.ui.MessageDialog.super.prototype.setManager.call( this, manager );
7108
7109 // Events
7110 this.manager.connect( this, {
7111 resize: 'onResize'
7112 } );
7113
7114 return this;
7115 };
7116
7117 /**
7118 * @inheritdoc
7119 */
7120 OO.ui.MessageDialog.prototype.onActionResize = function ( action ) {
7121 this.fitActions();
7122 return OO.ui.MessageDialog.super.prototype.onActionResize.call( this, action );
7123 };
7124
7125 /**
7126 * Handle window resized events.
7127 *
7128 * @private
7129 */
7130 OO.ui.MessageDialog.prototype.onResize = function () {
7131 var dialog = this;
7132 dialog.fitActions();
7133 // Wait for CSS transition to finish and do it again :(
7134 setTimeout( function () {
7135 dialog.fitActions();
7136 }, 300 );
7137 };
7138
7139 /**
7140 * Toggle action layout between vertical and horizontal.
7141 *
7142 *
7143 * @private
7144 * @param {boolean} [value] Layout actions vertically, omit to toggle
7145 * @chainable
7146 */
7147 OO.ui.MessageDialog.prototype.toggleVerticalActionLayout = function ( value ) {
7148 value = value === undefined ? !this.verticalActionLayout : !!value;
7149
7150 if ( value !== this.verticalActionLayout ) {
7151 this.verticalActionLayout = value;
7152 this.$actions
7153 .toggleClass( 'oo-ui-messageDialog-actions-vertical', value )
7154 .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value );
7155 }
7156
7157 return this;
7158 };
7159
7160 /**
7161 * @inheritdoc
7162 */
7163 OO.ui.MessageDialog.prototype.getActionProcess = function ( action ) {
7164 if ( action ) {
7165 return new OO.ui.Process( function () {
7166 this.close( { action: action } );
7167 }, this );
7168 }
7169 return OO.ui.MessageDialog.super.prototype.getActionProcess.call( this, action );
7170 };
7171
7172 /**
7173 * @inheritdoc
7174 *
7175 * @param {Object} [data] Dialog opening data
7176 * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
7177 * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
7178 * @param {boolean} [data.verbose] Message is verbose and should be styled as a long message
7179 * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each
7180 * action item
7181 */
7182 OO.ui.MessageDialog.prototype.getSetupProcess = function ( data ) {
7183 data = data || {};
7184
7185 // Parent method
7186 return OO.ui.MessageDialog.super.prototype.getSetupProcess.call( this, data )
7187 .next( function () {
7188 this.title.setLabel(
7189 data.title !== undefined ? data.title : this.constructor.static.title
7190 );
7191 this.message.setLabel(
7192 data.message !== undefined ? data.message : this.constructor.static.message
7193 );
7194 this.message.$element.toggleClass(
7195 'oo-ui-messageDialog-message-verbose',
7196 data.verbose !== undefined ? data.verbose : this.constructor.static.verbose
7197 );
7198 }, this );
7199 };
7200
7201 /**
7202 * @inheritdoc
7203 */
7204 OO.ui.MessageDialog.prototype.getBodyHeight = function () {
7205 var bodyHeight, oldOverflow,
7206 $scrollable = this.container.$element;
7207
7208 oldOverflow = $scrollable[ 0 ].style.overflow;
7209 $scrollable[ 0 ].style.overflow = 'hidden';
7210
7211 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
7212
7213 bodyHeight = this.text.$element.outerHeight( true );
7214 $scrollable[ 0 ].style.overflow = oldOverflow;
7215
7216 return bodyHeight;
7217 };
7218
7219 /**
7220 * @inheritdoc
7221 */
7222 OO.ui.MessageDialog.prototype.setDimensions = function ( dim ) {
7223 var $scrollable = this.container.$element;
7224 OO.ui.MessageDialog.super.prototype.setDimensions.call( this, dim );
7225
7226 // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
7227 // Need to do it after transition completes (250ms), add 50ms just in case.
7228 setTimeout( function () {
7229 var oldOverflow = $scrollable[ 0 ].style.overflow;
7230 $scrollable[ 0 ].style.overflow = 'hidden';
7231
7232 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
7233
7234 $scrollable[ 0 ].style.overflow = oldOverflow;
7235 }, 300 );
7236
7237 return this;
7238 };
7239
7240 /**
7241 * @inheritdoc
7242 */
7243 OO.ui.MessageDialog.prototype.initialize = function () {
7244 // Parent method
7245 OO.ui.MessageDialog.super.prototype.initialize.call( this );
7246
7247 // Properties
7248 this.$actions = $( '<div>' );
7249 this.container = new OO.ui.PanelLayout( {
7250 scrollable: true, classes: [ 'oo-ui-messageDialog-container' ]
7251 } );
7252 this.text = new OO.ui.PanelLayout( {
7253 padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ]
7254 } );
7255 this.message = new OO.ui.LabelWidget( {
7256 classes: [ 'oo-ui-messageDialog-message' ]
7257 } );
7258
7259 // Initialization
7260 this.title.$element.addClass( 'oo-ui-messageDialog-title' );
7261 this.$content.addClass( 'oo-ui-messageDialog-content' );
7262 this.container.$element.append( this.text.$element );
7263 this.text.$element.append( this.title.$element, this.message.$element );
7264 this.$body.append( this.container.$element );
7265 this.$actions.addClass( 'oo-ui-messageDialog-actions' );
7266 this.$foot.append( this.$actions );
7267 };
7268
7269 /**
7270 * @inheritdoc
7271 */
7272 OO.ui.MessageDialog.prototype.attachActions = function () {
7273 var i, len, other, special, others;
7274
7275 // Parent method
7276 OO.ui.MessageDialog.super.prototype.attachActions.call( this );
7277
7278 special = this.actions.getSpecial();
7279 others = this.actions.getOthers();
7280 if ( special.safe ) {
7281 this.$actions.append( special.safe.$element );
7282 special.safe.toggleFramed( false );
7283 }
7284 if ( others.length ) {
7285 for ( i = 0, len = others.length; i < len; i++ ) {
7286 other = others[ i ];
7287 this.$actions.append( other.$element );
7288 other.toggleFramed( false );
7289 }
7290 }
7291 if ( special.primary ) {
7292 this.$actions.append( special.primary.$element );
7293 special.primary.toggleFramed( false );
7294 }
7295
7296 if ( !this.isOpening() ) {
7297 // If the dialog is currently opening, this will be called automatically soon.
7298 // This also calls #fitActions.
7299 this.updateSize();
7300 }
7301 };
7302
7303 /**
7304 * Fit action actions into columns or rows.
7305 *
7306 * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
7307 *
7308 * @private
7309 */
7310 OO.ui.MessageDialog.prototype.fitActions = function () {
7311 var i, len, action,
7312 previous = this.verticalActionLayout,
7313 actions = this.actions.get();
7314
7315 // Detect clipping
7316 this.toggleVerticalActionLayout( false );
7317 for ( i = 0, len = actions.length; i < len; i++ ) {
7318 action = actions[ i ];
7319 if ( action.$element.innerWidth() < action.$label.outerWidth( true ) ) {
7320 this.toggleVerticalActionLayout( true );
7321 break;
7322 }
7323 }
7324
7325 // Move the body out of the way of the foot
7326 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
7327
7328 if ( this.verticalActionLayout !== previous ) {
7329 // We changed the layout, window height might need to be updated.
7330 this.updateSize();
7331 }
7332 };
7333
7334 /**
7335 * ProcessDialog windows encapsulate a {@link OO.ui.Process process} and all of the code necessary
7336 * to complete it. If the process terminates with an error, a customizable {@link OO.ui.Error error
7337 * interface} alerts users to the trouble, permitting the user to dismiss the error and try again when
7338 * relevant. The ProcessDialog class is always extended and customized with the actions and content
7339 * required for each process.
7340 *
7341 * The process dialog box consists of a header that visually represents the ‘working’ state of long
7342 * processes with an animation. The header contains the dialog title as well as
7343 * two {@link OO.ui.ActionWidget action widgets}: a ‘safe’ action on the left (e.g., ‘Cancel’) and
7344 * a ‘primary’ action on the right (e.g., ‘Done’).
7345 *
7346 * Like other windows, the process dialog is managed by a {@link OO.ui.WindowManager window manager}.
7347 * Please see the [OOjs UI documentation on MediaWiki][1] for more information and examples.
7348 *
7349 * @example
7350 * // Example: Creating and opening a process dialog window.
7351 * function ProcessDialog( config ) {
7352 * ProcessDialog.super.call( this, config );
7353 * }
7354 * OO.inheritClass( ProcessDialog, OO.ui.ProcessDialog );
7355 *
7356 * ProcessDialog.static.title = 'Process dialog';
7357 * ProcessDialog.static.actions = [
7358 * { action: 'save', label: 'Done', flags: 'primary' },
7359 * { label: 'Cancel', flags: 'safe' }
7360 * ];
7361 *
7362 * ProcessDialog.prototype.initialize = function () {
7363 * ProcessDialog.super.prototype.initialize.apply( this, arguments );
7364 * this.content = new OO.ui.PanelLayout( { $: this.$, padded: true, expanded: false } );
7365 * this.content.$element.append( '<p>This is a process dialog window. The header contains the title and two buttons: \'Cancel\' (a safe action) on the left and \'Done\' (a primary action) on the right. </p>' );
7366 * this.$body.append( this.content.$element );
7367 * };
7368 * ProcessDialog.prototype.getActionProcess = function ( action ) {
7369 * var dialog = this;
7370 * if ( action ) {
7371 * return new OO.ui.Process( function () {
7372 * dialog.close( { action: action } );
7373 * } );
7374 * }
7375 * return ProcessDialog.super.prototype.getActionProcess.call( this, action );
7376 * };
7377 *
7378 * var windowManager = new OO.ui.WindowManager();
7379 * $( 'body' ).append( windowManager.$element );
7380 *
7381 * var processDialog = new ProcessDialog();
7382 * windowManager.addWindows( [ processDialog ] );
7383 * windowManager.openWindow( processDialog );
7384 *
7385 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
7386 *
7387 * @abstract
7388 * @class
7389 * @extends OO.ui.Dialog
7390 *
7391 * @constructor
7392 * @param {Object} [config] Configuration options
7393 */
7394 OO.ui.ProcessDialog = function OoUiProcessDialog( config ) {
7395 // Parent constructor
7396 OO.ui.ProcessDialog.super.call( this, config );
7397
7398 // Initialization
7399 this.$element.addClass( 'oo-ui-processDialog' );
7400 };
7401
7402 /* Setup */
7403
7404 OO.inheritClass( OO.ui.ProcessDialog, OO.ui.Dialog );
7405
7406 /* Methods */
7407
7408 /**
7409 * Handle dismiss button click events.
7410 *
7411 * Hides errors.
7412 *
7413 * @private
7414 */
7415 OO.ui.ProcessDialog.prototype.onDismissErrorButtonClick = function () {
7416 this.hideErrors();
7417 };
7418
7419 /**
7420 * Handle retry button click events.
7421 *
7422 * Hides errors and then tries again.
7423 *
7424 * @private
7425 */
7426 OO.ui.ProcessDialog.prototype.onRetryButtonClick = function () {
7427 this.hideErrors();
7428 this.executeAction( this.currentAction );
7429 };
7430
7431 /**
7432 * @inheritdoc
7433 */
7434 OO.ui.ProcessDialog.prototype.onActionResize = function ( action ) {
7435 if ( this.actions.isSpecial( action ) ) {
7436 this.fitLabel();
7437 }
7438 return OO.ui.ProcessDialog.super.prototype.onActionResize.call( this, action );
7439 };
7440
7441 /**
7442 * @inheritdoc
7443 */
7444 OO.ui.ProcessDialog.prototype.initialize = function () {
7445 // Parent method
7446 OO.ui.ProcessDialog.super.prototype.initialize.call( this );
7447
7448 // Properties
7449 this.$navigation = $( '<div>' );
7450 this.$location = $( '<div>' );
7451 this.$safeActions = $( '<div>' );
7452 this.$primaryActions = $( '<div>' );
7453 this.$otherActions = $( '<div>' );
7454 this.dismissButton = new OO.ui.ButtonWidget( {
7455 label: OO.ui.msg( 'ooui-dialog-process-dismiss' )
7456 } );
7457 this.retryButton = new OO.ui.ButtonWidget();
7458 this.$errors = $( '<div>' );
7459 this.$errorsTitle = $( '<div>' );
7460
7461 // Events
7462 this.dismissButton.connect( this, { click: 'onDismissErrorButtonClick' } );
7463 this.retryButton.connect( this, { click: 'onRetryButtonClick' } );
7464
7465 // Initialization
7466 this.title.$element.addClass( 'oo-ui-processDialog-title' );
7467 this.$location
7468 .append( this.title.$element )
7469 .addClass( 'oo-ui-processDialog-location' );
7470 this.$safeActions.addClass( 'oo-ui-processDialog-actions-safe' );
7471 this.$primaryActions.addClass( 'oo-ui-processDialog-actions-primary' );
7472 this.$otherActions.addClass( 'oo-ui-processDialog-actions-other' );
7473 this.$errorsTitle
7474 .addClass( 'oo-ui-processDialog-errors-title' )
7475 .text( OO.ui.msg( 'ooui-dialog-process-error' ) );
7476 this.$errors
7477 .addClass( 'oo-ui-processDialog-errors oo-ui-element-hidden' )
7478 .append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element );
7479 this.$content
7480 .addClass( 'oo-ui-processDialog-content' )
7481 .append( this.$errors );
7482 this.$navigation
7483 .addClass( 'oo-ui-processDialog-navigation' )
7484 .append( this.$safeActions, this.$location, this.$primaryActions );
7485 this.$head.append( this.$navigation );
7486 this.$foot.append( this.$otherActions );
7487 };
7488
7489 /**
7490 * @inheritdoc
7491 */
7492 OO.ui.ProcessDialog.prototype.attachActions = function () {
7493 var i, len, other, special, others;
7494
7495 // Parent method
7496 OO.ui.ProcessDialog.super.prototype.attachActions.call( this );
7497
7498 special = this.actions.getSpecial();
7499 others = this.actions.getOthers();
7500 if ( special.primary ) {
7501 this.$primaryActions.append( special.primary.$element );
7502 special.primary.toggleFramed( true );
7503 }
7504 for ( i = 0, len = others.length; i < len; i++ ) {
7505 other = others[ i ];
7506 this.$otherActions.append( other.$element );
7507 other.toggleFramed( true );
7508 }
7509 if ( special.safe ) {
7510 this.$safeActions.append( special.safe.$element );
7511 special.safe.toggleFramed( true );
7512 }
7513
7514 this.fitLabel();
7515 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
7516 };
7517
7518 /**
7519 * @inheritdoc
7520 */
7521 OO.ui.ProcessDialog.prototype.executeAction = function ( action ) {
7522 OO.ui.ProcessDialog.super.prototype.executeAction.call( this, action )
7523 .fail( this.showErrors.bind( this ) );
7524 };
7525
7526 /**
7527 * Fit label between actions.
7528 *
7529 * @private
7530 * @chainable
7531 */
7532 OO.ui.ProcessDialog.prototype.fitLabel = function () {
7533 var width = Math.max(
7534 this.$safeActions.is( ':visible' ) ? this.$safeActions.width() : 0,
7535 this.$primaryActions.is( ':visible' ) ? this.$primaryActions.width() : 0
7536 );
7537 this.$location.css( { paddingLeft: width, paddingRight: width } );
7538
7539 return this;
7540 };
7541
7542 /**
7543 * Handle errors that occurred during accept or reject processes.
7544 *
7545 * @private
7546 * @param {OO.ui.Error[]} errors Errors to be handled
7547 */
7548 OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
7549 var i, len, $item, actions,
7550 items = [],
7551 abilities = {},
7552 recoverable = true,
7553 warning = false;
7554
7555 for ( i = 0, len = errors.length; i < len; i++ ) {
7556 if ( !errors[ i ].isRecoverable() ) {
7557 recoverable = false;
7558 }
7559 if ( errors[ i ].isWarning() ) {
7560 warning = true;
7561 }
7562 $item = $( '<div>' )
7563 .addClass( 'oo-ui-processDialog-error' )
7564 .append( errors[ i ].getMessage() );
7565 items.push( $item[ 0 ] );
7566 }
7567 this.$errorItems = $( items );
7568 if ( recoverable ) {
7569 abilities[this.currentAction] = true;
7570 // Copy the flags from the first matching action
7571 actions = this.actions.get( { actions: this.currentAction } );
7572 if ( actions.length ) {
7573 this.retryButton.clearFlags().setFlags( actions[0].getFlags() );
7574 }
7575 } else {
7576 abilities[this.currentAction] = false;
7577 this.actions.setAbilities( abilities );
7578 }
7579 if ( warning ) {
7580 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-continue' ) );
7581 } else {
7582 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-retry' ) );
7583 }
7584 this.retryButton.toggle( recoverable );
7585 this.$errorsTitle.after( this.$errorItems );
7586 this.$errors.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 );
7587 };
7588
7589 /**
7590 * Hide errors.
7591 *
7592 * @private
7593 */
7594 OO.ui.ProcessDialog.prototype.hideErrors = function () {
7595 this.$errors.addClass( 'oo-ui-element-hidden' );
7596 if ( this.$errorItems ) {
7597 this.$errorItems.remove();
7598 this.$errorItems = null;
7599 }
7600 };
7601
7602 /**
7603 * @inheritdoc
7604 */
7605 OO.ui.ProcessDialog.prototype.getTeardownProcess = function ( data ) {
7606 // Parent method
7607 return OO.ui.ProcessDialog.super.prototype.getTeardownProcess.call( this, data )
7608 .first( function () {
7609 // Make sure to hide errors
7610 this.hideErrors();
7611 }, this );
7612 };
7613
7614 /**
7615 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
7616 * which is a widget that is specified by reference before any optional configuration settings.
7617 *
7618 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
7619 *
7620 * - **left**: The label is placed before the field-widget and aligned with the left margin.
7621 * A left-alignment is used for forms with many fields.
7622 * - **right**: The label is placed before the field-widget and aligned to the right margin.
7623 * A right-alignment is used for long but familiar forms which users tab through,
7624 * verifying the current field with a quick glance at the label.
7625 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
7626 * that users fill out from top to bottom.
7627 * - **inline**: The label is placed after the field-widget and aligned to the left.
7628 * An inline-alignment is best used with checkboxes or radio buttons.
7629 *
7630 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
7631 * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
7632 *
7633 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
7634 * @class
7635 * @extends OO.ui.Layout
7636 * @mixins OO.ui.LabelElement
7637 *
7638 * @constructor
7639 * @param {OO.ui.Widget} fieldWidget Field widget
7640 * @param {Object} [config] Configuration options
7641 * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline'
7642 * @cfg {string} [help] Explanatory text shown as a '?' icon.
7643 */
7644 OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
7645 // Allow passing positional parameters inside the config object
7646 if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
7647 config = fieldWidget;
7648 fieldWidget = config.fieldWidget;
7649 }
7650
7651 var hasInputWidget = fieldWidget instanceof OO.ui.InputWidget;
7652
7653 // Configuration initialization
7654 config = $.extend( { align: 'left' }, config );
7655
7656 // Parent constructor
7657 OO.ui.FieldLayout.super.call( this, config );
7658
7659 // Mixin constructors
7660 OO.ui.LabelElement.call( this, config );
7661
7662 // Properties
7663 this.fieldWidget = fieldWidget;
7664 this.$field = $( '<div>' );
7665 this.$body = $( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
7666 this.align = null;
7667 if ( config.help ) {
7668 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
7669 classes: [ 'oo-ui-fieldLayout-help' ],
7670 framed: false,
7671 icon: 'info'
7672 } );
7673
7674 this.popupButtonWidget.getPopup().$body.append(
7675 $( '<div>' )
7676 .text( config.help )
7677 .addClass( 'oo-ui-fieldLayout-help-content' )
7678 );
7679 this.$help = this.popupButtonWidget.$element;
7680 } else {
7681 this.$help = $( [] );
7682 }
7683
7684 // Events
7685 if ( hasInputWidget ) {
7686 this.$label.on( 'click', this.onLabelClick.bind( this ) );
7687 }
7688 this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
7689
7690 // Initialization
7691 this.$element
7692 .addClass( 'oo-ui-fieldLayout' )
7693 .append( this.$help, this.$body );
7694 this.$body.addClass( 'oo-ui-fieldLayout-body' );
7695 this.$field
7696 .addClass( 'oo-ui-fieldLayout-field' )
7697 .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget.isDisabled() )
7698 .append( this.fieldWidget.$element );
7699
7700 this.setAlignment( config.align );
7701 };
7702
7703 /* Setup */
7704
7705 OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
7706 OO.mixinClass( OO.ui.FieldLayout, OO.ui.LabelElement );
7707
7708 /* Methods */
7709
7710 /**
7711 * Handle field disable events.
7712 *
7713 * @param {boolean} value Field is disabled
7714 */
7715 OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
7716 this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
7717 };
7718
7719 /**
7720 * Handle label mouse click events.
7721 *
7722 * @param {jQuery.Event} e Mouse click event
7723 */
7724 OO.ui.FieldLayout.prototype.onLabelClick = function () {
7725 this.fieldWidget.simulateLabelClick();
7726 return false;
7727 };
7728
7729 /**
7730 * Get the field.
7731 *
7732 * @return {OO.ui.Widget} Field widget
7733 */
7734 OO.ui.FieldLayout.prototype.getField = function () {
7735 return this.fieldWidget;
7736 };
7737
7738 /**
7739 * Set the field alignment mode.
7740 *
7741 * @private
7742 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
7743 * @chainable
7744 */
7745 OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
7746 if ( value !== this.align ) {
7747 // Default to 'left'
7748 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) {
7749 value = 'left';
7750 }
7751 // Reorder elements
7752 if ( value === 'inline' ) {
7753 this.$body.append( this.$field, this.$label );
7754 } else {
7755 this.$body.append( this.$label, this.$field );
7756 }
7757 // Set classes. The following classes can be used here:
7758 // * oo-ui-fieldLayout-align-left
7759 // * oo-ui-fieldLayout-align-right
7760 // * oo-ui-fieldLayout-align-top
7761 // * oo-ui-fieldLayout-align-inline
7762 if ( this.align ) {
7763 this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
7764 }
7765 this.$element.addClass( 'oo-ui-fieldLayout-align-' + value );
7766 this.align = value;
7767 }
7768
7769 return this;
7770 };
7771
7772 /**
7773 * Layout made of a field, a button, and an optional label.
7774 *
7775 * @class
7776 * @extends OO.ui.FieldLayout
7777 *
7778 * @constructor
7779 * @param {OO.ui.Widget} fieldWidget Field widget
7780 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
7781 * @param {Object} [config] Configuration options
7782 * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline'
7783 * @cfg {string} [help] Explanatory text shown as a '?' icon.
7784 */
7785 OO.ui.ActionFieldLayout = function OoUiActionFieldLayout( fieldWidget, buttonWidget, config ) {
7786 // Allow passing positional parameters inside the config object
7787 if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
7788 config = fieldWidget;
7789 fieldWidget = config.fieldWidget;
7790 buttonWidget = config.buttonWidget;
7791 }
7792
7793 // Configuration initialization
7794 config = $.extend( { align: 'left' }, config );
7795
7796 // Parent constructor
7797 OO.ui.ActionFieldLayout.super.call( this, fieldWidget, config );
7798
7799 // Properties
7800 this.fieldWidget = fieldWidget;
7801 this.buttonWidget = buttonWidget;
7802 this.$button = $( '<div>' )
7803 .addClass( 'oo-ui-actionFieldLayout-button' )
7804 .append( this.buttonWidget.$element );
7805 this.$input = $( '<div>' )
7806 .addClass( 'oo-ui-actionFieldLayout-input' )
7807 .append( this.fieldWidget.$element );
7808 this.$field
7809 .addClass( 'oo-ui-actionFieldLayout' )
7810 .append( this.$input, this.$button );
7811 };
7812
7813 /* Setup */
7814
7815 OO.inheritClass( OO.ui.ActionFieldLayout, OO.ui.FieldLayout );
7816
7817 /**
7818 * Layout made of a fieldset and optional legend.
7819 *
7820 * Just add OO.ui.FieldLayout items.
7821 *
7822 * @class
7823 * @extends OO.ui.Layout
7824 * @mixins OO.ui.IconElement
7825 * @mixins OO.ui.LabelElement
7826 * @mixins OO.ui.GroupElement
7827 *
7828 * @constructor
7829 * @param {Object} [config] Configuration options
7830 * @cfg {OO.ui.FieldLayout[]} [items] Items to add
7831 */
7832 OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
7833 // Configuration initialization
7834 config = config || {};
7835
7836 // Parent constructor
7837 OO.ui.FieldsetLayout.super.call( this, config );
7838
7839 // Mixin constructors
7840 OO.ui.IconElement.call( this, config );
7841 OO.ui.LabelElement.call( this, config );
7842 OO.ui.GroupElement.call( this, config );
7843
7844 if ( config.help ) {
7845 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
7846 classes: [ 'oo-ui-fieldsetLayout-help' ],
7847 framed: false,
7848 icon: 'info'
7849 } );
7850
7851 this.popupButtonWidget.getPopup().$body.append(
7852 $( '<div>' )
7853 .text( config.help )
7854 .addClass( 'oo-ui-fieldsetLayout-help-content' )
7855 );
7856 this.$help = this.popupButtonWidget.$element;
7857 } else {
7858 this.$help = $( [] );
7859 }
7860
7861 // Initialization
7862 this.$element
7863 .addClass( 'oo-ui-fieldsetLayout' )
7864 .prepend( this.$help, this.$icon, this.$label, this.$group );
7865 if ( Array.isArray( config.items ) ) {
7866 this.addItems( config.items );
7867 }
7868 };
7869
7870 /* Setup */
7871
7872 OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
7873 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.IconElement );
7874 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.LabelElement );
7875 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.GroupElement );
7876
7877 /**
7878 * Layout with an HTML form.
7879 *
7880 * @class
7881 * @extends OO.ui.Layout
7882 * @mixins OO.ui.GroupElement
7883 *
7884 * @constructor
7885 * @param {Object} [config] Configuration options
7886 * @cfg {string} [method] HTML form `method` attribute
7887 * @cfg {string} [action] HTML form `action` attribute
7888 * @cfg {string} [enctype] HTML form `enctype` attribute
7889 * @cfg {OO.ui.FieldsetLayout[]} [items] Items to add
7890 */
7891 OO.ui.FormLayout = function OoUiFormLayout( config ) {
7892 // Configuration initialization
7893 config = config || {};
7894
7895 // Parent constructor
7896 OO.ui.FormLayout.super.call( this, config );
7897
7898 // Mixin constructors
7899 OO.ui.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
7900
7901 // Events
7902 this.$element.on( 'submit', this.onFormSubmit.bind( this ) );
7903
7904 // Initialization
7905 this.$element
7906 .addClass( 'oo-ui-formLayout' )
7907 .attr( {
7908 method: config.method,
7909 action: config.action,
7910 enctype: config.enctype
7911 } );
7912 if ( Array.isArray( config.items ) ) {
7913 this.addItems( config.items );
7914 }
7915 };
7916
7917 /* Setup */
7918
7919 OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout );
7920 OO.mixinClass( OO.ui.FormLayout, OO.ui.GroupElement );
7921
7922 /* Events */
7923
7924 /**
7925 * @event submit
7926 */
7927
7928 /* Static Properties */
7929
7930 OO.ui.FormLayout.static.tagName = 'form';
7931
7932 /* Methods */
7933
7934 /**
7935 * Handle form submit events.
7936 *
7937 * @param {jQuery.Event} e Submit event
7938 * @fires submit
7939 */
7940 OO.ui.FormLayout.prototype.onFormSubmit = function () {
7941 this.emit( 'submit' );
7942 return false;
7943 };
7944
7945 /**
7946 * Layout with a content and menu area.
7947 *
7948 * The menu area can be positioned at the top, after, bottom or before. The content area will fill
7949 * all remaining space.
7950 *
7951 * @class
7952 * @extends OO.ui.Layout
7953 *
7954 * @constructor
7955 * @param {Object} [config] Configuration options
7956 * @cfg {number|string} [menuSize='18em'] Size of menu in pixels or any CSS unit
7957 * @cfg {boolean} [showMenu=true] Show menu
7958 * @cfg {string} [position='before'] Position of menu, either `top`, `after`, `bottom` or `before`
7959 * @cfg {boolean} [collapse] Collapse the menu out of view
7960 */
7961 OO.ui.MenuLayout = function OoUiMenuLayout( config ) {
7962 var positions = this.constructor.static.menuPositions;
7963
7964 // Configuration initialization
7965 config = config || {};
7966
7967 // Parent constructor
7968 OO.ui.MenuLayout.super.call( this, config );
7969
7970 // Properties
7971 this.showMenu = config.showMenu !== false;
7972 this.menuSize = config.menuSize || '18em';
7973 this.menuPosition = positions[ config.menuPosition ] || positions.before;
7974
7975 /**
7976 * Menu DOM node
7977 *
7978 * @property {jQuery}
7979 */
7980 this.$menu = $( '<div>' );
7981 /**
7982 * Content DOM node
7983 *
7984 * @property {jQuery}
7985 */
7986 this.$content = $( '<div>' );
7987
7988 // Initialization
7989 this.toggleMenu( this.showMenu );
7990 this.updateSizes();
7991 this.$menu
7992 .addClass( 'oo-ui-menuLayout-menu' )
7993 .css( this.menuPosition.sizeProperty, this.menuSize );
7994 this.$content.addClass( 'oo-ui-menuLayout-content' );
7995 this.$element
7996 .addClass( 'oo-ui-menuLayout ' + this.menuPosition.className )
7997 .append( this.$content, this.$menu );
7998 };
7999
8000 /* Setup */
8001
8002 OO.inheritClass( OO.ui.MenuLayout, OO.ui.Layout );
8003
8004 /* Static Properties */
8005
8006 OO.ui.MenuLayout.static.menuPositions = {
8007 top: {
8008 sizeProperty: 'height',
8009 className: 'oo-ui-menuLayout-top'
8010 },
8011 after: {
8012 sizeProperty: 'width',
8013 className: 'oo-ui-menuLayout-after'
8014 },
8015 bottom: {
8016 sizeProperty: 'height',
8017 className: 'oo-ui-menuLayout-bottom'
8018 },
8019 before: {
8020 sizeProperty: 'width',
8021 className: 'oo-ui-menuLayout-before'
8022 }
8023 };
8024
8025 /* Methods */
8026
8027 /**
8028 * Toggle menu.
8029 *
8030 * @param {boolean} showMenu Show menu, omit to toggle
8031 * @chainable
8032 */
8033 OO.ui.MenuLayout.prototype.toggleMenu = function ( showMenu ) {
8034 showMenu = showMenu === undefined ? !this.showMenu : !!showMenu;
8035
8036 if ( this.showMenu !== showMenu ) {
8037 this.showMenu = showMenu;
8038 this.updateSizes();
8039 }
8040
8041 return this;
8042 };
8043
8044 /**
8045 * Check if menu is visible
8046 *
8047 * @return {boolean} Menu is visible
8048 */
8049 OO.ui.MenuLayout.prototype.isMenuVisible = function () {
8050 return this.showMenu;
8051 };
8052
8053 /**
8054 * Set menu size.
8055 *
8056 * @param {number|string} size Size of menu in pixels or any CSS unit
8057 * @chainable
8058 */
8059 OO.ui.MenuLayout.prototype.setMenuSize = function ( size ) {
8060 this.menuSize = size;
8061 this.updateSizes();
8062
8063 return this;
8064 };
8065
8066 /**
8067 * Update menu and content CSS based on current menu size and visibility
8068 *
8069 * This method is called internally when size or position is changed.
8070 */
8071 OO.ui.MenuLayout.prototype.updateSizes = function () {
8072 if ( this.showMenu ) {
8073 this.$menu
8074 .css( this.menuPosition.sizeProperty, this.menuSize )
8075 .css( 'overflow', '' );
8076 // Set offsets on all sides. CSS resets all but one with
8077 // 'important' rules so directionality flips are supported
8078 this.$content.css( {
8079 top: this.menuSize,
8080 right: this.menuSize,
8081 bottom: this.menuSize,
8082 left: this.menuSize
8083 } );
8084 } else {
8085 this.$menu
8086 .css( this.menuPosition.sizeProperty, 0 )
8087 .css( 'overflow', 'hidden' );
8088 this.$content.css( {
8089 top: 0,
8090 right: 0,
8091 bottom: 0,
8092 left: 0
8093 } );
8094 }
8095 };
8096
8097 /**
8098 * Get menu size.
8099 *
8100 * @return {number|string} Menu size
8101 */
8102 OO.ui.MenuLayout.prototype.getMenuSize = function () {
8103 return this.menuSize;
8104 };
8105
8106 /**
8107 * Set menu position.
8108 *
8109 * @param {string} position Position of menu, either `top`, `after`, `bottom` or `before`
8110 * @throws {Error} If position value is not supported
8111 * @chainable
8112 */
8113 OO.ui.MenuLayout.prototype.setMenuPosition = function ( position ) {
8114 var positions = this.constructor.static.menuPositions;
8115
8116 if ( !positions[ position ] ) {
8117 throw new Error( 'Cannot set position; unsupported position value: ' + position );
8118 }
8119
8120 this.$menu.css( this.menuPosition.sizeProperty, '' );
8121 this.$element.removeClass( this.menuPosition.className );
8122
8123 this.menuPosition = positions[ position ];
8124
8125 this.updateSizes();
8126 this.$element.addClass( this.menuPosition.className );
8127
8128 return this;
8129 };
8130
8131 /**
8132 * Get menu position.
8133 *
8134 * @return {string} Menu position
8135 */
8136 OO.ui.MenuLayout.prototype.getMenuPosition = function () {
8137 return this.menuPosition;
8138 };
8139
8140 /**
8141 * Layout containing a series of pages.
8142 *
8143 * @class
8144 * @extends OO.ui.MenuLayout
8145 *
8146 * @constructor
8147 * @param {Object} [config] Configuration options
8148 * @cfg {boolean} [continuous=false] Show all pages, one after another
8149 * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when changing to a page
8150 * @cfg {boolean} [outlined=false] Show an outline
8151 * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
8152 */
8153 OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
8154 // Configuration initialization
8155 config = config || {};
8156
8157 // Parent constructor
8158 OO.ui.BookletLayout.super.call( this, config );
8159
8160 // Properties
8161 this.currentPageName = null;
8162 this.pages = {};
8163 this.ignoreFocus = false;
8164 this.stackLayout = new OO.ui.StackLayout( { continuous: !!config.continuous } );
8165 this.$content.append( this.stackLayout.$element );
8166 this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
8167 this.outlineVisible = false;
8168 this.outlined = !!config.outlined;
8169 if ( this.outlined ) {
8170 this.editable = !!config.editable;
8171 this.outlineControlsWidget = null;
8172 this.outlineSelectWidget = new OO.ui.OutlineSelectWidget();
8173 this.outlinePanel = new OO.ui.PanelLayout( { scrollable: true } );
8174 this.$menu.append( this.outlinePanel.$element );
8175 this.outlineVisible = true;
8176 if ( this.editable ) {
8177 this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
8178 this.outlineSelectWidget
8179 );
8180 }
8181 }
8182 this.toggleMenu( this.outlined );
8183
8184 // Events
8185 this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
8186 if ( this.outlined ) {
8187 this.outlineSelectWidget.connect( this, { select: 'onOutlineSelectWidgetSelect' } );
8188 }
8189 if ( this.autoFocus ) {
8190 // Event 'focus' does not bubble, but 'focusin' does
8191 this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
8192 }
8193
8194 // Initialization
8195 this.$element.addClass( 'oo-ui-bookletLayout' );
8196 this.stackLayout.$element.addClass( 'oo-ui-bookletLayout-stackLayout' );
8197 if ( this.outlined ) {
8198 this.outlinePanel.$element
8199 .addClass( 'oo-ui-bookletLayout-outlinePanel' )
8200 .append( this.outlineSelectWidget.$element );
8201 if ( this.editable ) {
8202 this.outlinePanel.$element
8203 .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
8204 .append( this.outlineControlsWidget.$element );
8205 }
8206 }
8207 };
8208
8209 /* Setup */
8210
8211 OO.inheritClass( OO.ui.BookletLayout, OO.ui.MenuLayout );
8212
8213 /* Events */
8214
8215 /**
8216 * @event set
8217 * @param {OO.ui.PageLayout} page Current page
8218 */
8219
8220 /**
8221 * @event add
8222 * @param {OO.ui.PageLayout[]} page Added pages
8223 * @param {number} index Index pages were added at
8224 */
8225
8226 /**
8227 * @event remove
8228 * @param {OO.ui.PageLayout[]} pages Removed pages
8229 */
8230
8231 /* Methods */
8232
8233 /**
8234 * Handle stack layout focus.
8235 *
8236 * @param {jQuery.Event} e Focusin event
8237 */
8238 OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) {
8239 var name, $target;
8240
8241 // Find the page that an element was focused within
8242 $target = $( e.target ).closest( '.oo-ui-pageLayout' );
8243 for ( name in this.pages ) {
8244 // Check for page match, exclude current page to find only page changes
8245 if ( this.pages[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentPageName ) {
8246 this.setPage( name );
8247 break;
8248 }
8249 }
8250 };
8251
8252 /**
8253 * Handle stack layout set events.
8254 *
8255 * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
8256 */
8257 OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
8258 var layout = this;
8259 if ( page ) {
8260 page.scrollElementIntoView( { complete: function () {
8261 if ( layout.autoFocus ) {
8262 layout.focus();
8263 }
8264 } } );
8265 }
8266 };
8267
8268 /**
8269 * Focus the first input in the current page.
8270 *
8271 * If no page is selected, the first selectable page will be selected.
8272 * If the focus is already in an element on the current page, nothing will happen.
8273 */
8274 OO.ui.BookletLayout.prototype.focus = function () {
8275 var $input, page = this.stackLayout.getCurrentItem();
8276 if ( !page && this.outlined ) {
8277 this.selectFirstSelectablePage();
8278 page = this.stackLayout.getCurrentItem();
8279 }
8280 if ( !page ) {
8281 return;
8282 }
8283 // Only change the focus if is not already in the current page
8284 if ( !page.$element.find( ':focus' ).length ) {
8285 $input = page.$element.find( ':input:first' );
8286 if ( $input.length ) {
8287 $input[ 0 ].focus();
8288 }
8289 }
8290 };
8291
8292 /**
8293 * Handle outline widget select events.
8294 *
8295 * @param {OO.ui.OptionWidget|null} item Selected item
8296 */
8297 OO.ui.BookletLayout.prototype.onOutlineSelectWidgetSelect = function ( item ) {
8298 if ( item ) {
8299 this.setPage( item.getData() );
8300 }
8301 };
8302
8303 /**
8304 * Check if booklet has an outline.
8305 *
8306 * @return {boolean}
8307 */
8308 OO.ui.BookletLayout.prototype.isOutlined = function () {
8309 return this.outlined;
8310 };
8311
8312 /**
8313 * Check if booklet has editing controls.
8314 *
8315 * @return {boolean}
8316 */
8317 OO.ui.BookletLayout.prototype.isEditable = function () {
8318 return this.editable;
8319 };
8320
8321 /**
8322 * Check if booklet has a visible outline.
8323 *
8324 * @return {boolean}
8325 */
8326 OO.ui.BookletLayout.prototype.isOutlineVisible = function () {
8327 return this.outlined && this.outlineVisible;
8328 };
8329
8330 /**
8331 * Hide or show the outline.
8332 *
8333 * @param {boolean} [show] Show outline, omit to invert current state
8334 * @chainable
8335 */
8336 OO.ui.BookletLayout.prototype.toggleOutline = function ( show ) {
8337 if ( this.outlined ) {
8338 show = show === undefined ? !this.outlineVisible : !!show;
8339 this.outlineVisible = show;
8340 this.toggleMenu( show );
8341 }
8342
8343 return this;
8344 };
8345
8346 /**
8347 * Get the outline widget.
8348 *
8349 * @param {OO.ui.PageLayout} page Page to be selected
8350 * @return {OO.ui.PageLayout|null} Closest page to another
8351 */
8352 OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) {
8353 var next, prev, level,
8354 pages = this.stackLayout.getItems(),
8355 index = $.inArray( page, pages );
8356
8357 if ( index !== -1 ) {
8358 next = pages[ index + 1 ];
8359 prev = pages[ index - 1 ];
8360 // Prefer adjacent pages at the same level
8361 if ( this.outlined ) {
8362 level = this.outlineSelectWidget.getItemFromData( page.getName() ).getLevel();
8363 if (
8364 prev &&
8365 level === this.outlineSelectWidget.getItemFromData( prev.getName() ).getLevel()
8366 ) {
8367 return prev;
8368 }
8369 if (
8370 next &&
8371 level === this.outlineSelectWidget.getItemFromData( next.getName() ).getLevel()
8372 ) {
8373 return next;
8374 }
8375 }
8376 }
8377 return prev || next || null;
8378 };
8379
8380 /**
8381 * Get the outline widget.
8382 *
8383 * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if booklet has no outline
8384 */
8385 OO.ui.BookletLayout.prototype.getOutline = function () {
8386 return this.outlineSelectWidget;
8387 };
8388
8389 /**
8390 * Get the outline controls widget. If the outline is not editable, null is returned.
8391 *
8392 * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
8393 */
8394 OO.ui.BookletLayout.prototype.getOutlineControls = function () {
8395 return this.outlineControlsWidget;
8396 };
8397
8398 /**
8399 * Get a page by name.
8400 *
8401 * @param {string} name Symbolic name of page
8402 * @return {OO.ui.PageLayout|undefined} Page, if found
8403 */
8404 OO.ui.BookletLayout.prototype.getPage = function ( name ) {
8405 return this.pages[ name ];
8406 };
8407
8408 /**
8409 * Get the current page
8410 *
8411 * @return {OO.ui.PageLayout|undefined} Current page, if found
8412 */
8413 OO.ui.BookletLayout.prototype.getCurrentPage = function () {
8414 var name = this.getCurrentPageName();
8415 return name ? this.getPage( name ) : undefined;
8416 };
8417
8418 /**
8419 * Get the current page name.
8420 *
8421 * @return {string|null} Current page name
8422 */
8423 OO.ui.BookletLayout.prototype.getCurrentPageName = function () {
8424 return this.currentPageName;
8425 };
8426
8427 /**
8428 * Add a page to the layout.
8429 *
8430 * When pages are added with the same names as existing pages, the existing pages will be
8431 * automatically removed before the new pages are added.
8432 *
8433 * @param {OO.ui.PageLayout[]} pages Pages to add
8434 * @param {number} index Index to insert pages after
8435 * @fires add
8436 * @chainable
8437 */
8438 OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) {
8439 var i, len, name, page, item, currentIndex,
8440 stackLayoutPages = this.stackLayout.getItems(),
8441 remove = [],
8442 items = [];
8443
8444 // Remove pages with same names
8445 for ( i = 0, len = pages.length; i < len; i++ ) {
8446 page = pages[ i ];
8447 name = page.getName();
8448
8449 if ( Object.prototype.hasOwnProperty.call( this.pages, name ) ) {
8450 // Correct the insertion index
8451 currentIndex = $.inArray( this.pages[ name ], stackLayoutPages );
8452 if ( currentIndex !== -1 && currentIndex + 1 < index ) {
8453 index--;
8454 }
8455 remove.push( this.pages[ name ] );
8456 }
8457 }
8458 if ( remove.length ) {
8459 this.removePages( remove );
8460 }
8461
8462 // Add new pages
8463 for ( i = 0, len = pages.length; i < len; i++ ) {
8464 page = pages[ i ];
8465 name = page.getName();
8466 this.pages[ page.getName() ] = page;
8467 if ( this.outlined ) {
8468 item = new OO.ui.OutlineOptionWidget( { data: name } );
8469 page.setOutlineItem( item );
8470 items.push( item );
8471 }
8472 }
8473
8474 if ( this.outlined && items.length ) {
8475 this.outlineSelectWidget.addItems( items, index );
8476 this.selectFirstSelectablePage();
8477 }
8478 this.stackLayout.addItems( pages, index );
8479 this.emit( 'add', pages, index );
8480
8481 return this;
8482 };
8483
8484 /**
8485 * Remove a page from the layout.
8486 *
8487 * @fires remove
8488 * @chainable
8489 */
8490 OO.ui.BookletLayout.prototype.removePages = function ( pages ) {
8491 var i, len, name, page,
8492 items = [];
8493
8494 for ( i = 0, len = pages.length; i < len; i++ ) {
8495 page = pages[ i ];
8496 name = page.getName();
8497 delete this.pages[ name ];
8498 if ( this.outlined ) {
8499 items.push( this.outlineSelectWidget.getItemFromData( name ) );
8500 page.setOutlineItem( null );
8501 }
8502 }
8503 if ( this.outlined && items.length ) {
8504 this.outlineSelectWidget.removeItems( items );
8505 this.selectFirstSelectablePage();
8506 }
8507 this.stackLayout.removeItems( pages );
8508 this.emit( 'remove', pages );
8509
8510 return this;
8511 };
8512
8513 /**
8514 * Clear all pages from the layout.
8515 *
8516 * @fires remove
8517 * @chainable
8518 */
8519 OO.ui.BookletLayout.prototype.clearPages = function () {
8520 var i, len,
8521 pages = this.stackLayout.getItems();
8522
8523 this.pages = {};
8524 this.currentPageName = null;
8525 if ( this.outlined ) {
8526 this.outlineSelectWidget.clearItems();
8527 for ( i = 0, len = pages.length; i < len; i++ ) {
8528 pages[ i ].setOutlineItem( null );
8529 }
8530 }
8531 this.stackLayout.clearItems();
8532
8533 this.emit( 'remove', pages );
8534
8535 return this;
8536 };
8537
8538 /**
8539 * Set the current page by name.
8540 *
8541 * @fires set
8542 * @param {string} name Symbolic name of page
8543 */
8544 OO.ui.BookletLayout.prototype.setPage = function ( name ) {
8545 var selectedItem,
8546 $focused,
8547 page = this.pages[ name ];
8548
8549 if ( name !== this.currentPageName ) {
8550 if ( this.outlined ) {
8551 selectedItem = this.outlineSelectWidget.getSelectedItem();
8552 if ( selectedItem && selectedItem.getData() !== name ) {
8553 this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getItemFromData( name ) );
8554 }
8555 }
8556 if ( page ) {
8557 if ( this.currentPageName && this.pages[ this.currentPageName ] ) {
8558 this.pages[ this.currentPageName ].setActive( false );
8559 // Blur anything focused if the next page doesn't have anything focusable - this
8560 // is not needed if the next page has something focusable because once it is focused
8561 // this blur happens automatically
8562 if ( this.autoFocus && !page.$element.find( ':input' ).length ) {
8563 $focused = this.pages[ this.currentPageName ].$element.find( ':focus' );
8564 if ( $focused.length ) {
8565 $focused[ 0 ].blur();
8566 }
8567 }
8568 }
8569 this.currentPageName = name;
8570 this.stackLayout.setItem( page );
8571 page.setActive( true );
8572 this.emit( 'set', page );
8573 }
8574 }
8575 };
8576
8577 /**
8578 * Select the first selectable page.
8579 *
8580 * @chainable
8581 */
8582 OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () {
8583 if ( !this.outlineSelectWidget.getSelectedItem() ) {
8584 this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getFirstSelectableItem() );
8585 }
8586
8587 return this;
8588 };
8589
8590 /**
8591 * Layout that expands to cover the entire area of its parent, with optional scrolling and padding.
8592 *
8593 * @class
8594 * @extends OO.ui.Layout
8595 *
8596 * @constructor
8597 * @param {Object} [config] Configuration options
8598 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
8599 * @cfg {boolean} [padded=false] Pad the content from the edges
8600 * @cfg {boolean} [expanded=true] Expand size to fill the entire parent element
8601 * @cfg {boolean} [framed=false] Wrap in a frame to visually separate from outside content
8602 */
8603 OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
8604 // Configuration initialization
8605 config = $.extend( {
8606 scrollable: false,
8607 padded: false,
8608 expanded: true,
8609 framed: false
8610 }, config );
8611
8612 // Parent constructor
8613 OO.ui.PanelLayout.super.call( this, config );
8614
8615 // Initialization
8616 this.$element.addClass( 'oo-ui-panelLayout' );
8617 if ( config.scrollable ) {
8618 this.$element.addClass( 'oo-ui-panelLayout-scrollable' );
8619 }
8620 if ( config.padded ) {
8621 this.$element.addClass( 'oo-ui-panelLayout-padded' );
8622 }
8623 if ( config.expanded ) {
8624 this.$element.addClass( 'oo-ui-panelLayout-expanded' );
8625 }
8626 if ( config.framed ) {
8627 this.$element.addClass( 'oo-ui-panelLayout-framed' );
8628 }
8629 };
8630
8631 /* Setup */
8632
8633 OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
8634
8635 /**
8636 * Page within an booklet layout.
8637 *
8638 * @class
8639 * @extends OO.ui.PanelLayout
8640 *
8641 * @constructor
8642 * @param {string} name Unique symbolic name of page
8643 * @param {Object} [config] Configuration options
8644 */
8645 OO.ui.PageLayout = function OoUiPageLayout( name, config ) {
8646 // Allow passing positional parameters inside the config object
8647 if ( OO.isPlainObject( name ) && config === undefined ) {
8648 config = name;
8649 name = config.name;
8650 }
8651
8652 // Configuration initialization
8653 config = $.extend( { scrollable: true }, config );
8654
8655 // Parent constructor
8656 OO.ui.PageLayout.super.call( this, config );
8657
8658 // Properties
8659 this.name = name;
8660 this.outlineItem = null;
8661 this.active = false;
8662
8663 // Initialization
8664 this.$element.addClass( 'oo-ui-pageLayout' );
8665 };
8666
8667 /* Setup */
8668
8669 OO.inheritClass( OO.ui.PageLayout, OO.ui.PanelLayout );
8670
8671 /* Events */
8672
8673 /**
8674 * @event active
8675 * @param {boolean} active Page is active
8676 */
8677
8678 /* Methods */
8679
8680 /**
8681 * Get page name.
8682 *
8683 * @return {string} Symbolic name of page
8684 */
8685 OO.ui.PageLayout.prototype.getName = function () {
8686 return this.name;
8687 };
8688
8689 /**
8690 * Check if page is active.
8691 *
8692 * @return {boolean} Page is active
8693 */
8694 OO.ui.PageLayout.prototype.isActive = function () {
8695 return this.active;
8696 };
8697
8698 /**
8699 * Get outline item.
8700 *
8701 * @return {OO.ui.OutlineOptionWidget|null} Outline item widget
8702 */
8703 OO.ui.PageLayout.prototype.getOutlineItem = function () {
8704 return this.outlineItem;
8705 };
8706
8707 /**
8708 * Set outline item.
8709 *
8710 * @localdoc Subclasses should override #setupOutlineItem instead of this method to adjust the
8711 * outline item as desired; this method is called for setting (with an object) and unsetting
8712 * (with null) and overriding methods would have to check the value of `outlineItem` to avoid
8713 * operating on null instead of an OO.ui.OutlineOptionWidget object.
8714 *
8715 * @param {OO.ui.OutlineOptionWidget|null} outlineItem Outline item widget, null to clear
8716 * @chainable
8717 */
8718 OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) {
8719 this.outlineItem = outlineItem || null;
8720 if ( outlineItem ) {
8721 this.setupOutlineItem();
8722 }
8723 return this;
8724 };
8725
8726 /**
8727 * Setup outline item.
8728 *
8729 * @localdoc Subclasses should override this method to adjust the outline item as desired.
8730 *
8731 * @param {OO.ui.OutlineOptionWidget} outlineItem Outline item widget to setup
8732 * @chainable
8733 */
8734 OO.ui.PageLayout.prototype.setupOutlineItem = function () {
8735 return this;
8736 };
8737
8738 /**
8739 * Set page active state.
8740 *
8741 * @param {boolean} Page is active
8742 * @fires active
8743 */
8744 OO.ui.PageLayout.prototype.setActive = function ( active ) {
8745 active = !!active;
8746
8747 if ( active !== this.active ) {
8748 this.active = active;
8749 this.$element.toggleClass( 'oo-ui-pageLayout-active', active );
8750 this.emit( 'active', this.active );
8751 }
8752 };
8753
8754 /**
8755 * Layout containing a series of mutually exclusive pages.
8756 *
8757 * @class
8758 * @extends OO.ui.PanelLayout
8759 * @mixins OO.ui.GroupElement
8760 *
8761 * @constructor
8762 * @param {Object} [config] Configuration options
8763 * @cfg {boolean} [continuous=false] Show all pages, one after another
8764 * @cfg {OO.ui.Layout[]} [items] Layouts to add
8765 */
8766 OO.ui.StackLayout = function OoUiStackLayout( config ) {
8767 // Configuration initialization
8768 config = $.extend( { scrollable: true }, config );
8769
8770 // Parent constructor
8771 OO.ui.StackLayout.super.call( this, config );
8772
8773 // Mixin constructors
8774 OO.ui.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
8775
8776 // Properties
8777 this.currentItem = null;
8778 this.continuous = !!config.continuous;
8779
8780 // Initialization
8781 this.$element.addClass( 'oo-ui-stackLayout' );
8782 if ( this.continuous ) {
8783 this.$element.addClass( 'oo-ui-stackLayout-continuous' );
8784 }
8785 if ( Array.isArray( config.items ) ) {
8786 this.addItems( config.items );
8787 }
8788 };
8789
8790 /* Setup */
8791
8792 OO.inheritClass( OO.ui.StackLayout, OO.ui.PanelLayout );
8793 OO.mixinClass( OO.ui.StackLayout, OO.ui.GroupElement );
8794
8795 /* Events */
8796
8797 /**
8798 * @event set
8799 * @param {OO.ui.Layout|null} item Current item or null if there is no longer a layout shown
8800 */
8801
8802 /* Methods */
8803
8804 /**
8805 * Get the current item.
8806 *
8807 * @return {OO.ui.Layout|null}
8808 */
8809 OO.ui.StackLayout.prototype.getCurrentItem = function () {
8810 return this.currentItem;
8811 };
8812
8813 /**
8814 * Unset the current item.
8815 *
8816 * @private
8817 * @param {OO.ui.StackLayout} layout
8818 * @fires set
8819 */
8820 OO.ui.StackLayout.prototype.unsetCurrentItem = function () {
8821 var prevItem = this.currentItem;
8822 if ( prevItem === null ) {
8823 return;
8824 }
8825
8826 this.currentItem = null;
8827 this.emit( 'set', null );
8828 };
8829
8830 /**
8831 * Add items.
8832 *
8833 * Adding an existing item (by value) will move it.
8834 *
8835 * @param {OO.ui.Layout[]} items Items to add
8836 * @param {number} [index] Index to insert items after
8837 * @chainable
8838 */
8839 OO.ui.StackLayout.prototype.addItems = function ( items, index ) {
8840 // Update the visibility
8841 this.updateHiddenState( items, this.currentItem );
8842
8843 // Mixin method
8844 OO.ui.GroupElement.prototype.addItems.call( this, items, index );
8845
8846 if ( !this.currentItem && items.length ) {
8847 this.setItem( items[ 0 ] );
8848 }
8849
8850 return this;
8851 };
8852
8853 /**
8854 * Remove items.
8855 *
8856 * Items will be detached, not removed, so they can be used later.
8857 *
8858 * @param {OO.ui.Layout[]} items Items to remove
8859 * @chainable
8860 * @fires set
8861 */
8862 OO.ui.StackLayout.prototype.removeItems = function ( items ) {
8863 // Mixin method
8864 OO.ui.GroupElement.prototype.removeItems.call( this, items );
8865
8866 if ( $.inArray( this.currentItem, items ) !== -1 ) {
8867 if ( this.items.length ) {
8868 this.setItem( this.items[ 0 ] );
8869 } else {
8870 this.unsetCurrentItem();
8871 }
8872 }
8873
8874 return this;
8875 };
8876
8877 /**
8878 * Clear all items.
8879 *
8880 * Items will be detached, not removed, so they can be used later.
8881 *
8882 * @chainable
8883 * @fires set
8884 */
8885 OO.ui.StackLayout.prototype.clearItems = function () {
8886 this.unsetCurrentItem();
8887 OO.ui.GroupElement.prototype.clearItems.call( this );
8888
8889 return this;
8890 };
8891
8892 /**
8893 * Show item.
8894 *
8895 * Any currently shown item will be hidden.
8896 *
8897 * FIXME: If the passed item to show has not been added in the items list, then
8898 * this method drops it and unsets the current item.
8899 *
8900 * @param {OO.ui.Layout} item Item to show
8901 * @chainable
8902 * @fires set
8903 */
8904 OO.ui.StackLayout.prototype.setItem = function ( item ) {
8905 if ( item !== this.currentItem ) {
8906 this.updateHiddenState( this.items, item );
8907
8908 if ( $.inArray( item, this.items ) !== -1 ) {
8909 this.currentItem = item;
8910 this.emit( 'set', item );
8911 } else {
8912 this.unsetCurrentItem();
8913 }
8914 }
8915
8916 return this;
8917 };
8918
8919 /**
8920 * Update the visibility of all items in case of non-continuous view.
8921 *
8922 * Ensure all items are hidden except for the selected one.
8923 * This method does nothing when the stack is continuous.
8924 *
8925 * @param {OO.ui.Layout[]} items Item list iterate over
8926 * @param {OO.ui.Layout} [selectedItem] Selected item to show
8927 */
8928 OO.ui.StackLayout.prototype.updateHiddenState = function ( items, selectedItem ) {
8929 var i, len;
8930
8931 if ( !this.continuous ) {
8932 for ( i = 0, len = items.length; i < len; i++ ) {
8933 if ( !selectedItem || selectedItem !== items[ i ] ) {
8934 items[ i ].$element.addClass( 'oo-ui-element-hidden' );
8935 }
8936 }
8937 if ( selectedItem ) {
8938 selectedItem.$element.removeClass( 'oo-ui-element-hidden' );
8939 }
8940 }
8941 };
8942
8943 /**
8944 * Horizontal bar layout of tools as icon buttons.
8945 *
8946 * @class
8947 * @extends OO.ui.ToolGroup
8948 *
8949 * @constructor
8950 * @param {OO.ui.Toolbar} toolbar
8951 * @param {Object} [config] Configuration options
8952 */
8953 OO.ui.BarToolGroup = function OoUiBarToolGroup( toolbar, config ) {
8954 // Allow passing positional parameters inside the config object
8955 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
8956 config = toolbar;
8957 toolbar = config.toolbar;
8958 }
8959
8960 // Parent constructor
8961 OO.ui.BarToolGroup.super.call( this, toolbar, config );
8962
8963 // Initialization
8964 this.$element.addClass( 'oo-ui-barToolGroup' );
8965 };
8966
8967 /* Setup */
8968
8969 OO.inheritClass( OO.ui.BarToolGroup, OO.ui.ToolGroup );
8970
8971 /* Static Properties */
8972
8973 OO.ui.BarToolGroup.static.titleTooltips = true;
8974
8975 OO.ui.BarToolGroup.static.accelTooltips = true;
8976
8977 OO.ui.BarToolGroup.static.name = 'bar';
8978
8979 /**
8980 * Popup list of tools with an icon and optional label.
8981 *
8982 * @abstract
8983 * @class
8984 * @extends OO.ui.ToolGroup
8985 * @mixins OO.ui.IconElement
8986 * @mixins OO.ui.IndicatorElement
8987 * @mixins OO.ui.LabelElement
8988 * @mixins OO.ui.TitledElement
8989 * @mixins OO.ui.ClippableElement
8990 *
8991 * @constructor
8992 * @param {OO.ui.Toolbar} toolbar
8993 * @param {Object} [config] Configuration options
8994 * @cfg {string} [header] Text to display at the top of the pop-up
8995 */
8996 OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
8997 // Allow passing positional parameters inside the config object
8998 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
8999 config = toolbar;
9000 toolbar = config.toolbar;
9001 }
9002
9003 // Configuration initialization
9004 config = config || {};
9005
9006 // Parent constructor
9007 OO.ui.PopupToolGroup.super.call( this, toolbar, config );
9008
9009 // Mixin constructors
9010 OO.ui.IconElement.call( this, config );
9011 OO.ui.IndicatorElement.call( this, config );
9012 OO.ui.LabelElement.call( this, config );
9013 OO.ui.TitledElement.call( this, config );
9014 OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
9015
9016 // Properties
9017 this.active = false;
9018 this.dragging = false;
9019 this.onBlurHandler = this.onBlur.bind( this );
9020 this.$handle = $( '<span>' );
9021
9022 // Events
9023 this.$handle.on( {
9024 mousedown: this.onHandlePointerDown.bind( this ),
9025 mouseup: this.onHandlePointerUp.bind( this )
9026 } );
9027
9028 // Initialization
9029 this.$handle
9030 .addClass( 'oo-ui-popupToolGroup-handle' )
9031 .append( this.$icon, this.$label, this.$indicator );
9032 // If the pop-up should have a header, add it to the top of the toolGroup.
9033 // Note: If this feature is useful for other widgets, we could abstract it into an
9034 // OO.ui.HeaderedElement mixin constructor.
9035 if ( config.header !== undefined ) {
9036 this.$group
9037 .prepend( $( '<span>' )
9038 .addClass( 'oo-ui-popupToolGroup-header' )
9039 .text( config.header )
9040 );
9041 }
9042 this.$element
9043 .addClass( 'oo-ui-popupToolGroup' )
9044 .prepend( this.$handle );
9045 };
9046
9047 /* Setup */
9048
9049 OO.inheritClass( OO.ui.PopupToolGroup, OO.ui.ToolGroup );
9050 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IconElement );
9051 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IndicatorElement );
9052 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.LabelElement );
9053 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.TitledElement );
9054 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.ClippableElement );
9055
9056 /* Static Properties */
9057
9058 /* Methods */
9059
9060 /**
9061 * @inheritdoc
9062 */
9063 OO.ui.PopupToolGroup.prototype.setDisabled = function () {
9064 // Parent method
9065 OO.ui.PopupToolGroup.super.prototype.setDisabled.apply( this, arguments );
9066
9067 if ( this.isDisabled() && this.isElementAttached() ) {
9068 this.setActive( false );
9069 }
9070 };
9071
9072 /**
9073 * Handle focus being lost.
9074 *
9075 * The event is actually generated from a mouseup, so it is not a normal blur event object.
9076 *
9077 * @param {jQuery.Event} e Mouse up event
9078 */
9079 OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) {
9080 // Only deactivate when clicking outside the dropdown element
9081 if ( $( e.target ).closest( '.oo-ui-popupToolGroup' )[ 0 ] !== this.$element[ 0 ] ) {
9082 this.setActive( false );
9083 }
9084 };
9085
9086 /**
9087 * @inheritdoc
9088 */
9089 OO.ui.PopupToolGroup.prototype.onPointerUp = function ( e ) {
9090 // Only close toolgroup when a tool was actually selected
9091 if ( !this.isDisabled() && e.which === 1 && this.pressed && this.pressed === this.getTargetTool( e ) ) {
9092 this.setActive( false );
9093 }
9094 return OO.ui.PopupToolGroup.super.prototype.onPointerUp.call( this, e );
9095 };
9096
9097 /**
9098 * Handle mouse up events.
9099 *
9100 * @param {jQuery.Event} e Mouse up event
9101 */
9102 OO.ui.PopupToolGroup.prototype.onHandlePointerUp = function () {
9103 return false;
9104 };
9105
9106 /**
9107 * Handle mouse down events.
9108 *
9109 * @param {jQuery.Event} e Mouse down event
9110 */
9111 OO.ui.PopupToolGroup.prototype.onHandlePointerDown = function ( e ) {
9112 if ( !this.isDisabled() && e.which === 1 ) {
9113 this.setActive( !this.active );
9114 }
9115 return false;
9116 };
9117
9118 /**
9119 * Switch into active mode.
9120 *
9121 * When active, mouseup events anywhere in the document will trigger deactivation.
9122 */
9123 OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
9124 value = !!value;
9125 if ( this.active !== value ) {
9126 this.active = value;
9127 if ( value ) {
9128 this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler, true );
9129
9130 // Try anchoring the popup to the left first
9131 this.$element.addClass( 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left' );
9132 this.toggleClipping( true );
9133 if ( this.isClippedHorizontally() ) {
9134 // Anchoring to the left caused the popup to clip, so anchor it to the right instead
9135 this.toggleClipping( false );
9136 this.$element
9137 .removeClass( 'oo-ui-popupToolGroup-left' )
9138 .addClass( 'oo-ui-popupToolGroup-right' );
9139 this.toggleClipping( true );
9140 }
9141 } else {
9142 this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler, true );
9143 this.$element.removeClass(
9144 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left oo-ui-popupToolGroup-right'
9145 );
9146 this.toggleClipping( false );
9147 }
9148 }
9149 };
9150
9151 /**
9152 * Drop down list layout of tools as labeled icon buttons.
9153 *
9154 * This layout allows some tools to be collapsible, controlled by a "More" / "Fewer" option at the
9155 * bottom of the main list. These are not automatically positioned at the bottom of the list; you
9156 * may want to use the 'promote' and 'demote' configuration options to achieve this.
9157 *
9158 * @class
9159 * @extends OO.ui.PopupToolGroup
9160 *
9161 * @constructor
9162 * @param {OO.ui.Toolbar} toolbar
9163 * @param {Object} [config] Configuration options
9164 * @cfg {Array} [allowCollapse] List of tools that can be collapsed. Remaining tools will be always
9165 * shown.
9166 * @cfg {Array} [forceExpand] List of tools that *may not* be collapsed. All remaining tools will be
9167 * allowed to be collapsed.
9168 * @cfg {boolean} [expanded=false] Whether the collapsible tools are expanded by default
9169 */
9170 OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
9171 // Allow passing positional parameters inside the config object
9172 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
9173 config = toolbar;
9174 toolbar = config.toolbar;
9175 }
9176
9177 // Configuration initialization
9178 config = config || {};
9179
9180 // Properties (must be set before parent constructor, which calls #populate)
9181 this.allowCollapse = config.allowCollapse;
9182 this.forceExpand = config.forceExpand;
9183 this.expanded = config.expanded !== undefined ? config.expanded : false;
9184 this.collapsibleTools = [];
9185
9186 // Parent constructor
9187 OO.ui.ListToolGroup.super.call( this, toolbar, config );
9188
9189 // Initialization
9190 this.$element.addClass( 'oo-ui-listToolGroup' );
9191 };
9192
9193 /* Setup */
9194
9195 OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup );
9196
9197 /* Static Properties */
9198
9199 OO.ui.ListToolGroup.static.accelTooltips = true;
9200
9201 OO.ui.ListToolGroup.static.name = 'list';
9202
9203 /* Methods */
9204
9205 /**
9206 * @inheritdoc
9207 */
9208 OO.ui.ListToolGroup.prototype.populate = function () {
9209 var i, len, allowCollapse = [];
9210
9211 OO.ui.ListToolGroup.super.prototype.populate.call( this );
9212
9213 // Update the list of collapsible tools
9214 if ( this.allowCollapse !== undefined ) {
9215 allowCollapse = this.allowCollapse;
9216 } else if ( this.forceExpand !== undefined ) {
9217 allowCollapse = OO.simpleArrayDifference( Object.keys( this.tools ), this.forceExpand );
9218 }
9219
9220 this.collapsibleTools = [];
9221 for ( i = 0, len = allowCollapse.length; i < len; i++ ) {
9222 if ( this.tools[ allowCollapse[ i ] ] !== undefined ) {
9223 this.collapsibleTools.push( this.tools[ allowCollapse[ i ] ] );
9224 }
9225 }
9226
9227 // Keep at the end, even when tools are added
9228 this.$group.append( this.getExpandCollapseTool().$element );
9229
9230 this.getExpandCollapseTool().toggle( this.collapsibleTools.length !== 0 );
9231 this.updateCollapsibleState();
9232 };
9233
9234 OO.ui.ListToolGroup.prototype.getExpandCollapseTool = function () {
9235 if ( this.expandCollapseTool === undefined ) {
9236 var ExpandCollapseTool = function () {
9237 ExpandCollapseTool.super.apply( this, arguments );
9238 };
9239
9240 OO.inheritClass( ExpandCollapseTool, OO.ui.Tool );
9241
9242 ExpandCollapseTool.prototype.onSelect = function () {
9243 this.toolGroup.expanded = !this.toolGroup.expanded;
9244 this.toolGroup.updateCollapsibleState();
9245 this.setActive( false );
9246 };
9247 ExpandCollapseTool.prototype.onUpdateState = function () {
9248 // Do nothing. Tool interface requires an implementation of this function.
9249 };
9250
9251 ExpandCollapseTool.static.name = 'more-fewer';
9252
9253 this.expandCollapseTool = new ExpandCollapseTool( this );
9254 }
9255 return this.expandCollapseTool;
9256 };
9257
9258 /**
9259 * @inheritdoc
9260 */
9261 OO.ui.ListToolGroup.prototype.onPointerUp = function ( e ) {
9262 var ret = OO.ui.ListToolGroup.super.prototype.onPointerUp.call( this, e );
9263
9264 // Do not close the popup when the user wants to show more/fewer tools
9265 if ( $( e.target ).closest( '.oo-ui-tool-name-more-fewer' ).length ) {
9266 // Prevent the popup list from being hidden
9267 this.setActive( true );
9268 }
9269
9270 return ret;
9271 };
9272
9273 OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () {
9274 var i, len;
9275
9276 this.getExpandCollapseTool()
9277 .setIcon( this.expanded ? 'collapse' : 'expand' )
9278 .setTitle( OO.ui.msg( this.expanded ? 'ooui-toolgroup-collapse' : 'ooui-toolgroup-expand' ) );
9279
9280 for ( i = 0, len = this.collapsibleTools.length; i < len; i++ ) {
9281 this.collapsibleTools[ i ].toggle( this.expanded );
9282 }
9283 };
9284
9285 /**
9286 * Drop down menu layout of tools as selectable menu items.
9287 *
9288 * @class
9289 * @extends OO.ui.PopupToolGroup
9290 *
9291 * @constructor
9292 * @param {OO.ui.Toolbar} toolbar
9293 * @param {Object} [config] Configuration options
9294 */
9295 OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
9296 // Allow passing positional parameters inside the config object
9297 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
9298 config = toolbar;
9299 toolbar = config.toolbar;
9300 }
9301
9302 // Configuration initialization
9303 config = config || {};
9304
9305 // Parent constructor
9306 OO.ui.MenuToolGroup.super.call( this, toolbar, config );
9307
9308 // Events
9309 this.toolbar.connect( this, { updateState: 'onUpdateState' } );
9310
9311 // Initialization
9312 this.$element.addClass( 'oo-ui-menuToolGroup' );
9313 };
9314
9315 /* Setup */
9316
9317 OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup );
9318
9319 /* Static Properties */
9320
9321 OO.ui.MenuToolGroup.static.accelTooltips = true;
9322
9323 OO.ui.MenuToolGroup.static.name = 'menu';
9324
9325 /* Methods */
9326
9327 /**
9328 * Handle the toolbar state being updated.
9329 *
9330 * When the state changes, the title of each active item in the menu will be joined together and
9331 * used as a label for the group. The label will be empty if none of the items are active.
9332 */
9333 OO.ui.MenuToolGroup.prototype.onUpdateState = function () {
9334 var name,
9335 labelTexts = [];
9336
9337 for ( name in this.tools ) {
9338 if ( this.tools[ name ].isActive() ) {
9339 labelTexts.push( this.tools[ name ].getTitle() );
9340 }
9341 }
9342
9343 this.setLabel( labelTexts.join( ', ' ) || ' ' );
9344 };
9345
9346 /**
9347 * Tool that shows a popup when selected.
9348 *
9349 * @abstract
9350 * @class
9351 * @extends OO.ui.Tool
9352 * @mixins OO.ui.PopupElement
9353 *
9354 * @constructor
9355 * @param {OO.ui.ToolGroup} toolGroup
9356 * @param {Object} [config] Configuration options
9357 */
9358 OO.ui.PopupTool = function OoUiPopupTool( toolGroup, config ) {
9359 // Allow passing positional parameters inside the config object
9360 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
9361 config = toolGroup;
9362 toolGroup = config.toolGroup;
9363 }
9364
9365 // Parent constructor
9366 OO.ui.PopupTool.super.call( this, toolGroup, config );
9367
9368 // Mixin constructors
9369 OO.ui.PopupElement.call( this, config );
9370
9371 // Initialization
9372 this.$element
9373 .addClass( 'oo-ui-popupTool' )
9374 .append( this.popup.$element );
9375 };
9376
9377 /* Setup */
9378
9379 OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool );
9380 OO.mixinClass( OO.ui.PopupTool, OO.ui.PopupElement );
9381
9382 /* Methods */
9383
9384 /**
9385 * Handle the tool being selected.
9386 *
9387 * @inheritdoc
9388 */
9389 OO.ui.PopupTool.prototype.onSelect = function () {
9390 if ( !this.isDisabled() ) {
9391 this.popup.toggle();
9392 }
9393 this.setActive( false );
9394 return false;
9395 };
9396
9397 /**
9398 * Handle the toolbar state being updated.
9399 *
9400 * @inheritdoc
9401 */
9402 OO.ui.PopupTool.prototype.onUpdateState = function () {
9403 this.setActive( false );
9404 };
9405
9406 /**
9407 * Tool that has a tool group inside. This is a bad workaround for the lack of proper hierarchical
9408 * menus in toolbars (T74159).
9409 *
9410 * @abstract
9411 * @class
9412 * @extends OO.ui.Tool
9413 *
9414 * @constructor
9415 * @param {OO.ui.ToolGroup} toolGroup
9416 * @param {Object} [config] Configuration options
9417 */
9418 OO.ui.ToolGroupTool = function OoUiToolGroupTool( toolGroup, config ) {
9419 // Allow passing positional parameters inside the config object
9420 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
9421 config = toolGroup;
9422 toolGroup = config.toolGroup;
9423 }
9424
9425 // Parent constructor
9426 OO.ui.ToolGroupTool.super.call( this, toolGroup, config );
9427
9428 // Properties
9429 this.innerToolGroup = this.createGroup( this.constructor.static.groupConfig );
9430
9431 // Initialization
9432 this.$link.remove();
9433 this.$element
9434 .addClass( 'oo-ui-toolGroupTool' )
9435 .append( this.innerToolGroup.$element );
9436 };
9437
9438 /* Setup */
9439
9440 OO.inheritClass( OO.ui.ToolGroupTool, OO.ui.Tool );
9441
9442 /* Static Properties */
9443
9444 /**
9445 * Tool group configuration. See OO.ui.Toolbar#setup for the accepted values.
9446 *
9447 * @property {Object.<string,Array>}
9448 */
9449 OO.ui.ToolGroupTool.static.groupConfig = {};
9450
9451 /* Methods */
9452
9453 /**
9454 * Handle the tool being selected.
9455 *
9456 * @inheritdoc
9457 */
9458 OO.ui.ToolGroupTool.prototype.onSelect = function () {
9459 this.innerToolGroup.setActive( !this.innerToolGroup.active );
9460 return false;
9461 };
9462
9463 /**
9464 * Handle the toolbar state being updated.
9465 *
9466 * @inheritdoc
9467 */
9468 OO.ui.ToolGroupTool.prototype.onUpdateState = function () {
9469 this.setActive( false );
9470 };
9471
9472 /**
9473 * Build a OO.ui.ToolGroup from the configuration.
9474 *
9475 * @param {Object.<string,Array>} group Tool group configuration. See OO.ui.Toolbar#setup for the
9476 * accepted values.
9477 * @return {OO.ui.ListToolGroup}
9478 */
9479 OO.ui.ToolGroupTool.prototype.createGroup = function ( group ) {
9480 if ( group.include === '*' ) {
9481 // Apply defaults to catch-all groups
9482 if ( group.label === undefined ) {
9483 group.label = OO.ui.msg( 'ooui-toolbar-more' );
9484 }
9485 }
9486
9487 return this.toolbar.getToolGroupFactory().create( 'list', this.toolbar, group );
9488 };
9489
9490 /**
9491 * Mixin for OO.ui.Widget subclasses to provide OO.ui.GroupElement.
9492 *
9493 * Use together with OO.ui.ItemWidget to make disabled state inheritable.
9494 *
9495 * @private
9496 * @abstract
9497 * @class
9498 * @extends OO.ui.GroupElement
9499 *
9500 * @constructor
9501 * @param {Object} [config] Configuration options
9502 */
9503 OO.ui.GroupWidget = function OoUiGroupWidget( config ) {
9504 // Parent constructor
9505 OO.ui.GroupWidget.super.call( this, config );
9506 };
9507
9508 /* Setup */
9509
9510 OO.inheritClass( OO.ui.GroupWidget, OO.ui.GroupElement );
9511
9512 /* Methods */
9513
9514 /**
9515 * Set the disabled state of the widget.
9516 *
9517 * This will also update the disabled state of child widgets.
9518 *
9519 * @param {boolean} disabled Disable widget
9520 * @chainable
9521 */
9522 OO.ui.GroupWidget.prototype.setDisabled = function ( disabled ) {
9523 var i, len;
9524
9525 // Parent method
9526 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
9527 OO.ui.Widget.prototype.setDisabled.call( this, disabled );
9528
9529 // During construction, #setDisabled is called before the OO.ui.GroupElement constructor
9530 if ( this.items ) {
9531 for ( i = 0, len = this.items.length; i < len; i++ ) {
9532 this.items[ i ].updateDisabled();
9533 }
9534 }
9535
9536 return this;
9537 };
9538
9539 /**
9540 * Mixin for widgets used as items in widgets that inherit OO.ui.GroupWidget.
9541 *
9542 * Item widgets have a reference to a OO.ui.GroupWidget while they are attached to the group. This
9543 * allows bidirectional communication.
9544 *
9545 * Use together with OO.ui.GroupWidget to make disabled state inheritable.
9546 *
9547 * @private
9548 * @abstract
9549 * @class
9550 *
9551 * @constructor
9552 */
9553 OO.ui.ItemWidget = function OoUiItemWidget() {
9554 //
9555 };
9556
9557 /* Methods */
9558
9559 /**
9560 * Check if widget is disabled.
9561 *
9562 * Checks parent if present, making disabled state inheritable.
9563 *
9564 * @return {boolean} Widget is disabled
9565 */
9566 OO.ui.ItemWidget.prototype.isDisabled = function () {
9567 return this.disabled ||
9568 ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() );
9569 };
9570
9571 /**
9572 * Set group element is in.
9573 *
9574 * @param {OO.ui.GroupElement|null} group Group element, null if none
9575 * @chainable
9576 */
9577 OO.ui.ItemWidget.prototype.setElementGroup = function ( group ) {
9578 // Parent method
9579 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
9580 OO.ui.Element.prototype.setElementGroup.call( this, group );
9581
9582 // Initialize item disabled states
9583 this.updateDisabled();
9584
9585 return this;
9586 };
9587
9588 /**
9589 * OutlineControlsWidget is a set of controls for an {@link OO.ui.OutlineSelectWidget outline select widget}.
9590 * Controls include moving items up and down, removing items, and adding different kinds of items.
9591 * ####Currently, this class is only used by {@link OO.ui.BookletLayout BookletLayouts}.####
9592 *
9593 * @class
9594 * @extends OO.ui.Widget
9595 * @mixins OO.ui.GroupElement
9596 * @mixins OO.ui.IconElement
9597 *
9598 * @constructor
9599 * @param {OO.ui.OutlineSelectWidget} outline Outline to control
9600 * @param {Object} [config] Configuration options
9601 */
9602 OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
9603 // Allow passing positional parameters inside the config object
9604 if ( OO.isPlainObject( outline ) && config === undefined ) {
9605 config = outline;
9606 outline = config.outline;
9607 }
9608
9609 // Configuration initialization
9610 config = $.extend( { icon: 'add' }, config );
9611
9612 // Parent constructor
9613 OO.ui.OutlineControlsWidget.super.call( this, config );
9614
9615 // Mixin constructors
9616 OO.ui.GroupElement.call( this, config );
9617 OO.ui.IconElement.call( this, config );
9618
9619 // Properties
9620 this.outline = outline;
9621 this.$movers = $( '<div>' );
9622 this.upButton = new OO.ui.ButtonWidget( {
9623 framed: false,
9624 icon: 'collapse',
9625 title: OO.ui.msg( 'ooui-outline-control-move-up' )
9626 } );
9627 this.downButton = new OO.ui.ButtonWidget( {
9628 framed: false,
9629 icon: 'expand',
9630 title: OO.ui.msg( 'ooui-outline-control-move-down' )
9631 } );
9632 this.removeButton = new OO.ui.ButtonWidget( {
9633 framed: false,
9634 icon: 'remove',
9635 title: OO.ui.msg( 'ooui-outline-control-remove' )
9636 } );
9637
9638 // Events
9639 outline.connect( this, {
9640 select: 'onOutlineChange',
9641 add: 'onOutlineChange',
9642 remove: 'onOutlineChange'
9643 } );
9644 this.upButton.connect( this, { click: [ 'emit', 'move', -1 ] } );
9645 this.downButton.connect( this, { click: [ 'emit', 'move', 1 ] } );
9646 this.removeButton.connect( this, { click: [ 'emit', 'remove' ] } );
9647
9648 // Initialization
9649 this.$element.addClass( 'oo-ui-outlineControlsWidget' );
9650 this.$group.addClass( 'oo-ui-outlineControlsWidget-items' );
9651 this.$movers
9652 .addClass( 'oo-ui-outlineControlsWidget-movers' )
9653 .append( this.removeButton.$element, this.upButton.$element, this.downButton.$element );
9654 this.$element.append( this.$icon, this.$group, this.$movers );
9655 };
9656
9657 /* Setup */
9658
9659 OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget );
9660 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.GroupElement );
9661 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.IconElement );
9662
9663 /* Events */
9664
9665 /**
9666 * @event move
9667 * @param {number} places Number of places to move
9668 */
9669
9670 /**
9671 * @event remove
9672 */
9673
9674 /* Methods */
9675
9676 /**
9677 *
9678 * @private
9679 * Handle outline change events.
9680 */
9681 OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () {
9682 var i, len, firstMovable, lastMovable,
9683 items = this.outline.getItems(),
9684 selectedItem = this.outline.getSelectedItem(),
9685 movable = selectedItem && selectedItem.isMovable(),
9686 removable = selectedItem && selectedItem.isRemovable();
9687
9688 if ( movable ) {
9689 i = -1;
9690 len = items.length;
9691 while ( ++i < len ) {
9692 if ( items[ i ].isMovable() ) {
9693 firstMovable = items[ i ];
9694 break;
9695 }
9696 }
9697 i = len;
9698 while ( i-- ) {
9699 if ( items[ i ].isMovable() ) {
9700 lastMovable = items[ i ];
9701 break;
9702 }
9703 }
9704 }
9705 this.upButton.setDisabled( !movable || selectedItem === firstMovable );
9706 this.downButton.setDisabled( !movable || selectedItem === lastMovable );
9707 this.removeButton.setDisabled( !removable );
9708 };
9709
9710 /**
9711 * ToggleWidget is mixed into other classes to create widgets with an on/off state.
9712 * Please see OO.ui.ToggleButtonWidget and OO.ui.ToggleSwitchWidget for examples.
9713 *
9714 * @abstract
9715 * @class
9716 *
9717 * @constructor
9718 * @param {Object} [config] Configuration options
9719 * @cfg {boolean} [value=false] The toggle’s initial on/off state.
9720 * By default, the toggle is in the 'off' state.
9721 */
9722 OO.ui.ToggleWidget = function OoUiToggleWidget( config ) {
9723 // Configuration initialization
9724 config = config || {};
9725
9726 // Properties
9727 this.value = null;
9728
9729 // Initialization
9730 this.$element.addClass( 'oo-ui-toggleWidget' );
9731 this.setValue( !!config.value );
9732 };
9733
9734 /* Events */
9735
9736 /**
9737 * @event change
9738 *
9739 * A change event is emitted when the on/off state of the toggle changes.
9740 *
9741 * @param {boolean} value Value representing the new state of the toggle
9742 */
9743
9744 /* Methods */
9745
9746 /**
9747 * Get the value representing the toggle’s state.
9748 *
9749 * @return {boolean} The on/off state of the toggle
9750 */
9751 OO.ui.ToggleWidget.prototype.getValue = function () {
9752 return this.value;
9753 };
9754
9755 /**
9756 * Set the state of the toggle: `true` for 'on', `false' for 'off'.
9757 *
9758 * @param {boolean} value The state of the toggle
9759 * @fires change
9760 * @chainable
9761 */
9762 OO.ui.ToggleWidget.prototype.setValue = function ( value ) {
9763 value = !!value;
9764 if ( this.value !== value ) {
9765 this.value = value;
9766 this.emit( 'change', value );
9767 this.$element.toggleClass( 'oo-ui-toggleWidget-on', value );
9768 this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value );
9769 this.$element.attr( 'aria-checked', value.toString() );
9770 }
9771 return this;
9772 };
9773
9774 /**
9775 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
9776 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
9777 * removed, and cleared from the group.
9778 *
9779 * @example
9780 * // Example: A ButtonGroupWidget with two buttons
9781 * var button1 = new OO.ui.PopupButtonWidget( {
9782 * label : 'Select a category',
9783 * icon : 'menu',
9784 * popup : {
9785 * $content: $( '<p>List of categories...</p>' ),
9786 * padded: true,
9787 * align: 'left'
9788 * }
9789 * } );
9790 * var button2 = new OO.ui.ButtonWidget( {
9791 * label : 'Add item'
9792 * });
9793 * var buttonGroup = new OO.ui.ButtonGroupWidget( {
9794 * items: [button1, button2]
9795 * } );
9796 * $('body').append(buttonGroup.$element);
9797 *
9798 * @class
9799 * @extends OO.ui.Widget
9800 * @mixins OO.ui.GroupElement
9801 *
9802 * @constructor
9803 * @param {Object} [config] Configuration options
9804 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
9805 */
9806 OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
9807 // Configuration initialization
9808 config = config || {};
9809
9810 // Parent constructor
9811 OO.ui.ButtonGroupWidget.super.call( this, config );
9812
9813 // Mixin constructors
9814 OO.ui.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
9815
9816 // Initialization
9817 this.$element.addClass( 'oo-ui-buttonGroupWidget' );
9818 if ( Array.isArray( config.items ) ) {
9819 this.addItems( config.items );
9820 }
9821 };
9822
9823 /* Setup */
9824
9825 OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget );
9826 OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.GroupElement );
9827
9828 /**
9829 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
9830 * feels, and functionality can be customized via the class’s configuration options
9831 * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
9832 * and examples.
9833 *
9834 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
9835 *
9836 * @example
9837 * // A button widget
9838 * var button = new OO.ui.ButtonWidget( {
9839 * label : 'Button with Icon',
9840 * icon : 'remove',
9841 * iconTitle : 'Remove'
9842 * } );
9843 * $( 'body' ).append( button.$element );
9844 *
9845 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
9846 *
9847 * @class
9848 * @extends OO.ui.Widget
9849 * @mixins OO.ui.ButtonElement
9850 * @mixins OO.ui.IconElement
9851 * @mixins OO.ui.IndicatorElement
9852 * @mixins OO.ui.LabelElement
9853 * @mixins OO.ui.TitledElement
9854 * @mixins OO.ui.FlaggedElement
9855 * @mixins OO.ui.TabIndexedElement
9856 *
9857 * @constructor
9858 * @param {Object} [config] Configuration options
9859 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
9860 * @cfg {string} [target] The frame or window in which to open the hyperlink.
9861 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
9862 */
9863 OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
9864 // Configuration initialization
9865 // FIXME: The `nofollow` alias is deprecated and will be removed (T89767)
9866 config = $.extend( { noFollow: config && config.nofollow }, config );
9867
9868 // Parent constructor
9869 OO.ui.ButtonWidget.super.call( this, config );
9870
9871 // Mixin constructors
9872 OO.ui.ButtonElement.call( this, config );
9873 OO.ui.IconElement.call( this, config );
9874 OO.ui.IndicatorElement.call( this, config );
9875 OO.ui.LabelElement.call( this, config );
9876 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
9877 OO.ui.FlaggedElement.call( this, config );
9878 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
9879
9880 // Properties
9881 this.href = null;
9882 this.target = null;
9883 this.noFollow = false;
9884 this.isHyperlink = false;
9885
9886 // Initialization
9887 this.$button.append( this.$icon, this.$label, this.$indicator );
9888 this.$element
9889 .addClass( 'oo-ui-buttonWidget' )
9890 .append( this.$button );
9891 this.setHref( config.href );
9892 this.setTarget( config.target );
9893 this.setNoFollow( config.noFollow );
9894 };
9895
9896 /* Setup */
9897
9898 OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget );
9899 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.ButtonElement );
9900 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IconElement );
9901 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IndicatorElement );
9902 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.LabelElement );
9903 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TitledElement );
9904 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.FlaggedElement );
9905 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TabIndexedElement );
9906
9907 /* Methods */
9908
9909 /**
9910 * @inheritdoc
9911 */
9912 OO.ui.ButtonWidget.prototype.onMouseDown = function ( e ) {
9913 if ( !this.isDisabled() ) {
9914 // Remove the tab-index while the button is down to prevent the button from stealing focus
9915 this.$button.removeAttr( 'tabindex' );
9916 }
9917
9918 return OO.ui.ButtonElement.prototype.onMouseDown.call( this, e );
9919 };
9920
9921 /**
9922 * @inheritdoc
9923 */
9924 OO.ui.ButtonWidget.prototype.onMouseUp = function ( e ) {
9925 if ( !this.isDisabled() ) {
9926 // Restore the tab-index after the button is up to restore the button's accessibility
9927 this.$button.attr( 'tabindex', this.tabIndex );
9928 }
9929
9930 return OO.ui.ButtonElement.prototype.onMouseUp.call( this, e );
9931 };
9932
9933 /**
9934 * @inheritdoc
9935 */
9936 OO.ui.ButtonWidget.prototype.onClick = function ( e ) {
9937 var ret = OO.ui.ButtonElement.prototype.onClick.call( this, e );
9938 if ( this.isHyperlink ) {
9939 return true;
9940 }
9941 return ret;
9942 };
9943
9944 /**
9945 * @inheritdoc
9946 */
9947 OO.ui.ButtonWidget.prototype.onKeyPress = function ( e ) {
9948 var ret = OO.ui.ButtonElement.prototype.onKeyPress.call( this, e );
9949 if ( this.isHyperlink ) {
9950 return true;
9951 }
9952 return ret;
9953 };
9954
9955 /**
9956 * Get hyperlink location.
9957 *
9958 * @return {string} Hyperlink location
9959 */
9960 OO.ui.ButtonWidget.prototype.getHref = function () {
9961 return this.href;
9962 };
9963
9964 /**
9965 * Get hyperlink target.
9966 *
9967 * @return {string} Hyperlink target
9968 */
9969 OO.ui.ButtonWidget.prototype.getTarget = function () {
9970 return this.target;
9971 };
9972
9973 /**
9974 * Get search engine traversal hint.
9975 *
9976 * @return {boolean} Whether search engines should avoid traversing this hyperlink
9977 */
9978 OO.ui.ButtonWidget.prototype.getNoFollow = function () {
9979 return this.noFollow;
9980 };
9981
9982 /**
9983 * Set hyperlink location.
9984 *
9985 * @param {string|null} href Hyperlink location, null to remove
9986 */
9987 OO.ui.ButtonWidget.prototype.setHref = function ( href ) {
9988 href = typeof href === 'string' ? href : null;
9989
9990 if ( href !== this.href ) {
9991 this.href = href;
9992 if ( href !== null ) {
9993 this.$button.attr( 'href', href );
9994 this.isHyperlink = true;
9995 } else {
9996 this.$button.removeAttr( 'href' );
9997 this.isHyperlink = false;
9998 }
9999 }
10000
10001 return this;
10002 };
10003
10004 /**
10005 * Set hyperlink target.
10006 *
10007 * @param {string|null} target Hyperlink target, null to remove
10008 */
10009 OO.ui.ButtonWidget.prototype.setTarget = function ( target ) {
10010 target = typeof target === 'string' ? target : null;
10011
10012 if ( target !== this.target ) {
10013 this.target = target;
10014 if ( target !== null ) {
10015 this.$button.attr( 'target', target );
10016 } else {
10017 this.$button.removeAttr( 'target' );
10018 }
10019 }
10020
10021 return this;
10022 };
10023
10024 /**
10025 * Set search engine traversal hint.
10026 *
10027 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
10028 */
10029 OO.ui.ButtonWidget.prototype.setNoFollow = function ( noFollow ) {
10030 noFollow = typeof noFollow === 'boolean' ? noFollow : true;
10031
10032 if ( noFollow !== this.noFollow ) {
10033 this.noFollow = noFollow;
10034 if ( noFollow ) {
10035 this.$button.attr( 'rel', 'nofollow' );
10036 } else {
10037 this.$button.removeAttr( 'rel' );
10038 }
10039 }
10040
10041 return this;
10042 };
10043
10044 /**
10045 * An ActionWidget is a {@link OO.ui.ButtonWidget button widget} that executes an action.
10046 * Action widgets are used with OO.ui.ActionSet, which manages the behavior and availability
10047 * of the actions.
10048 *
10049 * Both actions and action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
10050 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information
10051 * and examples.
10052 *
10053 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
10054 *
10055 * @class
10056 * @extends OO.ui.ButtonWidget
10057 * @mixins OO.ui.PendingElement
10058 *
10059 * @constructor
10060 * @param {Object} [config] Configuration options
10061 * @cfg {string} [action] Symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
10062 * @cfg {string[]} [modes] Symbolic names of the modes (e.g., ‘edit’ or ‘read’) in which the action
10063 * should be made available. See the action set's {@link OO.ui.ActionSet#setMode setMode} method
10064 * for more information about setting modes.
10065 * @cfg {boolean} [framed=false] Render the action button with a frame
10066 */
10067 OO.ui.ActionWidget = function OoUiActionWidget( config ) {
10068 // Configuration initialization
10069 config = $.extend( { framed: false }, config );
10070
10071 // Parent constructor
10072 OO.ui.ActionWidget.super.call( this, config );
10073
10074 // Mixin constructors
10075 OO.ui.PendingElement.call( this, config );
10076
10077 // Properties
10078 this.action = config.action || '';
10079 this.modes = config.modes || [];
10080 this.width = 0;
10081 this.height = 0;
10082
10083 // Initialization
10084 this.$element.addClass( 'oo-ui-actionWidget' );
10085 };
10086
10087 /* Setup */
10088
10089 OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget );
10090 OO.mixinClass( OO.ui.ActionWidget, OO.ui.PendingElement );
10091
10092 /* Events */
10093
10094 /**
10095 * A resize event is emitted when the size of the widget changes.
10096 *
10097 * @event resize
10098 */
10099
10100 /* Methods */
10101
10102 /**
10103 * Check if the action is configured to be available in the specified `mode`.
10104 *
10105 * @param {string} mode Name of mode
10106 * @return {boolean} The action is configured with the mode
10107 */
10108 OO.ui.ActionWidget.prototype.hasMode = function ( mode ) {
10109 return this.modes.indexOf( mode ) !== -1;
10110 };
10111
10112 /**
10113 * Get the symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
10114 *
10115 * @return {string}
10116 */
10117 OO.ui.ActionWidget.prototype.getAction = function () {
10118 return this.action;
10119 };
10120
10121 /**
10122 * Get the symbolic name of the mode or modes for which the action is configured to be available.
10123 *
10124 * The current mode is set with the action set's {@link OO.ui.ActionSet#setMode setMode} method.
10125 * Only actions that are configured to be avaiable in the current mode will be visible. All other actions
10126 * are hidden.
10127 *
10128 * @return {string[]}
10129 */
10130 OO.ui.ActionWidget.prototype.getModes = function () {
10131 return this.modes.slice();
10132 };
10133
10134 /**
10135 * Emit a resize event if the size has changed.
10136 *
10137 * @private
10138 * @chainable
10139 */
10140 OO.ui.ActionWidget.prototype.propagateResize = function () {
10141 var width, height;
10142
10143 if ( this.isElementAttached() ) {
10144 width = this.$element.width();
10145 height = this.$element.height();
10146
10147 if ( width !== this.width || height !== this.height ) {
10148 this.width = width;
10149 this.height = height;
10150 this.emit( 'resize' );
10151 }
10152 }
10153
10154 return this;
10155 };
10156
10157 /**
10158 * @inheritdoc
10159 */
10160 OO.ui.ActionWidget.prototype.setIcon = function () {
10161 // Mixin method
10162 OO.ui.IconElement.prototype.setIcon.apply( this, arguments );
10163 this.propagateResize();
10164
10165 return this;
10166 };
10167
10168 /**
10169 * @inheritdoc
10170 */
10171 OO.ui.ActionWidget.prototype.setLabel = function () {
10172 // Mixin method
10173 OO.ui.LabelElement.prototype.setLabel.apply( this, arguments );
10174 this.propagateResize();
10175
10176 return this;
10177 };
10178
10179 /**
10180 * @inheritdoc
10181 */
10182 OO.ui.ActionWidget.prototype.setFlags = function () {
10183 // Mixin method
10184 OO.ui.FlaggedElement.prototype.setFlags.apply( this, arguments );
10185 this.propagateResize();
10186
10187 return this;
10188 };
10189
10190 /**
10191 * @inheritdoc
10192 */
10193 OO.ui.ActionWidget.prototype.clearFlags = function () {
10194 // Mixin method
10195 OO.ui.FlaggedElement.prototype.clearFlags.apply( this, arguments );
10196 this.propagateResize();
10197
10198 return this;
10199 };
10200
10201 /**
10202 * Toggle the visibility of the action button.
10203 *
10204 * @param {boolean} [show] Show button, omit to toggle visibility
10205 * @chainable
10206 */
10207 OO.ui.ActionWidget.prototype.toggle = function () {
10208 // Parent method
10209 OO.ui.ActionWidget.super.prototype.toggle.apply( this, arguments );
10210 this.propagateResize();
10211
10212 return this;
10213 };
10214
10215 /**
10216 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
10217 * which is used to display additional information or options.
10218 *
10219 * @example
10220 * // Example of a popup button.
10221 * var popupButton = new OO.ui.PopupButtonWidget( {
10222 * label: 'Popup button with options',
10223 * icon: 'menu',
10224 * popup: {
10225 * $content: $( '<p>Additional options here.</p>' ),
10226 * padded: true,
10227 * align: 'left'
10228 * }
10229 * } );
10230 * // Append the button to the DOM.
10231 * $( 'body' ).append( popupButton.$element );
10232 *
10233 * @class
10234 * @extends OO.ui.ButtonWidget
10235 * @mixins OO.ui.PopupElement
10236 *
10237 * @constructor
10238 * @param {Object} [config] Configuration options
10239 */
10240 OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
10241 // Parent constructor
10242 OO.ui.PopupButtonWidget.super.call( this, config );
10243
10244 // Mixin constructors
10245 OO.ui.PopupElement.call( this, config );
10246
10247 // Events
10248 this.connect( this, { click: 'onAction' } );
10249
10250 // Initialization
10251 this.$element
10252 .addClass( 'oo-ui-popupButtonWidget' )
10253 .attr( 'aria-haspopup', 'true' )
10254 .append( this.popup.$element );
10255 };
10256
10257 /* Setup */
10258
10259 OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
10260 OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.PopupElement );
10261
10262 /* Methods */
10263
10264 /**
10265 * Handle the button action being triggered.
10266 *
10267 * @private
10268 */
10269 OO.ui.PopupButtonWidget.prototype.onAction = function () {
10270 this.popup.toggle();
10271 };
10272
10273 /**
10274 * ToggleButtons are buttons that have a state (‘on’ or ‘off’) that is represented by a
10275 * Boolean value. Like other {@link OO.ui.ButtonWidget buttons}, toggle buttons can be
10276 * configured with {@link OO.ui.IconElement icons}, {@link OO.ui.IndicatorElement indicators},
10277 * {@link OO.ui.TitledElement titles}, {@link OO.ui.FlaggedElement styling flags},
10278 * and {@link OO.ui.LabelElement labels}. Please see
10279 * the [OOjs UI documentation][1] on MediaWiki for more information.
10280 *
10281 * @example
10282 * // Toggle buttons in the 'off' and 'on' state.
10283 * var toggleButton1 = new OO.ui.ToggleButtonWidget( {
10284 * label: 'Toggle Button off'
10285 * } );
10286 * var toggleButton2 = new OO.ui.ToggleButtonWidget( {
10287 * label: 'Toggle Button on',
10288 * value: true
10289 * } );
10290 * // Append the buttons to the DOM.
10291 * $( 'body' ).append( toggleButton1.$element, toggleButton2.$element );
10292 *
10293 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Toggle_buttons
10294 *
10295 * @class
10296 * @extends OO.ui.ButtonWidget
10297 * @mixins OO.ui.ToggleWidget
10298 *
10299 * @constructor
10300 * @param {Object} [config] Configuration options
10301 * @cfg {boolean} [value=false] The toggle button’s initial on/off
10302 * state. By default, the button is in the 'off' state.
10303 */
10304 OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) {
10305 // Configuration initialization
10306 config = config || {};
10307
10308 // Parent constructor
10309 OO.ui.ToggleButtonWidget.super.call( this, config );
10310
10311 // Mixin constructors
10312 OO.ui.ToggleWidget.call( this, config );
10313
10314 // Events
10315 this.connect( this, { click: 'onAction' } );
10316
10317 // Initialization
10318 this.$element.addClass( 'oo-ui-toggleButtonWidget' );
10319 };
10320
10321 /* Setup */
10322
10323 OO.inheritClass( OO.ui.ToggleButtonWidget, OO.ui.ButtonWidget );
10324 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget );
10325
10326 /* Methods */
10327
10328 /**
10329 *
10330 * @private
10331 * Handle the button action being triggered.
10332 */
10333 OO.ui.ToggleButtonWidget.prototype.onAction = function () {
10334 this.setValue( !this.value );
10335 };
10336
10337 /**
10338 * @inheritdoc
10339 */
10340 OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
10341 value = !!value;
10342 if ( value !== this.value ) {
10343 this.$button.attr( 'aria-pressed', value.toString() );
10344 this.setActive( value );
10345 }
10346
10347 // Parent method (from mixin)
10348 OO.ui.ToggleWidget.prototype.setValue.call( this, value );
10349
10350 return this;
10351 };
10352
10353 /**
10354 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
10355 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
10356 * users can interact with it.
10357 *
10358 * @example
10359 * // Example: A DropdownWidget with a menu that contains three options
10360 * var dropDown=new OO.ui.DropdownWidget( {
10361 * label: 'Dropdown menu: Select a menu option',
10362 * menu: {
10363 * items: [
10364 * new OO.ui.MenuOptionWidget( {
10365 * data: 'a',
10366 * label: 'First'
10367 * } ),
10368 * new OO.ui.MenuOptionWidget( {
10369 * data: 'b',
10370 * label: 'Second'
10371 * } ),
10372 * new OO.ui.MenuOptionWidget( {
10373 * data: 'c',
10374 * label: 'Third'
10375 * } )
10376 * ]
10377 * }
10378 * } );
10379 *
10380 * $('body').append(dropDown.$element);
10381 *
10382 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
10383 *
10384 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
10385 *
10386 * @class
10387 * @extends OO.ui.Widget
10388 * @mixins OO.ui.IconElement
10389 * @mixins OO.ui.IndicatorElement
10390 * @mixins OO.ui.LabelElement
10391 * @mixins OO.ui.TitledElement
10392 * @mixins OO.ui.TabIndexedElement
10393 *
10394 * @constructor
10395 * @param {Object} [config] Configuration options
10396 * @cfg {Object} [menu] Configuration options to pass to menu widget
10397 */
10398 OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
10399 // Configuration initialization
10400 config = $.extend( { indicator: 'down' }, config );
10401
10402 // Parent constructor
10403 OO.ui.DropdownWidget.super.call( this, config );
10404
10405 // Properties (must be set before TabIndexedElement constructor call)
10406 this.$handle = this.$( '<span>' );
10407
10408 // Mixin constructors
10409 OO.ui.IconElement.call( this, config );
10410 OO.ui.IndicatorElement.call( this, config );
10411 OO.ui.LabelElement.call( this, config );
10412 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
10413 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
10414
10415 // Properties
10416 this.menu = new OO.ui.MenuSelectWidget( $.extend( { widget: this }, config.menu ) );
10417
10418 // Events
10419 this.$handle.on( {
10420 click: this.onClick.bind( this ),
10421 keypress: this.onKeyPress.bind( this )
10422 } );
10423 this.menu.connect( this, { select: 'onMenuSelect' } );
10424
10425 // Initialization
10426 this.$handle
10427 .addClass( 'oo-ui-dropdownWidget-handle' )
10428 .append( this.$icon, this.$label, this.$indicator );
10429 this.$element
10430 .addClass( 'oo-ui-dropdownWidget' )
10431 .append( this.$handle, this.menu.$element );
10432 };
10433
10434 /* Setup */
10435
10436 OO.inheritClass( OO.ui.DropdownWidget, OO.ui.Widget );
10437 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.IconElement );
10438 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.IndicatorElement );
10439 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.LabelElement );
10440 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.TitledElement );
10441 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.TabIndexedElement );
10442
10443 /* Methods */
10444
10445 /**
10446 * Get the menu.
10447 *
10448 * @return {OO.ui.MenuSelectWidget} Menu of widget
10449 */
10450 OO.ui.DropdownWidget.prototype.getMenu = function () {
10451 return this.menu;
10452 };
10453
10454 /**
10455 * Handles menu select events.
10456 *
10457 * @private
10458 * @param {OO.ui.MenuOptionWidget} item Selected menu item
10459 */
10460 OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
10461 var selectedLabel;
10462
10463 if ( !item ) {
10464 return;
10465 }
10466
10467 selectedLabel = item.getLabel();
10468
10469 // If the label is a DOM element, clone it, because setLabel will append() it
10470 if ( selectedLabel instanceof jQuery ) {
10471 selectedLabel = selectedLabel.clone();
10472 }
10473
10474 this.setLabel( selectedLabel );
10475 };
10476
10477 /**
10478 * Handle mouse click events.
10479 *
10480 * @private
10481 * @param {jQuery.Event} e Mouse click event
10482 */
10483 OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
10484 if ( !this.isDisabled() && e.which === 1 ) {
10485 this.menu.toggle();
10486 }
10487 return false;
10488 };
10489
10490 /**
10491 * Handle key press events.
10492 *
10493 * @private
10494 * @param {jQuery.Event} e Key press event
10495 */
10496 OO.ui.DropdownWidget.prototype.onKeyPress = function ( e ) {
10497 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
10498 this.menu.toggle();
10499 return false;
10500 }
10501 };
10502
10503 /**
10504 * IconWidget is a generic widget for {@link OO.ui.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
10505 * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
10506 * for a list of icons included in the library.
10507 *
10508 * @example
10509 * // An icon widget with a label
10510 * var myIcon = new OO.ui.IconWidget({
10511 * icon: 'help',
10512 * iconTitle: 'Help'
10513 * });
10514 * // Create a label.
10515 * var iconLabel = new OO.ui.LabelWidget({
10516 * label: 'Help'
10517 * });
10518 * $('body').append(myIcon.$element, iconLabel.$element);
10519 *
10520 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
10521 *
10522 * @class
10523 * @extends OO.ui.Widget
10524 * @mixins OO.ui.IconElement
10525 * @mixins OO.ui.TitledElement
10526 *
10527 * @constructor
10528 * @param {Object} [config] Configuration options
10529 */
10530 OO.ui.IconWidget = function OoUiIconWidget( config ) {
10531 // Configuration initialization
10532 config = config || {};
10533
10534 // Parent constructor
10535 OO.ui.IconWidget.super.call( this, config );
10536
10537 // Mixin constructors
10538 OO.ui.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) );
10539 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
10540
10541 // Initialization
10542 this.$element.addClass( 'oo-ui-iconWidget' );
10543 };
10544
10545 /* Setup */
10546
10547 OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
10548 OO.mixinClass( OO.ui.IconWidget, OO.ui.IconElement );
10549 OO.mixinClass( OO.ui.IconWidget, OO.ui.TitledElement );
10550
10551 /* Static Properties */
10552
10553 OO.ui.IconWidget.static.tagName = 'span';
10554
10555 /**
10556 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
10557 * attention to the status of an item or to clarify the function of a control. For a list of
10558 * indicators included in the library, please see the [OOjs UI documentation on MediaWiki][1].
10559 *
10560 * @example
10561 * // Example of an indicator widget
10562 * var indicator1 = new OO.ui.IndicatorWidget( {
10563 * indicator: 'alert'
10564 * });
10565 *
10566 * // Create a fieldset layout to add a label
10567 * var fieldset = new OO.ui.FieldsetLayout( );
10568 * fieldset.addItems( [
10569 * new OO.ui.FieldLayout( indicator1, {label: 'An alert indicator:'} )
10570 * ] );
10571 * $( 'body' ).append( fieldset.$element );
10572 *
10573 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
10574 *
10575 * @class
10576 * @extends OO.ui.Widget
10577 * @mixins OO.ui.IndicatorElement
10578 * @mixins OO.ui.TitledElement
10579 *
10580 * @constructor
10581 * @param {Object} [config] Configuration options
10582 */
10583 OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
10584 // Configuration initialization
10585 config = config || {};
10586
10587 // Parent constructor
10588 OO.ui.IndicatorWidget.super.call( this, config );
10589
10590 // Mixin constructors
10591 OO.ui.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) );
10592 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
10593
10594 // Initialization
10595 this.$element.addClass( 'oo-ui-indicatorWidget' );
10596 };
10597
10598 /* Setup */
10599
10600 OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
10601 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.IndicatorElement );
10602 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.TitledElement );
10603
10604 /* Static Properties */
10605
10606 OO.ui.IndicatorWidget.static.tagName = 'span';
10607
10608 /**
10609 * InputWidget is the base class for all input widgets, which
10610 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
10611 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
10612 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
10613 *
10614 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
10615 *
10616 * @abstract
10617 * @class
10618 * @extends OO.ui.Widget
10619 * @mixins OO.ui.FlaggedElement
10620 * @mixins OO.ui.TabIndexedElement
10621 *
10622 * @constructor
10623 * @param {Object} [config] Configuration options
10624 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
10625 * @cfg {string} [value=''] The value of the input.
10626 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
10627 * before it is accepted.
10628 */
10629 OO.ui.InputWidget = function OoUiInputWidget( config ) {
10630 // Configuration initialization
10631 config = config || {};
10632
10633 // Parent constructor
10634 OO.ui.InputWidget.super.call( this, config );
10635
10636 // Properties
10637 this.$input = this.getInputElement( config );
10638 this.value = '';
10639 this.inputFilter = config.inputFilter;
10640
10641 // Mixin constructors
10642 OO.ui.FlaggedElement.call( this, config );
10643 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$input } ) );
10644
10645 // Events
10646 this.$input.on( 'keydown mouseup cut paste change input select', this.onEdit.bind( this ) );
10647
10648 // Initialization
10649 this.$input
10650 .attr( 'name', config.name )
10651 .prop( 'disabled', this.isDisabled() );
10652 this.$element.addClass( 'oo-ui-inputWidget' ).append( this.$input, $( '<span>' ) );
10653 this.setValue( config.value );
10654 };
10655
10656 /* Setup */
10657
10658 OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
10659 OO.mixinClass( OO.ui.InputWidget, OO.ui.FlaggedElement );
10660 OO.mixinClass( OO.ui.InputWidget, OO.ui.TabIndexedElement );
10661
10662 /* Events */
10663
10664 /**
10665 * @event change
10666 *
10667 * A change event is emitted when the value of the input changes.
10668 *
10669 * @param {string} value
10670 */
10671
10672 /* Methods */
10673
10674 /**
10675 * Get input element.
10676 *
10677 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
10678 * different circumstances. The element must have a `value` property (like form elements).
10679 *
10680 * @private
10681 * @param {Object} config Configuration options
10682 * @return {jQuery} Input element
10683 */
10684 OO.ui.InputWidget.prototype.getInputElement = function () {
10685 return $( '<input>' );
10686 };
10687
10688 /**
10689 * Handle potentially value-changing events.
10690 *
10691 * @private
10692 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
10693 */
10694 OO.ui.InputWidget.prototype.onEdit = function () {
10695 var widget = this;
10696 if ( !this.isDisabled() ) {
10697 // Allow the stack to clear so the value will be updated
10698 setTimeout( function () {
10699 widget.setValue( widget.$input.val() );
10700 } );
10701 }
10702 };
10703
10704 /**
10705 * Get the value of the input.
10706 *
10707 * @return {string} Input value
10708 */
10709 OO.ui.InputWidget.prototype.getValue = function () {
10710 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
10711 // it, and we won't know unless they're kind enough to trigger a 'change' event.
10712 var value = this.$input.val();
10713 if ( this.value !== value ) {
10714 this.setValue( value );
10715 }
10716 return this.value;
10717 };
10718
10719 /**
10720 * Set the direction of the input, either RTL (right-to-left) or LTR (left-to-right).
10721 *
10722 * @param {boolean} isRTL
10723 * Direction is right-to-left
10724 */
10725 OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
10726 this.$input.prop( 'dir', isRTL ? 'rtl' : 'ltr' );
10727 };
10728
10729 /**
10730 * Set the value of the input.
10731 *
10732 * @param {string} value New value
10733 * @fires change
10734 * @chainable
10735 */
10736 OO.ui.InputWidget.prototype.setValue = function ( value ) {
10737 value = this.cleanUpValue( value );
10738 // Update the DOM if it has changed. Note that with cleanUpValue, it
10739 // is possible for the DOM value to change without this.value changing.
10740 if ( this.$input.val() !== value ) {
10741 this.$input.val( value );
10742 }
10743 if ( this.value !== value ) {
10744 this.value = value;
10745 this.emit( 'change', this.value );
10746 }
10747 return this;
10748 };
10749
10750 /**
10751 * Clean up incoming value.
10752 *
10753 * Ensures value is a string, and converts undefined and null to empty string.
10754 *
10755 * @private
10756 * @param {string} value Original value
10757 * @return {string} Cleaned up value
10758 */
10759 OO.ui.InputWidget.prototype.cleanUpValue = function ( value ) {
10760 if ( value === undefined || value === null ) {
10761 return '';
10762 } else if ( this.inputFilter ) {
10763 return this.inputFilter( String( value ) );
10764 } else {
10765 return String( value );
10766 }
10767 };
10768
10769 /**
10770 * Simulate the behavior of clicking on a label bound to this input. This method is only called by
10771 * {@link OO.ui.LabelWidget LabelWidget} and {@link OO.ui.FieldLayout FieldLayout}. It should not be
10772 * called directly.
10773 */
10774 OO.ui.InputWidget.prototype.simulateLabelClick = function () {
10775 if ( !this.isDisabled() ) {
10776 if ( this.$input.is( ':checkbox, :radio' ) ) {
10777 this.$input.click();
10778 }
10779 if ( this.$input.is( ':input' ) ) {
10780 this.$input[ 0 ].focus();
10781 }
10782 }
10783 };
10784
10785 /**
10786 * @inheritdoc
10787 */
10788 OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
10789 OO.ui.InputWidget.super.prototype.setDisabled.call( this, state );
10790 if ( this.$input ) {
10791 this.$input.prop( 'disabled', this.isDisabled() );
10792 }
10793 return this;
10794 };
10795
10796 /**
10797 * Focus the input.
10798 *
10799 * @chainable
10800 */
10801 OO.ui.InputWidget.prototype.focus = function () {
10802 this.$input[ 0 ].focus();
10803 return this;
10804 };
10805
10806 /**
10807 * Blur the input.
10808 *
10809 * @chainable
10810 */
10811 OO.ui.InputWidget.prototype.blur = function () {
10812 this.$input[ 0 ].blur();
10813 return this;
10814 };
10815
10816 /**
10817 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
10818 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
10819 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
10820 * HTML `<button/>` (the default) or an HTML `<input/>` tags. See the
10821 * [OOjs UI documentation on MediaWiki] [1] for more information.
10822 *
10823 * @example
10824 * // A ButtonInputWidget rendered as an HTML button, the default.
10825 * var button = new OO.ui.ButtonInputWidget( {
10826 * label: 'Input button',
10827 * icon: 'check',
10828 * value: 'check'
10829 * } );
10830 * $( 'body' ).append( button.$element );
10831 *
10832 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
10833 *
10834 * @class
10835 * @extends OO.ui.InputWidget
10836 * @mixins OO.ui.ButtonElement
10837 * @mixins OO.ui.IconElement
10838 * @mixins OO.ui.IndicatorElement
10839 * @mixins OO.ui.LabelElement
10840 * @mixins OO.ui.TitledElement
10841 * @mixins OO.ui.FlaggedElement
10842 *
10843 * @constructor
10844 * @param {Object} [config] Configuration options
10845 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
10846 * @cfg {boolean} [useInputTag=false] Use an `<input/>` tag instead of a `<button/>` tag, the default.
10847 * Widgets configured to be an `<input/>` do not support {@link #icon icons} and {@link #indicator indicators},
10848 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
10849 * be set to `true` when there’s need to support IE6 in a form with multiple buttons.
10850 */
10851 OO.ui.ButtonInputWidget = function OoUiButtonInputWidget( config ) {
10852 // Configuration initialization
10853 config = $.extend( { type: 'button', useInputTag: false }, config );
10854
10855 // Properties (must be set before parent constructor, which calls #setValue)
10856 this.useInputTag = config.useInputTag;
10857
10858 // Parent constructor
10859 OO.ui.ButtonInputWidget.super.call( this, config );
10860
10861 // Mixin constructors
10862 OO.ui.ButtonElement.call( this, $.extend( {}, config, { $button: this.$input } ) );
10863 OO.ui.IconElement.call( this, config );
10864 OO.ui.IndicatorElement.call( this, config );
10865 OO.ui.LabelElement.call( this, config );
10866 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
10867 OO.ui.FlaggedElement.call( this, config );
10868
10869 // Initialization
10870 if ( !config.useInputTag ) {
10871 this.$input.append( this.$icon, this.$label, this.$indicator );
10872 }
10873 this.$element.addClass( 'oo-ui-buttonInputWidget' );
10874 };
10875
10876 /* Setup */
10877
10878 OO.inheritClass( OO.ui.ButtonInputWidget, OO.ui.InputWidget );
10879 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.ButtonElement );
10880 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.IconElement );
10881 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.IndicatorElement );
10882 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.LabelElement );
10883 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.TitledElement );
10884 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.FlaggedElement );
10885
10886 /* Methods */
10887
10888 /**
10889 * @inheritdoc
10890 * @private
10891 */
10892 OO.ui.ButtonInputWidget.prototype.getInputElement = function ( config ) {
10893 var html = '<' + ( config.useInputTag ? 'input' : 'button' ) + ' type="' + config.type + '">';
10894 return $( html );
10895 };
10896
10897 /**
10898 * Set label value.
10899 *
10900 * If #useInputTag is `true`, the label is set as the `value` of the `<input/>` tag.
10901 *
10902 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
10903 * text, or `null` for no label
10904 * @chainable
10905 */
10906 OO.ui.ButtonInputWidget.prototype.setLabel = function ( label ) {
10907 OO.ui.LabelElement.prototype.setLabel.call( this, label );
10908
10909 if ( this.useInputTag ) {
10910 if ( typeof label === 'function' ) {
10911 label = OO.ui.resolveMsg( label );
10912 }
10913 if ( label instanceof jQuery ) {
10914 label = label.text();
10915 }
10916 if ( !label ) {
10917 label = '';
10918 }
10919 this.$input.val( label );
10920 }
10921
10922 return this;
10923 };
10924
10925 /**
10926 * Set the value of the input.
10927 *
10928 * This method is disabled for button inputs configured as {@link #useInputTag <input/> tags}, as
10929 * they do not support {@link #value values}.
10930 *
10931 * @param {string} value New value
10932 * @chainable
10933 */
10934 OO.ui.ButtonInputWidget.prototype.setValue = function ( value ) {
10935 if ( !this.useInputTag ) {
10936 OO.ui.ButtonInputWidget.super.prototype.setValue.call( this, value );
10937 }
10938 return this;
10939 };
10940
10941 /**
10942 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
10943 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
10944 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
10945 * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
10946 *
10947 * @example
10948 * // An example of selected, unselected, and disabled checkbox inputs
10949 * var checkbox1=new OO.ui.CheckboxInputWidget({
10950 * value: 'a',
10951 * selected: true
10952 * });
10953 * var checkbox2=new OO.ui.CheckboxInputWidget({
10954 * value: 'b'
10955 * });
10956 * var checkbox3=new OO.ui.CheckboxInputWidget( {
10957 * value:'c',
10958 * disabled: true
10959 * } );
10960 * // Create a fieldset layout with fields for each checkbox.
10961 * var fieldset = new OO.ui.FieldsetLayout( {
10962 * label: 'Checkboxes'
10963 * } );
10964 * fieldset.addItems( [
10965 * new OO.ui.FieldLayout( checkbox1, {label : 'Selected checkbox', align : 'inline'}),
10966 * new OO.ui.FieldLayout( checkbox2, {label : 'Unselected checkbox', align : 'inline'}),
10967 * new OO.ui.FieldLayout( checkbox3, {label : 'Disabled checkbox', align : 'inline'}),
10968 * ] );
10969 * $( 'body' ).append( fieldset.$element );
10970 *
10971 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
10972 *
10973 * @class
10974 * @extends OO.ui.InputWidget
10975 *
10976 * @constructor
10977 * @param {Object} [config] Configuration options
10978 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
10979 */
10980 OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
10981 // Configuration initialization
10982 config = config || {};
10983
10984 // Parent constructor
10985 OO.ui.CheckboxInputWidget.super.call( this, config );
10986
10987 // Initialization
10988 this.$element.addClass( 'oo-ui-checkboxInputWidget' );
10989 this.setSelected( config.selected !== undefined ? config.selected : false );
10990 };
10991
10992 /* Setup */
10993
10994 OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
10995
10996 /* Methods */
10997
10998 /**
10999 * @inheritdoc
11000 * @private
11001 */
11002 OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
11003 return $( '<input type="checkbox" />' );
11004 };
11005
11006 /**
11007 * @inheritdoc
11008 */
11009 OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
11010 var widget = this;
11011 if ( !this.isDisabled() ) {
11012 // Allow the stack to clear so the value will be updated
11013 setTimeout( function () {
11014 widget.setSelected( widget.$input.prop( 'checked' ) );
11015 } );
11016 }
11017 };
11018
11019 /**
11020 * Set selection state of this checkbox.
11021 *
11022 * @param {boolean} state `true` for selected
11023 * @chainable
11024 */
11025 OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state ) {
11026 state = !!state;
11027 if ( this.selected !== state ) {
11028 this.selected = state;
11029 this.$input.prop( 'checked', this.selected );
11030 this.emit( 'change', this.selected );
11031 }
11032 return this;
11033 };
11034
11035 /**
11036 * Check if this checkbox is selected.
11037 *
11038 * @return {boolean} Checkbox is selected
11039 */
11040 OO.ui.CheckboxInputWidget.prototype.isSelected = function () {
11041 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
11042 // it, and we won't know unless they're kind enough to trigger a 'change' event.
11043 var selected = this.$input.prop( 'checked' );
11044 if ( this.selected !== selected ) {
11045 this.setSelected( selected );
11046 }
11047 return this.selected;
11048 };
11049
11050 /**
11051 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
11052 * within a {@link OO.ui.FormLayout form}. The selected value is synchronized with the value
11053 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
11054 * more information about input widgets.
11055 *
11056 * @example
11057 * // Example: A DropdownInputWidget with three options
11058 * var dropDown=new OO.ui.DropdownInputWidget( {
11059 * label: 'Dropdown menu: Select a menu option',
11060 * options: [
11061 * { data: 'a', label: 'First' } ,
11062 * { data: 'b', label: 'Second'} ,
11063 * { data: 'c', label: 'Third' }
11064 * ]
11065 * } );
11066 * $('body').append(dropDown.$element);
11067 *
11068 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
11069 *
11070 * @class
11071 * @extends OO.ui.InputWidget
11072 *
11073 * @constructor
11074 * @param {Object} [config] Configuration options
11075 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
11076 */
11077 OO.ui.DropdownInputWidget = function OoUiDropdownInputWidget( config ) {
11078 // Configuration initialization
11079 config = config || {};
11080
11081 // Properties (must be done before parent constructor which calls #setDisabled)
11082 this.dropdownWidget = new OO.ui.DropdownWidget();
11083
11084 // Parent constructor
11085 OO.ui.DropdownInputWidget.super.call( this, config );
11086
11087 // Events
11088 this.dropdownWidget.getMenu().connect( this, { select: 'onMenuSelect' } );
11089
11090 // Initialization
11091 this.setOptions( config.options || [] );
11092 this.$element
11093 .addClass( 'oo-ui-dropdownInputWidget' )
11094 .append( this.dropdownWidget.$element );
11095 };
11096
11097 /* Setup */
11098
11099 OO.inheritClass( OO.ui.DropdownInputWidget, OO.ui.InputWidget );
11100
11101 /* Methods */
11102
11103 /**
11104 * @inheritdoc
11105 * @private
11106 */
11107 OO.ui.DropdownInputWidget.prototype.getInputElement = function () {
11108 return $( '<input type="hidden">' );
11109 };
11110
11111 /**
11112 * Handles menu select events.
11113 *
11114 * @private
11115 * @param {OO.ui.MenuOptionWidget} item Selected menu item
11116 */
11117 OO.ui.DropdownInputWidget.prototype.onMenuSelect = function ( item ) {
11118 this.setValue( item.getData() );
11119 };
11120
11121 /**
11122 * @inheritdoc
11123 */
11124 OO.ui.DropdownInputWidget.prototype.setValue = function ( value ) {
11125 var item = this.dropdownWidget.getMenu().getItemFromData( value );
11126 if ( item ) {
11127 this.dropdownWidget.getMenu().selectItem( item );
11128 }
11129 OO.ui.DropdownInputWidget.super.prototype.setValue.call( this, value );
11130 return this;
11131 };
11132
11133 /**
11134 * @inheritdoc
11135 */
11136 OO.ui.DropdownInputWidget.prototype.setDisabled = function ( state ) {
11137 this.dropdownWidget.setDisabled( state );
11138 OO.ui.DropdownInputWidget.super.prototype.setDisabled.call( this, state );
11139 return this;
11140 };
11141
11142 /**
11143 * Set the options available for this input.
11144 *
11145 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
11146 * @chainable
11147 */
11148 OO.ui.DropdownInputWidget.prototype.setOptions = function ( options ) {
11149 var value = this.getValue();
11150
11151 // Rebuild the dropdown menu
11152 this.dropdownWidget.getMenu()
11153 .clearItems()
11154 .addItems( options.map( function ( opt ) {
11155 return new OO.ui.MenuOptionWidget( {
11156 data: opt.data,
11157 label: opt.label !== undefined ? opt.label : opt.data
11158 } );
11159 } ) );
11160
11161 // Restore the previous value, or reset to something sensible
11162 if ( this.dropdownWidget.getMenu().getItemFromData( value ) ) {
11163 // Previous value is still available, ensure consistency with the dropdown
11164 this.setValue( value );
11165 } else {
11166 // No longer valid, reset
11167 if ( options.length ) {
11168 this.setValue( options[ 0 ].data );
11169 }
11170 }
11171
11172 return this;
11173 };
11174
11175 /**
11176 * @inheritdoc
11177 */
11178 OO.ui.DropdownInputWidget.prototype.focus = function () {
11179 this.dropdownWidget.getMenu().toggle( true );
11180 return this;
11181 };
11182
11183 /**
11184 * @inheritdoc
11185 */
11186 OO.ui.DropdownInputWidget.prototype.blur = function () {
11187 this.dropdownWidget.getMenu().toggle( false );
11188 return this;
11189 };
11190
11191 /**
11192 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
11193 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
11194 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
11195 * please see the [OOjs UI documentation on MediaWiki][1].
11196 *
11197 * @example
11198 * // An example of selected, unselected, and disabled radio inputs
11199 * var radio1=new OO.ui.RadioInputWidget({
11200 * value: 'a',
11201 * selected: true
11202 * });
11203 * var radio2=new OO.ui.RadioInputWidget({
11204 * value: 'b'
11205 * });
11206 * var radio3=new OO.ui.RadioInputWidget( {
11207 * value:'c',
11208 * disabled: true
11209 * } );
11210 * // Create a fieldset layout with fields for each radio button.
11211 * var fieldset = new OO.ui.FieldsetLayout( {
11212 * label: 'Radio inputs'
11213 * } );
11214 * fieldset.addItems( [
11215 * new OO.ui.FieldLayout( radio1, {label : 'Selected', align : 'inline'}),
11216 * new OO.ui.FieldLayout( radio2, {label : 'Unselected', align : 'inline'}),
11217 * new OO.ui.FieldLayout( radio3, {label : 'Disabled', align : 'inline'}),
11218 * ] );
11219 * $( 'body' ).append( fieldset.$element );
11220 *
11221 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
11222 *
11223 * @class
11224 * @extends OO.ui.InputWidget
11225 *
11226 * @constructor
11227 * @param {Object} [config] Configuration options
11228 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
11229 */
11230 OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) {
11231 // Configuration initialization
11232 config = config || {};
11233
11234 // Parent constructor
11235 OO.ui.RadioInputWidget.super.call( this, config );
11236
11237 // Initialization
11238 this.$element.addClass( 'oo-ui-radioInputWidget' );
11239 this.setSelected( config.selected !== undefined ? config.selected : false );
11240 };
11241
11242 /* Setup */
11243
11244 OO.inheritClass( OO.ui.RadioInputWidget, OO.ui.InputWidget );
11245
11246 /* Methods */
11247
11248 /**
11249 * @inheritdoc
11250 * @private
11251 */
11252 OO.ui.RadioInputWidget.prototype.getInputElement = function () {
11253 return $( '<input type="radio" />' );
11254 };
11255
11256 /**
11257 * @inheritdoc
11258 */
11259 OO.ui.RadioInputWidget.prototype.onEdit = function () {
11260 // RadioInputWidget doesn't track its state.
11261 };
11262
11263 /**
11264 * Set selection state of this radio button.
11265 *
11266 * @param {boolean} state `true` for selected
11267 * @chainable
11268 */
11269 OO.ui.RadioInputWidget.prototype.setSelected = function ( state ) {
11270 // RadioInputWidget doesn't track its state.
11271 this.$input.prop( 'checked', state );
11272 return this;
11273 };
11274
11275 /**
11276 * Check if this radio button is selected.
11277 *
11278 * @return {boolean} Radio is selected
11279 */
11280 OO.ui.RadioInputWidget.prototype.isSelected = function () {
11281 return this.$input.prop( 'checked' );
11282 };
11283
11284 /**
11285 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
11286 * size of the field as well as its presentation. In addition, these widgets can be configured
11287 * with {@link OO.ui.IconElement icons}, {@link OO.ui.IndicatorElement indicators}, an optional
11288 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
11289 * which modifies incoming values rather than validating them.
11290 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
11291 *
11292 * @example
11293 * // Example of a text input widget
11294 * var textInput=new OO.ui.TextInputWidget( {
11295 * value: 'Text input'
11296 * } )
11297 * $('body').append(textInput.$element);
11298 *
11299 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
11300 *
11301 * @class
11302 * @extends OO.ui.InputWidget
11303 * @mixins OO.ui.IconElement
11304 * @mixins OO.ui.IndicatorElement
11305 * @mixins OO.ui.PendingElement
11306 * @mixins OO.ui.LabelElement
11307 *
11308 * @constructor
11309 * @param {Object} [config] Configuration options
11310 * @cfg {string} [type='text'] The value of the HTML `type` attribute
11311 * @cfg {string} [placeholder] Placeholder text
11312 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
11313 * instruct the browser to focus this widget.
11314 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
11315 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
11316 * @cfg {boolean} [multiline=false] Allow multiple lines of text
11317 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
11318 * Use the #maxRows config to specify a maximum number of displayed rows.
11319 * @cfg {boolean} [maxRows=10] Maximum number of rows to display when #autosize is set to true.
11320 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
11321 * the value or placeholder text: `'before'` or `'after'`
11322 * @cfg {boolean} [required=false] Mark the field as required
11323 * @cfg {RegExp|string} [validate] Validation pattern, either a regular expression or the
11324 * symbolic name of a pattern defined by the class: 'non-empty' (the value cannot be an empty string)
11325 * or 'integer' (the value must contain only numbers).
11326 */
11327 OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
11328 // Configuration initialization
11329 config = $.extend( {
11330 type: 'text',
11331 labelPosition: 'after',
11332 maxRows: 10
11333 }, config );
11334
11335 // Parent constructor
11336 OO.ui.TextInputWidget.super.call( this, config );
11337
11338 // Mixin constructors
11339 OO.ui.IconElement.call( this, config );
11340 OO.ui.IndicatorElement.call( this, config );
11341 OO.ui.PendingElement.call( this, config );
11342 OO.ui.LabelElement.call( this, config );
11343
11344 // Properties
11345 this.readOnly = false;
11346 this.multiline = !!config.multiline;
11347 this.autosize = !!config.autosize;
11348 this.maxRows = config.maxRows;
11349 this.validate = null;
11350
11351 // Clone for resizing
11352 if ( this.autosize ) {
11353 this.$clone = this.$input
11354 .clone()
11355 .insertAfter( this.$input )
11356 .attr( 'aria-hidden', 'true' )
11357 .addClass( 'oo-ui-element-hidden' );
11358 }
11359
11360 this.setValidation( config.validate );
11361 this.setLabelPosition( config.labelPosition );
11362
11363 // Events
11364 this.$input.on( {
11365 keypress: this.onKeyPress.bind( this ),
11366 blur: this.setValidityFlag.bind( this )
11367 } );
11368 this.$input.one( {
11369 focus: this.onElementAttach.bind( this )
11370 } );
11371 this.$icon.on( 'mousedown', this.onIconMouseDown.bind( this ) );
11372 this.$indicator.on( 'mousedown', this.onIndicatorMouseDown.bind( this ) );
11373 this.on( 'labelChange', this.updatePosition.bind( this ) );
11374
11375 // Initialization
11376 this.$element
11377 .addClass( 'oo-ui-textInputWidget' )
11378 .append( this.$icon, this.$indicator );
11379 this.setReadOnly( !!config.readOnly );
11380 if ( config.placeholder ) {
11381 this.$input.attr( 'placeholder', config.placeholder );
11382 }
11383 if ( config.maxLength !== undefined ) {
11384 this.$input.attr( 'maxlength', config.maxLength );
11385 }
11386 if ( config.autofocus ) {
11387 this.$input.attr( 'autofocus', 'autofocus' );
11388 }
11389 if ( config.required ) {
11390 this.$input.attr( 'required', 'true' );
11391 }
11392 if ( this.label || config.autosize ) {
11393 this.installParentChangeDetector();
11394 }
11395 };
11396
11397 /* Setup */
11398
11399 OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
11400 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IconElement );
11401 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IndicatorElement );
11402 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.PendingElement );
11403 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.LabelElement );
11404
11405 /* Static properties */
11406
11407 OO.ui.TextInputWidget.static.validationPatterns = {
11408 'non-empty': /.+/,
11409 integer: /^\d+$/
11410 };
11411
11412 /* Events */
11413
11414 /**
11415 * An `enter` event is emitted when the user presses 'enter' inside the text box.
11416 *
11417 * Not emitted if the input is multiline.
11418 *
11419 * @event enter
11420 */
11421
11422 /* Methods */
11423
11424 /**
11425 * Handle icon mouse down events.
11426 *
11427 * @private
11428 * @param {jQuery.Event} e Mouse down event
11429 * @fires icon
11430 */
11431 OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
11432 if ( e.which === 1 ) {
11433 this.$input[ 0 ].focus();
11434 return false;
11435 }
11436 };
11437
11438 /**
11439 * Handle indicator mouse down events.
11440 *
11441 * @private
11442 * @param {jQuery.Event} e Mouse down event
11443 * @fires indicator
11444 */
11445 OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
11446 if ( e.which === 1 ) {
11447 this.$input[ 0 ].focus();
11448 return false;
11449 }
11450 };
11451
11452 /**
11453 * Handle key press events.
11454 *
11455 * @private
11456 * @param {jQuery.Event} e Key press event
11457 * @fires enter If enter key is pressed and input is not multiline
11458 */
11459 OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
11460 if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) {
11461 this.emit( 'enter', e );
11462 }
11463 };
11464
11465 /**
11466 * Handle element attach events.
11467 *
11468 * @private
11469 * @param {jQuery.Event} e Element attach event
11470 */
11471 OO.ui.TextInputWidget.prototype.onElementAttach = function () {
11472 // Any previously calculated size is now probably invalid if we reattached elsewhere
11473 this.valCache = null;
11474 this.adjustSize();
11475 this.positionLabel();
11476 };
11477
11478 /**
11479 * @inheritdoc
11480 */
11481 OO.ui.TextInputWidget.prototype.onEdit = function () {
11482 this.adjustSize();
11483
11484 // Parent method
11485 return OO.ui.TextInputWidget.super.prototype.onEdit.call( this );
11486 };
11487
11488 /**
11489 * @inheritdoc
11490 */
11491 OO.ui.TextInputWidget.prototype.setValue = function ( value ) {
11492 // Parent method
11493 OO.ui.TextInputWidget.super.prototype.setValue.call( this, value );
11494
11495 this.setValidityFlag();
11496 this.adjustSize();
11497 return this;
11498 };
11499
11500 /**
11501 * Check if the input is {@link #readOnly read-only}.
11502 *
11503 * @return {boolean}
11504 */
11505 OO.ui.TextInputWidget.prototype.isReadOnly = function () {
11506 return this.readOnly;
11507 };
11508
11509 /**
11510 * Set the {@link #readOnly read-only} state of the input.
11511 *
11512 * @param {boolean} state Make input read-only
11513 * @chainable
11514 */
11515 OO.ui.TextInputWidget.prototype.setReadOnly = function ( state ) {
11516 this.readOnly = !!state;
11517 this.$input.prop( 'readOnly', this.readOnly );
11518 return this;
11519 };
11520
11521 /**
11522 * Support function for making #onElementAttach work across browsers.
11523 *
11524 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
11525 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
11526 *
11527 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
11528 * first time that the element gets attached to the documented.
11529 */
11530 OO.ui.TextInputWidget.prototype.installParentChangeDetector = function () {
11531 var mutationObserver, onRemove, topmostNode, fakeParentNode,
11532 MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
11533 widget = this;
11534
11535 if ( MutationObserver ) {
11536 // The new way. If only it wasn't so ugly.
11537
11538 if ( this.$element.closest( 'html' ).length ) {
11539 // Widget is attached already, do nothing. This breaks the functionality of this function when
11540 // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
11541 // would require observation of the whole document, which would hurt performance of other,
11542 // more important code.
11543 return;
11544 }
11545
11546 // Find topmost node in the tree
11547 topmostNode = this.$element[0];
11548 while ( topmostNode.parentNode ) {
11549 topmostNode = topmostNode.parentNode;
11550 }
11551
11552 // We have no way to detect the $element being attached somewhere without observing the entire
11553 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
11554 // parent node of $element, and instead detect when $element is removed from it (and thus
11555 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
11556 // doesn't get attached, we end up back here and create the parent.
11557
11558 mutationObserver = new MutationObserver( function ( mutations ) {
11559 var i, j, removedNodes;
11560 for ( i = 0; i < mutations.length; i++ ) {
11561 removedNodes = mutations[ i ].removedNodes;
11562 for ( j = 0; j < removedNodes.length; j++ ) {
11563 if ( removedNodes[ j ] === topmostNode ) {
11564 setTimeout( onRemove, 0 );
11565 return;
11566 }
11567 }
11568 }
11569 } );
11570
11571 onRemove = function () {
11572 // If the node was attached somewhere else, report it
11573 if ( widget.$element.closest( 'html' ).length ) {
11574 widget.onElementAttach();
11575 }
11576 mutationObserver.disconnect();
11577 widget.installParentChangeDetector();
11578 };
11579
11580 // Create a fake parent and observe it
11581 fakeParentNode = $( '<div>' ).append( this.$element )[0];
11582 mutationObserver.observe( fakeParentNode, { childList: true } );
11583 } else {
11584 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
11585 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
11586 this.$element.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach.bind( this ) );
11587 }
11588 };
11589
11590 /**
11591 * Automatically adjust the size of the text input.
11592 *
11593 * This only affects #multiline inputs that are {@link #autosize autosized}.
11594 *
11595 * @chainable
11596 */
11597 OO.ui.TextInputWidget.prototype.adjustSize = function () {
11598 var scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError, idealHeight;
11599
11600 if ( this.multiline && this.autosize && this.$input.val() !== this.valCache ) {
11601 this.$clone
11602 .val( this.$input.val() )
11603 .attr( 'rows', '' )
11604 // Set inline height property to 0 to measure scroll height
11605 .css( 'height', 0 );
11606
11607 this.$clone.removeClass( 'oo-ui-element-hidden' );
11608
11609 this.valCache = this.$input.val();
11610
11611 scrollHeight = this.$clone[ 0 ].scrollHeight;
11612
11613 // Remove inline height property to measure natural heights
11614 this.$clone.css( 'height', '' );
11615 innerHeight = this.$clone.innerHeight();
11616 outerHeight = this.$clone.outerHeight();
11617
11618 // Measure max rows height
11619 this.$clone
11620 .attr( 'rows', this.maxRows )
11621 .css( 'height', 'auto' )
11622 .val( '' );
11623 maxInnerHeight = this.$clone.innerHeight();
11624
11625 // Difference between reported innerHeight and scrollHeight with no scrollbars present
11626 // Equals 1 on Blink-based browsers and 0 everywhere else
11627 measurementError = maxInnerHeight - this.$clone[ 0 ].scrollHeight;
11628 idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
11629
11630 this.$clone.addClass( 'oo-ui-element-hidden' );
11631
11632 // Only apply inline height when expansion beyond natural height is needed
11633 if ( idealHeight > innerHeight ) {
11634 // Use the difference between the inner and outer height as a buffer
11635 this.$input.css( 'height', idealHeight + ( outerHeight - innerHeight ) );
11636 } else {
11637 this.$input.css( 'height', '' );
11638 }
11639 }
11640 return this;
11641 };
11642
11643 /**
11644 * @inheritdoc
11645 * @private
11646 */
11647 OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
11648 return config.multiline ? $( '<textarea>' ) : $( '<input type="' + config.type + '" />' );
11649 };
11650
11651 /**
11652 * Check if the input supports multiple lines.
11653 *
11654 * @return {boolean}
11655 */
11656 OO.ui.TextInputWidget.prototype.isMultiline = function () {
11657 return !!this.multiline;
11658 };
11659
11660 /**
11661 * Check if the input automatically adjusts its size.
11662 *
11663 * @return {boolean}
11664 */
11665 OO.ui.TextInputWidget.prototype.isAutosizing = function () {
11666 return !!this.autosize;
11667 };
11668
11669 /**
11670 * Select the entire text of the input.
11671 *
11672 * @chainable
11673 */
11674 OO.ui.TextInputWidget.prototype.select = function () {
11675 this.$input.select();
11676 return this;
11677 };
11678
11679 /**
11680 * Set the validation pattern.
11681 *
11682 * The validation pattern is either a regular expression or the symbolic name of a pattern
11683 * defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
11684 * value must contain only numbers).
11685 *
11686 * @param {RegExp|string|null} validate Regular expression or the symbolic name of a
11687 * pattern (either ‘integer’ or ‘non-empty’) defined by the class.
11688 */
11689 OO.ui.TextInputWidget.prototype.setValidation = function ( validate ) {
11690 if ( validate instanceof RegExp ) {
11691 this.validate = validate;
11692 } else {
11693 this.validate = this.constructor.static.validationPatterns[ validate ] || /.*/;
11694 }
11695 };
11696
11697 /**
11698 * Sets the 'invalid' flag appropriately.
11699 */
11700 OO.ui.TextInputWidget.prototype.setValidityFlag = function () {
11701 var widget = this;
11702 this.isValid().done( function ( valid ) {
11703 widget.setFlags( { invalid: !valid } );
11704 } );
11705 };
11706
11707 /**
11708 * Check if a value is valid.
11709 *
11710 * This method returns a promise that resolves with a boolean `true` if the current value is
11711 * considered valid according to the supplied {@link #validate validation pattern}.
11712 *
11713 * @return {jQuery.Deferred} A promise that resolves to a boolean `true` if the value is valid.
11714 */
11715 OO.ui.TextInputWidget.prototype.isValid = function () {
11716 return $.Deferred().resolve( !!this.getValue().match( this.validate ) ).promise();
11717 };
11718
11719 /**
11720 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
11721 *
11722 * @param {string} labelPosition Label position, 'before' or 'after'
11723 * @chainable
11724 */
11725 OO.ui.TextInputWidget.prototype.setLabelPosition = function ( labelPosition ) {
11726 this.labelPosition = labelPosition;
11727 this.updatePosition();
11728 return this;
11729 };
11730
11731 /**
11732 * Deprecated alias of #setLabelPosition
11733 *
11734 * @deprecated Use setLabelPosition instead.
11735 */
11736 OO.ui.TextInputWidget.prototype.setPosition =
11737 OO.ui.TextInputWidget.prototype.setLabelPosition;
11738
11739 /**
11740 * Update the position of the inline label.
11741 *
11742 * This method is called by #setLabelPosition, and can also be called on its own if
11743 * something causes the label to be mispositioned.
11744 *
11745 *
11746 * @chainable
11747 */
11748 OO.ui.TextInputWidget.prototype.updatePosition = function () {
11749 var after = this.labelPosition === 'after';
11750
11751 this.$element
11752 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label && after )
11753 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label && !after );
11754
11755 if ( this.label ) {
11756 this.positionLabel();
11757 }
11758
11759 return this;
11760 };
11761
11762 /**
11763 * Position the label by setting the correct padding on the input.
11764 *
11765 * @private
11766 * @chainable
11767 */
11768 OO.ui.TextInputWidget.prototype.positionLabel = function () {
11769 // Clear old values
11770 this.$input
11771 // Clear old values if present
11772 .css( {
11773 'padding-right': '',
11774 'padding-left': ''
11775 } );
11776
11777 if ( this.label ) {
11778 this.$element.append( this.$label );
11779 } else {
11780 this.$label.detach();
11781 return;
11782 }
11783
11784 var after = this.labelPosition === 'after',
11785 rtl = this.$element.css( 'direction' ) === 'rtl',
11786 property = after === rtl ? 'padding-left' : 'padding-right';
11787
11788 this.$input.css( property, this.$label.outerWidth( true ) );
11789
11790 return this;
11791 };
11792
11793 /**
11794 * ComboBoxWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
11795 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
11796 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
11797 *
11798 * - by typing a value in the text input field. If the value exactly matches the value of a menu
11799 * option, that option will appear to be selected.
11800 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
11801 * input field.
11802 *
11803 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
11804 *
11805 * @example
11806 * // Example: A ComboBoxWidget.
11807 * var comboBox=new OO.ui.ComboBoxWidget( {
11808 * label: 'ComboBoxWidget',
11809 * input: { value: 'Option One' },
11810 * menu: {
11811 * items: [
11812 * new OO.ui.MenuOptionWidget( {
11813 * data: 'Option 1',
11814 * label: 'Option One' } ),
11815 * new OO.ui.MenuOptionWidget( {
11816 * data: 'Option 2',
11817 * label: 'Option Two' } ),
11818 * new OO.ui.MenuOptionWidget( {
11819 * data: 'Option 3',
11820 * label: 'Option Three'} ),
11821 * new OO.ui.MenuOptionWidget( {
11822 * data: 'Option 4',
11823 * label: 'Option Four' } ),
11824 * new OO.ui.MenuOptionWidget( {
11825 * data: 'Option 5',
11826 * label: 'Option Five' } )
11827 * ]
11828 * }
11829 * } );
11830 * $('body').append(comboBox.$element);
11831 *
11832 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
11833 *
11834 * @class
11835 * @extends OO.ui.Widget
11836 * @mixins OO.ui.TabIndexedElement
11837 *
11838 * @constructor
11839 * @param {Object} [config] Configuration options
11840 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}.
11841 * @cfg {Object} [input] Configuration options to pass to the {@link OO.ui.TextInputWidget text input widget}.
11842 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
11843 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
11844 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
11845 */
11846 OO.ui.ComboBoxWidget = function OoUiComboBoxWidget( config ) {
11847 // Configuration initialization
11848 config = config || {};
11849
11850 // Parent constructor
11851 OO.ui.ComboBoxWidget.super.call( this, config );
11852
11853 // Properties (must be set before TabIndexedElement constructor call)
11854 this.$indicator = this.$( '<span>' );
11855
11856 // Mixin constructors
11857 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$indicator } ) );
11858
11859 // Properties
11860 this.$overlay = config.$overlay || this.$element;
11861 this.input = new OO.ui.TextInputWidget( $.extend(
11862 {
11863 indicator: 'down',
11864 $indicator: this.$indicator,
11865 disabled: this.isDisabled()
11866 },
11867 config.input
11868 ) );
11869 this.input.$input.eq( 0 ).attr( {
11870 role: 'combobox',
11871 'aria-autocomplete': 'list'
11872 } );
11873 this.menu = new OO.ui.TextInputMenuSelectWidget( this.input, $.extend(
11874 {
11875 widget: this,
11876 input: this.input,
11877 disabled: this.isDisabled()
11878 },
11879 config.menu
11880 ) );
11881
11882 // Events
11883 this.$indicator.on( {
11884 click: this.onClick.bind( this ),
11885 keypress: this.onKeyPress.bind( this )
11886 } );
11887 this.input.connect( this, {
11888 change: 'onInputChange',
11889 enter: 'onInputEnter'
11890 } );
11891 this.menu.connect( this, {
11892 choose: 'onMenuChoose',
11893 add: 'onMenuItemsChange',
11894 remove: 'onMenuItemsChange'
11895 } );
11896
11897 // Initialization
11898 this.$element.addClass( 'oo-ui-comboBoxWidget' ).append( this.input.$element );
11899 this.$overlay.append( this.menu.$element );
11900 this.onMenuItemsChange();
11901 };
11902
11903 /* Setup */
11904
11905 OO.inheritClass( OO.ui.ComboBoxWidget, OO.ui.Widget );
11906 OO.mixinClass( OO.ui.ComboBoxWidget, OO.ui.TabIndexedElement );
11907
11908 /* Methods */
11909
11910 /**
11911 * Get the combobox's menu.
11912 * @return {OO.ui.TextInputMenuSelectWidget} Menu widget
11913 */
11914 OO.ui.ComboBoxWidget.prototype.getMenu = function () {
11915 return this.menu;
11916 };
11917
11918 /**
11919 * Handle input change events.
11920 *
11921 * @private
11922 * @param {string} value New value
11923 */
11924 OO.ui.ComboBoxWidget.prototype.onInputChange = function ( value ) {
11925 var match = this.menu.getItemFromData( value );
11926
11927 this.menu.selectItem( match );
11928 if ( this.menu.getHighlightedItem() ) {
11929 this.menu.highlightItem( match );
11930 }
11931
11932 if ( !this.isDisabled() ) {
11933 this.menu.toggle( true );
11934 }
11935 };
11936
11937 /**
11938 * Handle mouse click events.
11939 *
11940 *
11941 * @private
11942 * @param {jQuery.Event} e Mouse click event
11943 */
11944 OO.ui.ComboBoxWidget.prototype.onClick = function ( e ) {
11945 if ( !this.isDisabled() && e.which === 1 ) {
11946 this.menu.toggle();
11947 this.input.$input[ 0 ].focus();
11948 }
11949 return false;
11950 };
11951
11952 /**
11953 * Handle key press events.
11954 *
11955 *
11956 * @private
11957 * @param {jQuery.Event} e Key press event
11958 */
11959 OO.ui.ComboBoxWidget.prototype.onKeyPress = function ( e ) {
11960 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
11961 this.menu.toggle();
11962 this.input.$input[ 0 ].focus();
11963 return false;
11964 }
11965 };
11966
11967 /**
11968 * Handle input enter events.
11969 *
11970 * @private
11971 */
11972 OO.ui.ComboBoxWidget.prototype.onInputEnter = function () {
11973 if ( !this.isDisabled() ) {
11974 this.menu.toggle( false );
11975 }
11976 };
11977
11978 /**
11979 * Handle menu choose events.
11980 *
11981 * @private
11982 * @param {OO.ui.OptionWidget} item Chosen item
11983 */
11984 OO.ui.ComboBoxWidget.prototype.onMenuChoose = function ( item ) {
11985 if ( item ) {
11986 this.input.setValue( item.getData() );
11987 }
11988 };
11989
11990 /**
11991 * Handle menu item change events.
11992 *
11993 * @private
11994 */
11995 OO.ui.ComboBoxWidget.prototype.onMenuItemsChange = function () {
11996 var match = this.menu.getItemFromData( this.input.getValue() );
11997 this.menu.selectItem( match );
11998 if ( this.menu.getHighlightedItem() ) {
11999 this.menu.highlightItem( match );
12000 }
12001 this.$element.toggleClass( 'oo-ui-comboBoxWidget-empty', this.menu.isEmpty() );
12002 };
12003
12004 /**
12005 * @inheritdoc
12006 */
12007 OO.ui.ComboBoxWidget.prototype.setDisabled = function ( disabled ) {
12008 // Parent method
12009 OO.ui.ComboBoxWidget.super.prototype.setDisabled.call( this, disabled );
12010
12011 if ( this.input ) {
12012 this.input.setDisabled( this.isDisabled() );
12013 }
12014 if ( this.menu ) {
12015 this.menu.setDisabled( this.isDisabled() );
12016 }
12017
12018 return this;
12019 };
12020
12021 /**
12022 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
12023 * be configured with a `label` option that is set to a string, a label node, or a function:
12024 *
12025 * - String: a plaintext string
12026 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
12027 * label that includes a link or special styling, such as a gray color or additional graphical elements.
12028 * - Function: a function that will produce a string in the future. Functions are used
12029 * in cases where the value of the label is not currently defined.
12030 *
12031 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
12032 * will come into focus when the label is clicked.
12033 *
12034 * @example
12035 * // Examples of LabelWidgets
12036 * var label1 = new OO.ui.LabelWidget({
12037 * label: 'plaintext label'
12038 * });
12039 * var label2 = new OO.ui.LabelWidget({
12040 * label: $( '<a href="default.html">jQuery label</a>' )
12041 * });
12042 * // Create a fieldset layout with fields for each example
12043 * var fieldset = new OO.ui.FieldsetLayout( );
12044 * fieldset.addItems( [
12045 * new OO.ui.FieldLayout( label1 ),
12046 * new OO.ui.FieldLayout( label2 )
12047 * ] );
12048 * $( 'body' ).append( fieldset.$element );
12049 *
12050 *
12051 * @class
12052 * @extends OO.ui.Widget
12053 * @mixins OO.ui.LabelElement
12054 *
12055 * @constructor
12056 * @param {Object} [config] Configuration options
12057 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
12058 * Clicking the label will focus the specified input field.
12059 */
12060 OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
12061 // Configuration initialization
12062 config = config || {};
12063
12064 // Parent constructor
12065 OO.ui.LabelWidget.super.call( this, config );
12066
12067 // Mixin constructors
12068 OO.ui.LabelElement.call( this, $.extend( {}, config, { $label: this.$element } ) );
12069 OO.ui.TitledElement.call( this, config );
12070
12071 // Properties
12072 this.input = config.input;
12073
12074 // Events
12075 if ( this.input instanceof OO.ui.InputWidget ) {
12076 this.$element.on( 'click', this.onClick.bind( this ) );
12077 }
12078
12079 // Initialization
12080 this.$element.addClass( 'oo-ui-labelWidget' );
12081 };
12082
12083 /* Setup */
12084
12085 OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
12086 OO.mixinClass( OO.ui.LabelWidget, OO.ui.LabelElement );
12087 OO.mixinClass( OO.ui.LabelWidget, OO.ui.TitledElement );
12088
12089 /* Static Properties */
12090
12091 OO.ui.LabelWidget.static.tagName = 'span';
12092
12093 /* Methods */
12094
12095 /**
12096 * Handles label mouse click events.
12097 *
12098 * @private
12099 * @param {jQuery.Event} e Mouse click event
12100 */
12101 OO.ui.LabelWidget.prototype.onClick = function () {
12102 this.input.simulateLabelClick();
12103 return false;
12104 };
12105
12106 /**
12107 * OptionWidgets are special elements that can be selected and configured with data. The
12108 * data is often unique for each option, but it does not have to be. OptionWidgets are used
12109 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
12110 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
12111 *
12112 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
12113 *
12114 * @class
12115 * @extends OO.ui.Widget
12116 * @mixins OO.ui.LabelElement
12117 * @mixins OO.ui.FlaggedElement
12118 *
12119 * @constructor
12120 * @param {Object} [config] Configuration options
12121 */
12122 OO.ui.OptionWidget = function OoUiOptionWidget( config ) {
12123 // Configuration initialization
12124 config = config || {};
12125
12126 // Parent constructor
12127 OO.ui.OptionWidget.super.call( this, config );
12128
12129 // Mixin constructors
12130 OO.ui.ItemWidget.call( this );
12131 OO.ui.LabelElement.call( this, config );
12132 OO.ui.FlaggedElement.call( this, config );
12133
12134 // Properties
12135 this.selected = false;
12136 this.highlighted = false;
12137 this.pressed = false;
12138
12139 // Initialization
12140 this.$element
12141 .data( 'oo-ui-optionWidget', this )
12142 .attr( 'role', 'option' )
12143 .addClass( 'oo-ui-optionWidget' )
12144 .append( this.$label );
12145 };
12146
12147 /* Setup */
12148
12149 OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
12150 OO.mixinClass( OO.ui.OptionWidget, OO.ui.ItemWidget );
12151 OO.mixinClass( OO.ui.OptionWidget, OO.ui.LabelElement );
12152 OO.mixinClass( OO.ui.OptionWidget, OO.ui.FlaggedElement );
12153
12154 /* Static Properties */
12155
12156 OO.ui.OptionWidget.static.selectable = true;
12157
12158 OO.ui.OptionWidget.static.highlightable = true;
12159
12160 OO.ui.OptionWidget.static.pressable = true;
12161
12162 OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
12163
12164 /* Methods */
12165
12166 /**
12167 * Check if the option can be selected.
12168 *
12169 * @return {boolean} Item is selectable
12170 */
12171 OO.ui.OptionWidget.prototype.isSelectable = function () {
12172 return this.constructor.static.selectable && !this.isDisabled();
12173 };
12174
12175 /**
12176 * Check if the option can be highlighted. A highlight indicates that the option
12177 * may be selected when a user presses enter or clicks. Disabled items cannot
12178 * be highlighted.
12179 *
12180 * @return {boolean} Item is highlightable
12181 */
12182 OO.ui.OptionWidget.prototype.isHighlightable = function () {
12183 return this.constructor.static.highlightable && !this.isDisabled();
12184 };
12185
12186 /**
12187 * Check if the option can be pressed. The pressed state occurs when a user mouses
12188 * down on an item, but has not yet let go of the mouse.
12189 *
12190 * @return {boolean} Item is pressable
12191 */
12192 OO.ui.OptionWidget.prototype.isPressable = function () {
12193 return this.constructor.static.pressable && !this.isDisabled();
12194 };
12195
12196 /**
12197 * Check if the option is selected.
12198 *
12199 * @return {boolean} Item is selected
12200 */
12201 OO.ui.OptionWidget.prototype.isSelected = function () {
12202 return this.selected;
12203 };
12204
12205 /**
12206 * Check if the option is highlighted. A highlight indicates that the
12207 * item may be selected when a user presses enter or clicks.
12208 *
12209 * @return {boolean} Item is highlighted
12210 */
12211 OO.ui.OptionWidget.prototype.isHighlighted = function () {
12212 return this.highlighted;
12213 };
12214
12215 /**
12216 * Check if the option is pressed. The pressed state occurs when a user mouses
12217 * down on an item, but has not yet let go of the mouse. The item may appear
12218 * selected, but it will not be selected until the user releases the mouse.
12219 *
12220 * @return {boolean} Item is pressed
12221 */
12222 OO.ui.OptionWidget.prototype.isPressed = function () {
12223 return this.pressed;
12224 };
12225
12226 /**
12227 * Set the option’s selected state. In general, all modifications to the selection
12228 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
12229 * method instead of this method.
12230 *
12231 * @param {boolean} [state=false] Select option
12232 * @chainable
12233 */
12234 OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
12235 if ( this.constructor.static.selectable ) {
12236 this.selected = !!state;
12237 this.$element
12238 .toggleClass( 'oo-ui-optionWidget-selected', state )
12239 .attr( 'aria-selected', state.toString() );
12240 if ( state && this.constructor.static.scrollIntoViewOnSelect ) {
12241 this.scrollElementIntoView();
12242 }
12243 this.updateThemeClasses();
12244 }
12245 return this;
12246 };
12247
12248 /**
12249 * Set the option’s highlighted state. In general, all programmatic
12250 * modifications to the highlight should be handled by the
12251 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
12252 * method instead of this method.
12253 *
12254 * @param {boolean} [state=false] Highlight option
12255 * @chainable
12256 */
12257 OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
12258 if ( this.constructor.static.highlightable ) {
12259 this.highlighted = !!state;
12260 this.$element.toggleClass( 'oo-ui-optionWidget-highlighted', state );
12261 this.updateThemeClasses();
12262 }
12263 return this;
12264 };
12265
12266 /**
12267 * Set the option’s pressed state. In general, all
12268 * programmatic modifications to the pressed state should be handled by the
12269 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
12270 * method instead of this method.
12271 *
12272 * @param {boolean} [state=false] Press option
12273 * @chainable
12274 */
12275 OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
12276 if ( this.constructor.static.pressable ) {
12277 this.pressed = !!state;
12278 this.$element.toggleClass( 'oo-ui-optionWidget-pressed', state );
12279 this.updateThemeClasses();
12280 }
12281 return this;
12282 };
12283
12284 /**
12285 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
12286 * with an {@link OO.ui.IconElement icon} and/or {@link OO.ui.IndicatorElement indicator}.
12287 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
12288 * options. For more information about options and selects, please see the
12289 * [OOjs UI documentation on MediaWiki][1].
12290 *
12291 * @example
12292 * // Decorated options in a select widget
12293 * var select=new OO.ui.SelectWidget( {
12294 * items: [
12295 * new OO.ui.DecoratedOptionWidget( {
12296 * data: 'a',
12297 * label: 'Option with icon',
12298 * icon: 'help'
12299 * } ),
12300 * new OO.ui.DecoratedOptionWidget( {
12301 * data: 'b',
12302 * label: 'Option with indicator',
12303 * indicator: 'next'
12304 * } )
12305 * ]
12306 * } );
12307 * $('body').append(select.$element);
12308 *
12309 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
12310 *
12311 * @class
12312 * @extends OO.ui.OptionWidget
12313 * @mixins OO.ui.IconElement
12314 * @mixins OO.ui.IndicatorElement
12315 *
12316 * @constructor
12317 * @param {Object} [config] Configuration options
12318 */
12319 OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( config ) {
12320 // Parent constructor
12321 OO.ui.DecoratedOptionWidget.super.call( this, config );
12322
12323 // Mixin constructors
12324 OO.ui.IconElement.call( this, config );
12325 OO.ui.IndicatorElement.call( this, config );
12326
12327 // Initialization
12328 this.$element
12329 .addClass( 'oo-ui-decoratedOptionWidget' )
12330 .prepend( this.$icon )
12331 .append( this.$indicator );
12332 };
12333
12334 /* Setup */
12335
12336 OO.inheritClass( OO.ui.DecoratedOptionWidget, OO.ui.OptionWidget );
12337 OO.mixinClass( OO.ui.OptionWidget, OO.ui.IconElement );
12338 OO.mixinClass( OO.ui.OptionWidget, OO.ui.IndicatorElement );
12339
12340 /**
12341 * ButtonOptionWidget is a special type of {@link OO.ui.ButtonElement button element} that
12342 * can be selected and configured with data. The class is
12343 * used with OO.ui.ButtonSelectWidget to create a selection of button options. Please see the
12344 * [OOjs UI documentation on MediaWiki] [1] for more information.
12345 *
12346 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_options
12347 *
12348 * @class
12349 * @extends OO.ui.DecoratedOptionWidget
12350 * @mixins OO.ui.ButtonElement
12351 * @mixins OO.ui.TabIndexedElement
12352 *
12353 * @constructor
12354 * @param {Object} [config] Configuration options
12355 */
12356 OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( config ) {
12357 // Configuration initialization
12358 config = $.extend( { tabIndex: -1 }, config );
12359
12360 // Parent constructor
12361 OO.ui.ButtonOptionWidget.super.call( this, config );
12362
12363 // Mixin constructors
12364 OO.ui.ButtonElement.call( this, config );
12365 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
12366
12367 // Initialization
12368 this.$element.addClass( 'oo-ui-buttonOptionWidget' );
12369 this.$button.append( this.$element.contents() );
12370 this.$element.append( this.$button );
12371 };
12372
12373 /* Setup */
12374
12375 OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.DecoratedOptionWidget );
12376 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.ButtonElement );
12377 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.TabIndexedElement );
12378
12379 /* Static Properties */
12380
12381 // Allow button mouse down events to pass through so they can be handled by the parent select widget
12382 OO.ui.ButtonOptionWidget.static.cancelButtonMouseDownEvents = false;
12383
12384 OO.ui.ButtonOptionWidget.static.highlightable = false;
12385
12386 /* Methods */
12387
12388 /**
12389 * @inheritdoc
12390 */
12391 OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) {
12392 OO.ui.ButtonOptionWidget.super.prototype.setSelected.call( this, state );
12393
12394 if ( this.constructor.static.selectable ) {
12395 this.setActive( state );
12396 }
12397
12398 return this;
12399 };
12400
12401 /**
12402 * RadioOptionWidget is an option widget that looks like a radio button.
12403 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
12404 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
12405 *
12406 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
12407 *
12408 * @class
12409 * @extends OO.ui.OptionWidget
12410 *
12411 * @constructor
12412 * @param {Object} [config] Configuration options
12413 */
12414 OO.ui.RadioOptionWidget = function OoUiRadioOptionWidget( config ) {
12415 // Configuration initialization
12416 config = config || {};
12417
12418 // Properties (must be done before parent constructor which calls #setDisabled)
12419 this.radio = new OO.ui.RadioInputWidget( { value: config.data, tabIndex: -1 } );
12420
12421 // Parent constructor
12422 OO.ui.RadioOptionWidget.super.call( this, config );
12423
12424 // Initialization
12425 this.$element
12426 .addClass( 'oo-ui-radioOptionWidget' )
12427 .prepend( this.radio.$element );
12428 };
12429
12430 /* Setup */
12431
12432 OO.inheritClass( OO.ui.RadioOptionWidget, OO.ui.OptionWidget );
12433
12434 /* Static Properties */
12435
12436 OO.ui.RadioOptionWidget.static.highlightable = false;
12437
12438 OO.ui.RadioOptionWidget.static.scrollIntoViewOnSelect = true;
12439
12440 OO.ui.RadioOptionWidget.static.pressable = false;
12441
12442 OO.ui.RadioOptionWidget.static.tagName = 'label';
12443
12444 /* Methods */
12445
12446 /**
12447 * @inheritdoc
12448 */
12449 OO.ui.RadioOptionWidget.prototype.setSelected = function ( state ) {
12450 OO.ui.RadioOptionWidget.super.prototype.setSelected.call( this, state );
12451
12452 this.radio.setSelected( state );
12453
12454 return this;
12455 };
12456
12457 /**
12458 * @inheritdoc
12459 */
12460 OO.ui.RadioOptionWidget.prototype.setDisabled = function ( disabled ) {
12461 OO.ui.RadioOptionWidget.super.prototype.setDisabled.call( this, disabled );
12462
12463 this.radio.setDisabled( this.isDisabled() );
12464
12465 return this;
12466 };
12467
12468 /**
12469 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
12470 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
12471 * the [OOjs UI documentation on MediaWiki] [1] for more information.
12472 *
12473 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
12474 *
12475 * @class
12476 * @extends OO.ui.DecoratedOptionWidget
12477 *
12478 * @constructor
12479 * @param {Object} [config] Configuration options
12480 */
12481 OO.ui.MenuOptionWidget = function OoUiMenuOptionWidget( config ) {
12482 // Configuration initialization
12483 config = $.extend( { icon: 'check' }, config );
12484
12485 // Parent constructor
12486 OO.ui.MenuOptionWidget.super.call( this, config );
12487
12488 // Initialization
12489 this.$element
12490 .attr( 'role', 'menuitem' )
12491 .addClass( 'oo-ui-menuOptionWidget' );
12492 };
12493
12494 /* Setup */
12495
12496 OO.inheritClass( OO.ui.MenuOptionWidget, OO.ui.DecoratedOptionWidget );
12497
12498 /* Static Properties */
12499
12500 OO.ui.MenuOptionWidget.static.scrollIntoViewOnSelect = true;
12501
12502 /**
12503 * Section to group one or more items in a OO.ui.MenuSelectWidget.
12504 *
12505 * @class
12506 * @extends OO.ui.DecoratedOptionWidget
12507 *
12508 * @constructor
12509 * @param {Object} [config] Configuration options
12510 */
12511 OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( config ) {
12512 // Parent constructor
12513 OO.ui.MenuSectionOptionWidget.super.call( this, config );
12514
12515 // Initialization
12516 this.$element.addClass( 'oo-ui-menuSectionOptionWidget' );
12517 };
12518
12519 /* Setup */
12520
12521 OO.inheritClass( OO.ui.MenuSectionOptionWidget, OO.ui.DecoratedOptionWidget );
12522
12523 /* Static Properties */
12524
12525 OO.ui.MenuSectionOptionWidget.static.selectable = false;
12526
12527 OO.ui.MenuSectionOptionWidget.static.highlightable = false;
12528
12529 /**
12530 * Items for an OO.ui.OutlineSelectWidget.
12531 *
12532 * @class
12533 * @extends OO.ui.DecoratedOptionWidget
12534 *
12535 * @constructor
12536 * @param {Object} [config] Configuration options
12537 * @cfg {number} [level] Indentation level
12538 * @cfg {boolean} [movable] Allow modification from outline controls
12539 */
12540 OO.ui.OutlineOptionWidget = function OoUiOutlineOptionWidget( config ) {
12541 // Configuration initialization
12542 config = config || {};
12543
12544 // Parent constructor
12545 OO.ui.OutlineOptionWidget.super.call( this, config );
12546
12547 // Properties
12548 this.level = 0;
12549 this.movable = !!config.movable;
12550 this.removable = !!config.removable;
12551
12552 // Initialization
12553 this.$element.addClass( 'oo-ui-outlineOptionWidget' );
12554 this.setLevel( config.level );
12555 };
12556
12557 /* Setup */
12558
12559 OO.inheritClass( OO.ui.OutlineOptionWidget, OO.ui.DecoratedOptionWidget );
12560
12561 /* Static Properties */
12562
12563 OO.ui.OutlineOptionWidget.static.highlightable = false;
12564
12565 OO.ui.OutlineOptionWidget.static.scrollIntoViewOnSelect = true;
12566
12567 OO.ui.OutlineOptionWidget.static.levelClass = 'oo-ui-outlineOptionWidget-level-';
12568
12569 OO.ui.OutlineOptionWidget.static.levels = 3;
12570
12571 /* Methods */
12572
12573 /**
12574 * Check if item is movable.
12575 *
12576 * Movability is used by outline controls.
12577 *
12578 * @return {boolean} Item is movable
12579 */
12580 OO.ui.OutlineOptionWidget.prototype.isMovable = function () {
12581 return this.movable;
12582 };
12583
12584 /**
12585 * Check if item is removable.
12586 *
12587 * Removability is used by outline controls.
12588 *
12589 * @return {boolean} Item is removable
12590 */
12591 OO.ui.OutlineOptionWidget.prototype.isRemovable = function () {
12592 return this.removable;
12593 };
12594
12595 /**
12596 * Get indentation level.
12597 *
12598 * @return {number} Indentation level
12599 */
12600 OO.ui.OutlineOptionWidget.prototype.getLevel = function () {
12601 return this.level;
12602 };
12603
12604 /**
12605 * Set movability.
12606 *
12607 * Movability is used by outline controls.
12608 *
12609 * @param {boolean} movable Item is movable
12610 * @chainable
12611 */
12612 OO.ui.OutlineOptionWidget.prototype.setMovable = function ( movable ) {
12613 this.movable = !!movable;
12614 this.updateThemeClasses();
12615 return this;
12616 };
12617
12618 /**
12619 * Set removability.
12620 *
12621 * Removability is used by outline controls.
12622 *
12623 * @param {boolean} movable Item is removable
12624 * @chainable
12625 */
12626 OO.ui.OutlineOptionWidget.prototype.setRemovable = function ( removable ) {
12627 this.removable = !!removable;
12628 this.updateThemeClasses();
12629 return this;
12630 };
12631
12632 /**
12633 * Set indentation level.
12634 *
12635 * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
12636 * @chainable
12637 */
12638 OO.ui.OutlineOptionWidget.prototype.setLevel = function ( level ) {
12639 var levels = this.constructor.static.levels,
12640 levelClass = this.constructor.static.levelClass,
12641 i = levels;
12642
12643 this.level = level ? Math.max( 0, Math.min( levels - 1, level ) ) : 0;
12644 while ( i-- ) {
12645 if ( this.level === i ) {
12646 this.$element.addClass( levelClass + i );
12647 } else {
12648 this.$element.removeClass( levelClass + i );
12649 }
12650 }
12651 this.updateThemeClasses();
12652
12653 return this;
12654 };
12655
12656 /**
12657 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
12658 * By default, each popup has an anchor that points toward its origin.
12659 * Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples.
12660 *
12661 * @example
12662 * // A popup widget.
12663 * var popup=new OO.ui.PopupWidget({
12664 * $content: $( '<p>Hi there!</p>' ),
12665 * padded: true,
12666 * width: 300
12667 * } );
12668 *
12669 * $('body').append(popup.$element);
12670 * // To display the popup, toggle the visibility to 'true'.
12671 * popup.toggle(true);
12672 *
12673 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups
12674 *
12675 * @class
12676 * @extends OO.ui.Widget
12677 * @mixins OO.ui.LabelElement
12678 *
12679 * @constructor
12680 * @param {Object} [config] Configuration options
12681 * @cfg {number} [width=320] Width of popup in pixels
12682 * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
12683 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
12684 * @cfg {string} [align='center'] Alignment of the popup: `center`, `left`, or `right`.
12685 * If the popup is right-aligned, the right edge of the popup is aligned to the anchor.
12686 * For left-aligned popups, the left edge is aligned to the anchor.
12687 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
12688 * See the [OOjs UI docs on MediaWiki][3] for an example.
12689 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
12690 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
12691 * @cfg {jQuery} [$content] Content to append to the popup's body
12692 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
12693 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
12694 * This config option is only relevant if #autoClose is set to `true`. See the [OOjs UI docs on MediaWiki][2]
12695 * for an example.
12696 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#autocloseExample
12697 * @cfg {boolean} [head] Show a popup header that contains a #label (if specified) and close
12698 * button.
12699 * @cfg {boolean} [padded] Add padding to the popup's body
12700 */
12701 OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
12702 // Configuration initialization
12703 config = config || {};
12704
12705 // Parent constructor
12706 OO.ui.PopupWidget.super.call( this, config );
12707
12708 // Properties (must be set before ClippableElement constructor call)
12709 this.$body = $( '<div>' );
12710
12711 // Mixin constructors
12712 OO.ui.LabelElement.call( this, config );
12713 OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$body } ) );
12714
12715 // Properties
12716 this.$popup = $( '<div>' );
12717 this.$head = $( '<div>' );
12718 this.$anchor = $( '<div>' );
12719 // If undefined, will be computed lazily in updateDimensions()
12720 this.$container = config.$container;
12721 this.containerPadding = config.containerPadding !== undefined ? config.containerPadding : 10;
12722 this.autoClose = !!config.autoClose;
12723 this.$autoCloseIgnore = config.$autoCloseIgnore;
12724 this.transitionTimeout = null;
12725 this.anchor = null;
12726 this.width = config.width !== undefined ? config.width : 320;
12727 this.height = config.height !== undefined ? config.height : null;
12728 this.align = config.align || 'center';
12729 this.closeButton = new OO.ui.ButtonWidget( { framed: false, icon: 'close' } );
12730 this.onMouseDownHandler = this.onMouseDown.bind( this );
12731 this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
12732
12733 // Events
12734 this.closeButton.connect( this, { click: 'onCloseButtonClick' } );
12735
12736 // Initialization
12737 this.toggleAnchor( config.anchor === undefined || config.anchor );
12738 this.$body.addClass( 'oo-ui-popupWidget-body' );
12739 this.$anchor.addClass( 'oo-ui-popupWidget-anchor' );
12740 this.$head
12741 .addClass( 'oo-ui-popupWidget-head' )
12742 .append( this.$label, this.closeButton.$element );
12743 if ( !config.head ) {
12744 this.$head.addClass( 'oo-ui-element-hidden' );
12745 }
12746 this.$popup
12747 .addClass( 'oo-ui-popupWidget-popup' )
12748 .append( this.$head, this.$body );
12749 this.$element
12750 .addClass( 'oo-ui-popupWidget' )
12751 .append( this.$popup, this.$anchor );
12752 // Move content, which was added to #$element by OO.ui.Widget, to the body
12753 if ( config.$content instanceof jQuery ) {
12754 this.$body.append( config.$content );
12755 }
12756 if ( config.padded ) {
12757 this.$body.addClass( 'oo-ui-popupWidget-body-padded' );
12758 }
12759
12760 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
12761 // that reference properties not initialized at that time of parent class construction
12762 // TODO: Find a better way to handle post-constructor setup
12763 this.visible = false;
12764 this.$element.addClass( 'oo-ui-element-hidden' );
12765 };
12766
12767 /* Setup */
12768
12769 OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
12770 OO.mixinClass( OO.ui.PopupWidget, OO.ui.LabelElement );
12771 OO.mixinClass( OO.ui.PopupWidget, OO.ui.ClippableElement );
12772
12773 /* Methods */
12774
12775 /**
12776 * Handles mouse down events.
12777 *
12778 * @private
12779 * @param {MouseEvent} e Mouse down event
12780 */
12781 OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
12782 if (
12783 this.isVisible() &&
12784 !$.contains( this.$element[ 0 ], e.target ) &&
12785 ( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length )
12786 ) {
12787 this.toggle( false );
12788 }
12789 };
12790
12791 /**
12792 * Bind mouse down listener.
12793 *
12794 * @private
12795 */
12796 OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
12797 // Capture clicks outside popup
12798 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
12799 };
12800
12801 /**
12802 * Handles close button click events.
12803 *
12804 * @private
12805 */
12806 OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
12807 if ( this.isVisible() ) {
12808 this.toggle( false );
12809 }
12810 };
12811
12812 /**
12813 * Unbind mouse down listener.
12814 *
12815 * @private
12816 */
12817 OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
12818 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
12819 };
12820
12821 /**
12822 * Handles key down events.
12823 *
12824 * @private
12825 * @param {KeyboardEvent} e Key down event
12826 */
12827 OO.ui.PopupWidget.prototype.onDocumentKeyDown = function ( e ) {
12828 if (
12829 e.which === OO.ui.Keys.ESCAPE &&
12830 this.isVisible()
12831 ) {
12832 this.toggle( false );
12833 e.preventDefault();
12834 e.stopPropagation();
12835 }
12836 };
12837
12838 /**
12839 * Bind key down listener.
12840 *
12841 * @private
12842 */
12843 OO.ui.PopupWidget.prototype.bindKeyDownListener = function () {
12844 this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
12845 };
12846
12847 /**
12848 * Unbind key down listener.
12849 *
12850 * @private
12851 */
12852 OO.ui.PopupWidget.prototype.unbindKeyDownListener = function () {
12853 this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
12854 };
12855
12856 /**
12857 * Show, hide, or toggle the visibility of the anchor.
12858 *
12859 * @param {boolean} [show] Show anchor, omit to toggle
12860 */
12861 OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) {
12862 show = show === undefined ? !this.anchored : !!show;
12863
12864 if ( this.anchored !== show ) {
12865 if ( show ) {
12866 this.$element.addClass( 'oo-ui-popupWidget-anchored' );
12867 } else {
12868 this.$element.removeClass( 'oo-ui-popupWidget-anchored' );
12869 }
12870 this.anchored = show;
12871 }
12872 };
12873
12874 /**
12875 * Check if the anchor is visible.
12876 *
12877 * @return {boolean} Anchor is visible
12878 */
12879 OO.ui.PopupWidget.prototype.hasAnchor = function () {
12880 return this.anchor;
12881 };
12882
12883 /**
12884 * @inheritdoc
12885 */
12886 OO.ui.PopupWidget.prototype.toggle = function ( show ) {
12887 show = show === undefined ? !this.isVisible() : !!show;
12888
12889 var change = show !== this.isVisible();
12890
12891 // Parent method
12892 OO.ui.PopupWidget.super.prototype.toggle.call( this, show );
12893
12894 if ( change ) {
12895 if ( show ) {
12896 if ( this.autoClose ) {
12897 this.bindMouseDownListener();
12898 this.bindKeyDownListener();
12899 }
12900 this.updateDimensions();
12901 this.toggleClipping( true );
12902 } else {
12903 this.toggleClipping( false );
12904 if ( this.autoClose ) {
12905 this.unbindMouseDownListener();
12906 this.unbindKeyDownListener();
12907 }
12908 }
12909 }
12910
12911 return this;
12912 };
12913
12914 /**
12915 * Set the size of the popup.
12916 *
12917 * Changing the size may also change the popup's position depending on the alignment.
12918 *
12919 * @param {number} width Width in pixels
12920 * @param {number} height Height in pixels
12921 * @param {boolean} [transition=false] Use a smooth transition
12922 * @chainable
12923 */
12924 OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) {
12925 this.width = width;
12926 this.height = height !== undefined ? height : null;
12927 if ( this.isVisible() ) {
12928 this.updateDimensions( transition );
12929 }
12930 };
12931
12932 /**
12933 * Update the size and position.
12934 *
12935 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
12936 * be called automatically.
12937 *
12938 * @param {boolean} [transition=false] Use a smooth transition
12939 * @chainable
12940 */
12941 OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
12942 var popupOffset, originOffset, containerLeft, containerWidth, containerRight,
12943 popupLeft, popupRight, overlapLeft, overlapRight, anchorWidth,
12944 widget = this;
12945
12946 if ( !this.$container ) {
12947 // Lazy-initialize $container if not specified in constructor
12948 this.$container = $( this.getClosestScrollableElementContainer() );
12949 }
12950
12951 // Set height and width before measuring things, since it might cause our measurements
12952 // to change (e.g. due to scrollbars appearing or disappearing)
12953 this.$popup.css( {
12954 width: this.width,
12955 height: this.height !== null ? this.height : 'auto'
12956 } );
12957
12958 // Compute initial popupOffset based on alignment
12959 popupOffset = this.width * ( { left: 0, center: -0.5, right: -1 } )[ this.align ];
12960
12961 // Figure out if this will cause the popup to go beyond the edge of the container
12962 originOffset = this.$element.offset().left;
12963 containerLeft = this.$container.offset().left;
12964 containerWidth = this.$container.innerWidth();
12965 containerRight = containerLeft + containerWidth;
12966 popupLeft = popupOffset - this.containerPadding;
12967 popupRight = popupOffset + this.containerPadding + this.width + this.containerPadding;
12968 overlapLeft = ( originOffset + popupLeft ) - containerLeft;
12969 overlapRight = containerRight - ( originOffset + popupRight );
12970
12971 // Adjust offset to make the popup not go beyond the edge, if needed
12972 if ( overlapRight < 0 ) {
12973 popupOffset += overlapRight;
12974 } else if ( overlapLeft < 0 ) {
12975 popupOffset -= overlapLeft;
12976 }
12977
12978 // Adjust offset to avoid anchor being rendered too close to the edge
12979 // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
12980 // TODO: Find a measurement that works for CSS anchors and image anchors
12981 anchorWidth = this.$anchor[ 0 ].scrollWidth * 2;
12982 if ( popupOffset + this.width < anchorWidth ) {
12983 popupOffset = anchorWidth - this.width;
12984 } else if ( -popupOffset < anchorWidth ) {
12985 popupOffset = -anchorWidth;
12986 }
12987
12988 // Prevent transition from being interrupted
12989 clearTimeout( this.transitionTimeout );
12990 if ( transition ) {
12991 // Enable transition
12992 this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
12993 }
12994
12995 // Position body relative to anchor
12996 this.$popup.css( 'margin-left', popupOffset );
12997
12998 if ( transition ) {
12999 // Prevent transitioning after transition is complete
13000 this.transitionTimeout = setTimeout( function () {
13001 widget.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
13002 }, 200 );
13003 } else {
13004 // Prevent transitioning immediately
13005 this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
13006 }
13007
13008 // Reevaluate clipping state since we've relocated and resized the popup
13009 this.clip();
13010
13011 return this;
13012 };
13013
13014 /**
13015 * Progress bars visually display the status of an operation, such as a download,
13016 * and can be either determinate or indeterminate:
13017 *
13018 * - **determinate** process bars show the percent of an operation that is complete.
13019 *
13020 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
13021 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
13022 * not use percentages.
13023 *
13024 * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
13025 *
13026 * @example
13027 * // Examples of determinate and indeterminate progress bars.
13028 * var progressBar1=new OO.ui.ProgressBarWidget( {
13029 * progress: 33
13030 * } );
13031 *
13032 * var progressBar2=new OO.ui.ProgressBarWidget( {
13033 * progress: false
13034 * } );
13035 * // Create a FieldsetLayout to layout progress bars
13036 * var fieldset = new OO.ui.FieldsetLayout;
13037 * fieldset.addItems( [
13038 * new OO.ui.FieldLayout( progressBar1, {label : 'Determinate', align : 'top'}),
13039 * new OO.ui.FieldLayout( progressBar2, {label : 'Indeterminate', align : 'top'})
13040 * ] );
13041 * $( 'body' ).append( fieldset.$element );
13042 *
13043 * @class
13044 * @extends OO.ui.Widget
13045 *
13046 * @constructor
13047 * @param {Object} [config] Configuration options
13048 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
13049 * To create a determinate progress bar, specify a number that reflects the initial percent complete.
13050 * By default, the progress bar is indeterminate.
13051 */
13052 OO.ui.ProgressBarWidget = function OoUiProgressBarWidget( config ) {
13053 // Configuration initialization
13054 config = config || {};
13055
13056 // Parent constructor
13057 OO.ui.ProgressBarWidget.super.call( this, config );
13058
13059 // Properties
13060 this.$bar = $( '<div>' );
13061 this.progress = null;
13062
13063 // Initialization
13064 this.setProgress( config.progress !== undefined ? config.progress : false );
13065 this.$bar.addClass( 'oo-ui-progressBarWidget-bar' );
13066 this.$element
13067 .attr( {
13068 role: 'progressbar',
13069 'aria-valuemin': 0,
13070 'aria-valuemax': 100
13071 } )
13072 .addClass( 'oo-ui-progressBarWidget' )
13073 .append( this.$bar );
13074 };
13075
13076 /* Setup */
13077
13078 OO.inheritClass( OO.ui.ProgressBarWidget, OO.ui.Widget );
13079
13080 /* Static Properties */
13081
13082 OO.ui.ProgressBarWidget.static.tagName = 'div';
13083
13084 /* Methods */
13085
13086 /**
13087 * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
13088 *
13089 * @return {number|boolean} Progress percent
13090 */
13091 OO.ui.ProgressBarWidget.prototype.getProgress = function () {
13092 return this.progress;
13093 };
13094
13095 /**
13096 * Set the percent of the process completed or `false` for an indeterminate process.
13097 *
13098 * @param {number|boolean} progress Progress percent or `false` for indeterminate
13099 */
13100 OO.ui.ProgressBarWidget.prototype.setProgress = function ( progress ) {
13101 this.progress = progress;
13102
13103 if ( progress !== false ) {
13104 this.$bar.css( 'width', this.progress + '%' );
13105 this.$element.attr( 'aria-valuenow', this.progress );
13106 } else {
13107 this.$bar.css( 'width', '' );
13108 this.$element.removeAttr( 'aria-valuenow' );
13109 }
13110 this.$element.toggleClass( 'oo-ui-progressBarWidget-indeterminate', !progress );
13111 };
13112
13113 /**
13114 * Search widget.
13115 *
13116 * Search widgets combine a query input, placed above, and a results selection widget, placed below.
13117 * Results are cleared and populated each time the query is changed.
13118 *
13119 * @class
13120 * @extends OO.ui.Widget
13121 *
13122 * @constructor
13123 * @param {Object} [config] Configuration options
13124 * @cfg {string|jQuery} [placeholder] Placeholder text for query input
13125 * @cfg {string} [value] Initial query value
13126 */
13127 OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
13128 // Configuration initialization
13129 config = config || {};
13130
13131 // Parent constructor
13132 OO.ui.SearchWidget.super.call( this, config );
13133
13134 // Properties
13135 this.query = new OO.ui.TextInputWidget( {
13136 icon: 'search',
13137 placeholder: config.placeholder,
13138 value: config.value
13139 } );
13140 this.results = new OO.ui.SelectWidget();
13141 this.$query = $( '<div>' );
13142 this.$results = $( '<div>' );
13143
13144 // Events
13145 this.query.connect( this, {
13146 change: 'onQueryChange',
13147 enter: 'onQueryEnter'
13148 } );
13149 this.results.connect( this, {
13150 highlight: 'onResultsHighlight',
13151 select: 'onResultsSelect'
13152 } );
13153 this.query.$input.on( 'keydown', this.onQueryKeydown.bind( this ) );
13154
13155 // Initialization
13156 this.$query
13157 .addClass( 'oo-ui-searchWidget-query' )
13158 .append( this.query.$element );
13159 this.$results
13160 .addClass( 'oo-ui-searchWidget-results' )
13161 .append( this.results.$element );
13162 this.$element
13163 .addClass( 'oo-ui-searchWidget' )
13164 .append( this.$results, this.$query );
13165 };
13166
13167 /* Setup */
13168
13169 OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget );
13170
13171 /* Events */
13172
13173 /**
13174 * @event highlight
13175 * @param {Object|null} item Item data or null if no item is highlighted
13176 */
13177
13178 /**
13179 * @event select
13180 * @param {Object|null} item Item data or null if no item is selected
13181 */
13182
13183 /* Methods */
13184
13185 /**
13186 * Handle query key down events.
13187 *
13188 * @param {jQuery.Event} e Key down event
13189 */
13190 OO.ui.SearchWidget.prototype.onQueryKeydown = function ( e ) {
13191 var highlightedItem, nextItem,
13192 dir = e.which === OO.ui.Keys.DOWN ? 1 : ( e.which === OO.ui.Keys.UP ? -1 : 0 );
13193
13194 if ( dir ) {
13195 highlightedItem = this.results.getHighlightedItem();
13196 if ( !highlightedItem ) {
13197 highlightedItem = this.results.getSelectedItem();
13198 }
13199 nextItem = this.results.getRelativeSelectableItem( highlightedItem, dir );
13200 this.results.highlightItem( nextItem );
13201 nextItem.scrollElementIntoView();
13202 }
13203 };
13204
13205 /**
13206 * Handle select widget select events.
13207 *
13208 * Clears existing results. Subclasses should repopulate items according to new query.
13209 *
13210 * @param {string} value New value
13211 */
13212 OO.ui.SearchWidget.prototype.onQueryChange = function () {
13213 // Reset
13214 this.results.clearItems();
13215 };
13216
13217 /**
13218 * Handle select widget enter key events.
13219 *
13220 * Selects highlighted item.
13221 *
13222 * @param {string} value New value
13223 */
13224 OO.ui.SearchWidget.prototype.onQueryEnter = function () {
13225 // Reset
13226 this.results.selectItem( this.results.getHighlightedItem() );
13227 };
13228
13229 /**
13230 * Handle select widget highlight events.
13231 *
13232 * @param {OO.ui.OptionWidget} item Highlighted item
13233 * @fires highlight
13234 */
13235 OO.ui.SearchWidget.prototype.onResultsHighlight = function ( item ) {
13236 this.emit( 'highlight', item ? item.getData() : null );
13237 };
13238
13239 /**
13240 * Handle select widget select events.
13241 *
13242 * @param {OO.ui.OptionWidget} item Selected item
13243 * @fires select
13244 */
13245 OO.ui.SearchWidget.prototype.onResultsSelect = function ( item ) {
13246 this.emit( 'select', item ? item.getData() : null );
13247 };
13248
13249 /**
13250 * Get the query input.
13251 *
13252 * @return {OO.ui.TextInputWidget} Query input
13253 */
13254 OO.ui.SearchWidget.prototype.getQuery = function () {
13255 return this.query;
13256 };
13257
13258 /**
13259 * Get the results list.
13260 *
13261 * @return {OO.ui.SelectWidget} Select list
13262 */
13263 OO.ui.SearchWidget.prototype.getResults = function () {
13264 return this.results;
13265 };
13266
13267 /**
13268 * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
13269 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
13270 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
13271 * menu selects}.
13272 *
13273 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
13274 * information, please see the [OOjs UI documentation on MediaWiki][1].
13275 *
13276 * @example
13277 * // Example of a select widget with three options
13278 * var select=new OO.ui.SelectWidget( {
13279 * items: [
13280 * new OO.ui.OptionWidget( {
13281 * data: 'a',
13282 * label: 'Option One',
13283 * } ),
13284 * new OO.ui.OptionWidget( {
13285 * data: 'b',
13286 * label: 'Option Two',
13287 * } ),
13288 * new OO.ui.OptionWidget( {
13289 * data: 'c',
13290 * label: 'Option Three',
13291 * } ),
13292 * ]
13293 * } );
13294 * $('body').append(select.$element);
13295 *
13296 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
13297 *
13298 * @class
13299 * @extends OO.ui.Widget
13300 * @mixins OO.ui.GroupElement
13301 *
13302 * @constructor
13303 * @param {Object} [config] Configuration options
13304 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
13305 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
13306 * the [OOjs UI documentation on MediaWiki] [2] for examples.
13307 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
13308 */
13309 OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
13310 // Configuration initialization
13311 config = config || {};
13312
13313 // Parent constructor
13314 OO.ui.SelectWidget.super.call( this, config );
13315
13316 // Mixin constructors
13317 OO.ui.GroupWidget.call( this, $.extend( {}, config, { $group: this.$element } ) );
13318
13319 // Properties
13320 this.pressed = false;
13321 this.selecting = null;
13322 this.onMouseUpHandler = this.onMouseUp.bind( this );
13323 this.onMouseMoveHandler = this.onMouseMove.bind( this );
13324 this.onKeyDownHandler = this.onKeyDown.bind( this );
13325
13326 // Events
13327 this.$element.on( {
13328 mousedown: this.onMouseDown.bind( this ),
13329 mouseover: this.onMouseOver.bind( this ),
13330 mouseleave: this.onMouseLeave.bind( this )
13331 } );
13332
13333 // Initialization
13334 this.$element
13335 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
13336 .attr( 'role', 'listbox' );
13337 if ( Array.isArray( config.items ) ) {
13338 this.addItems( config.items );
13339 }
13340 };
13341
13342 /* Setup */
13343
13344 OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
13345
13346 // Need to mixin base class as well
13347 OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupElement );
13348 OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupWidget );
13349
13350 /* Events */
13351
13352 /**
13353 * @event highlight
13354 *
13355 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
13356 *
13357 * @param {OO.ui.OptionWidget|null} item Highlighted item
13358 */
13359
13360 /**
13361 * @event press
13362 *
13363 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
13364 * pressed state of an option.
13365 *
13366 * @param {OO.ui.OptionWidget|null} item Pressed item
13367 */
13368
13369 /**
13370 * @event select
13371 *
13372 * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
13373 *
13374 * @param {OO.ui.OptionWidget|null} item Selected item
13375 */
13376
13377 /**
13378 * @event choose
13379 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
13380 * @param {OO.ui.OptionWidget|null} item Chosen item
13381 */
13382
13383 /**
13384 * @event add
13385 *
13386 * An `add` event is emitted when options are added to the select with the #addItems method.
13387 *
13388 * @param {OO.ui.OptionWidget[]} items Added items
13389 * @param {number} index Index of insertion point
13390 */
13391
13392 /**
13393 * @event remove
13394 *
13395 * A `remove` event is emitted when options are removed from the select with the #clearItems
13396 * or #removeItems methods.
13397 *
13398 * @param {OO.ui.OptionWidget[]} items Removed items
13399 */
13400
13401 /* Methods */
13402
13403 /**
13404 * Handle mouse down events.
13405 *
13406 * @private
13407 * @param {jQuery.Event} e Mouse down event
13408 */
13409 OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
13410 var item;
13411
13412 if ( !this.isDisabled() && e.which === 1 ) {
13413 this.togglePressed( true );
13414 item = this.getTargetItem( e );
13415 if ( item && item.isSelectable() ) {
13416 this.pressItem( item );
13417 this.selecting = item;
13418 this.getElementDocument().addEventListener(
13419 'mouseup',
13420 this.onMouseUpHandler,
13421 true
13422 );
13423 this.getElementDocument().addEventListener(
13424 'mousemove',
13425 this.onMouseMoveHandler,
13426 true
13427 );
13428 }
13429 }
13430 return false;
13431 };
13432
13433 /**
13434 * Handle mouse up events.
13435 *
13436 * @private
13437 * @param {jQuery.Event} e Mouse up event
13438 */
13439 OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
13440 var item;
13441
13442 this.togglePressed( false );
13443 if ( !this.selecting ) {
13444 item = this.getTargetItem( e );
13445 if ( item && item.isSelectable() ) {
13446 this.selecting = item;
13447 }
13448 }
13449 if ( !this.isDisabled() && e.which === 1 && this.selecting ) {
13450 this.pressItem( null );
13451 this.chooseItem( this.selecting );
13452 this.selecting = null;
13453 }
13454
13455 this.getElementDocument().removeEventListener(
13456 'mouseup',
13457 this.onMouseUpHandler,
13458 true
13459 );
13460 this.getElementDocument().removeEventListener(
13461 'mousemove',
13462 this.onMouseMoveHandler,
13463 true
13464 );
13465
13466 return false;
13467 };
13468
13469 /**
13470 * Handle mouse move events.
13471 *
13472 * @private
13473 * @param {jQuery.Event} e Mouse move event
13474 */
13475 OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
13476 var item;
13477
13478 if ( !this.isDisabled() && this.pressed ) {
13479 item = this.getTargetItem( e );
13480 if ( item && item !== this.selecting && item.isSelectable() ) {
13481 this.pressItem( item );
13482 this.selecting = item;
13483 }
13484 }
13485 return false;
13486 };
13487
13488 /**
13489 * Handle mouse over events.
13490 *
13491 * @private
13492 * @param {jQuery.Event} e Mouse over event
13493 */
13494 OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
13495 var item;
13496
13497 if ( !this.isDisabled() ) {
13498 item = this.getTargetItem( e );
13499 this.highlightItem( item && item.isHighlightable() ? item : null );
13500 }
13501 return false;
13502 };
13503
13504 /**
13505 * Handle mouse leave events.
13506 *
13507 * @private
13508 * @param {jQuery.Event} e Mouse over event
13509 */
13510 OO.ui.SelectWidget.prototype.onMouseLeave = function () {
13511 if ( !this.isDisabled() ) {
13512 this.highlightItem( null );
13513 }
13514 return false;
13515 };
13516
13517 /**
13518 * Handle key down events.
13519 *
13520 * @protected
13521 * @param {jQuery.Event} e Key down event
13522 */
13523 OO.ui.SelectWidget.prototype.onKeyDown = function ( e ) {
13524 var nextItem,
13525 handled = false,
13526 currentItem = this.getHighlightedItem() || this.getSelectedItem();
13527
13528 if ( !this.isDisabled() && this.isVisible() ) {
13529 switch ( e.keyCode ) {
13530 case OO.ui.Keys.ENTER:
13531 if ( currentItem && currentItem.constructor.static.highlightable ) {
13532 // Was only highlighted, now let's select it. No-op if already selected.
13533 this.chooseItem( currentItem );
13534 handled = true;
13535 }
13536 break;
13537 case OO.ui.Keys.UP:
13538 case OO.ui.Keys.LEFT:
13539 nextItem = this.getRelativeSelectableItem( currentItem, -1 );
13540 handled = true;
13541 break;
13542 case OO.ui.Keys.DOWN:
13543 case OO.ui.Keys.RIGHT:
13544 nextItem = this.getRelativeSelectableItem( currentItem, 1 );
13545 handled = true;
13546 break;
13547 case OO.ui.Keys.ESCAPE:
13548 case OO.ui.Keys.TAB:
13549 if ( currentItem && currentItem.constructor.static.highlightable ) {
13550 currentItem.setHighlighted( false );
13551 }
13552 this.unbindKeyDownListener();
13553 // Don't prevent tabbing away / defocusing
13554 handled = false;
13555 break;
13556 }
13557
13558 if ( nextItem ) {
13559 if ( nextItem.constructor.static.highlightable ) {
13560 this.highlightItem( nextItem );
13561 } else {
13562 this.chooseItem( nextItem );
13563 }
13564 nextItem.scrollElementIntoView();
13565 }
13566
13567 if ( handled ) {
13568 // Can't just return false, because e is not always a jQuery event
13569 e.preventDefault();
13570 e.stopPropagation();
13571 }
13572 }
13573 };
13574
13575 /**
13576 * Bind key down listener.
13577 *
13578 * @protected
13579 */
13580 OO.ui.SelectWidget.prototype.bindKeyDownListener = function () {
13581 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
13582 };
13583
13584 /**
13585 * Unbind key down listener.
13586 *
13587 * @protected
13588 */
13589 OO.ui.SelectWidget.prototype.unbindKeyDownListener = function () {
13590 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
13591 };
13592
13593 /**
13594 * Get the closest item to a jQuery.Event.
13595 *
13596 * @private
13597 * @param {jQuery.Event} e
13598 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
13599 */
13600 OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) {
13601 var $item = $( e.target ).closest( '.oo-ui-optionWidget' );
13602 if ( $item.length ) {
13603 return $item.data( 'oo-ui-optionWidget' );
13604 }
13605 return null;
13606 };
13607
13608 /**
13609 * Get selected item.
13610 *
13611 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
13612 */
13613 OO.ui.SelectWidget.prototype.getSelectedItem = function () {
13614 var i, len;
13615
13616 for ( i = 0, len = this.items.length; i < len; i++ ) {
13617 if ( this.items[ i ].isSelected() ) {
13618 return this.items[ i ];
13619 }
13620 }
13621 return null;
13622 };
13623
13624 /**
13625 * Get highlighted item.
13626 *
13627 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
13628 */
13629 OO.ui.SelectWidget.prototype.getHighlightedItem = function () {
13630 var i, len;
13631
13632 for ( i = 0, len = this.items.length; i < len; i++ ) {
13633 if ( this.items[ i ].isHighlighted() ) {
13634 return this.items[ i ];
13635 }
13636 }
13637 return null;
13638 };
13639
13640 /**
13641 * Toggle pressed state.
13642 *
13643 * Press is a state that occurs when a user mouses down on an item, but
13644 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
13645 * until the user releases the mouse.
13646 *
13647 * @param {boolean} pressed An option is being pressed
13648 */
13649 OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) {
13650 if ( pressed === undefined ) {
13651 pressed = !this.pressed;
13652 }
13653 if ( pressed !== this.pressed ) {
13654 this.$element
13655 .toggleClass( 'oo-ui-selectWidget-pressed', pressed )
13656 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed );
13657 this.pressed = pressed;
13658 }
13659 };
13660
13661 /**
13662 * Highlight an option. If the `item` param is omitted, no options will be highlighted
13663 * and any existing highlight will be removed. The highlight is mutually exclusive.
13664 *
13665 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
13666 * @fires highlight
13667 * @chainable
13668 */
13669 OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
13670 var i, len, highlighted,
13671 changed = false;
13672
13673 for ( i = 0, len = this.items.length; i < len; i++ ) {
13674 highlighted = this.items[ i ] === item;
13675 if ( this.items[ i ].isHighlighted() !== highlighted ) {
13676 this.items[ i ].setHighlighted( highlighted );
13677 changed = true;
13678 }
13679 }
13680 if ( changed ) {
13681 this.emit( 'highlight', item );
13682 }
13683
13684 return this;
13685 };
13686
13687 /**
13688 * Programmatically select an option by its reference. If the `item` parameter is omitted,
13689 * all options will be deselected.
13690 *
13691 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
13692 * @fires select
13693 * @chainable
13694 */
13695 OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
13696 var i, len, selected,
13697 changed = false;
13698
13699 for ( i = 0, len = this.items.length; i < len; i++ ) {
13700 selected = this.items[ i ] === item;
13701 if ( this.items[ i ].isSelected() !== selected ) {
13702 this.items[ i ].setSelected( selected );
13703 changed = true;
13704 }
13705 }
13706 if ( changed ) {
13707 this.emit( 'select', item );
13708 }
13709
13710 return this;
13711 };
13712
13713 /**
13714 * Press an item.
13715 *
13716 * Press is a state that occurs when a user mouses down on an item, but has not
13717 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
13718 * releases the mouse.
13719 *
13720 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
13721 * @fires press
13722 * @chainable
13723 */
13724 OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
13725 var i, len, pressed,
13726 changed = false;
13727
13728 for ( i = 0, len = this.items.length; i < len; i++ ) {
13729 pressed = this.items[ i ] === item;
13730 if ( this.items[ i ].isPressed() !== pressed ) {
13731 this.items[ i ].setPressed( pressed );
13732 changed = true;
13733 }
13734 }
13735 if ( changed ) {
13736 this.emit( 'press', item );
13737 }
13738
13739 return this;
13740 };
13741
13742 /**
13743 * Choose an item.
13744 *
13745 * Note that ‘choose’ should never be modified programmatically. A user can choose
13746 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
13747 * use the #selectItem method.
13748 *
13749 * This method is identical to #selectItem, but may vary in subclasses that take additional action
13750 * when users choose an item with the keyboard or mouse.
13751 *
13752 * @param {OO.ui.OptionWidget} item Item to choose
13753 * @fires choose
13754 * @chainable
13755 */
13756 OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
13757 this.selectItem( item );
13758 this.emit( 'choose', item );
13759
13760 return this;
13761 };
13762
13763 /**
13764 * Get an option by its position relative to the specified item (or to the start of the option array,
13765 * if item is `null`). The direction in which to search through the option array is specified with a
13766 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
13767 * `null` if there are no options in the array.
13768 *
13769 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
13770 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
13771 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
13772 */
13773 OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction ) {
13774 var currentIndex, nextIndex, i,
13775 increase = direction > 0 ? 1 : -1,
13776 len = this.items.length;
13777
13778 if ( item instanceof OO.ui.OptionWidget ) {
13779 currentIndex = $.inArray( item, this.items );
13780 nextIndex = ( currentIndex + increase + len ) % len;
13781 } else {
13782 // If no item is selected and moving forward, start at the beginning.
13783 // If moving backward, start at the end.
13784 nextIndex = direction > 0 ? 0 : len - 1;
13785 }
13786
13787 for ( i = 0; i < len; i++ ) {
13788 item = this.items[ nextIndex ];
13789 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
13790 return item;
13791 }
13792 nextIndex = ( nextIndex + increase + len ) % len;
13793 }
13794 return null;
13795 };
13796
13797 /**
13798 * Get the next selectable item or `null` if there are no selectable items.
13799 * Disabled options and menu-section markers and breaks are not selectable.
13800 *
13801 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
13802 */
13803 OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
13804 var i, len, item;
13805
13806 for ( i = 0, len = this.items.length; i < len; i++ ) {
13807 item = this.items[ i ];
13808 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
13809 return item;
13810 }
13811 }
13812
13813 return null;
13814 };
13815
13816 /**
13817 * Add an array of options to the select. Optionally, an index number can be used to
13818 * specify an insertion point.
13819 *
13820 * @param {OO.ui.OptionWidget[]} items Items to add
13821 * @param {number} [index] Index to insert items after
13822 * @fires add
13823 * @chainable
13824 */
13825 OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
13826 // Mixin method
13827 OO.ui.GroupWidget.prototype.addItems.call( this, items, index );
13828
13829 // Always provide an index, even if it was omitted
13830 this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index );
13831
13832 return this;
13833 };
13834
13835 /**
13836 * Remove the specified array of options from the select. Options will be detached
13837 * from the DOM, not removed, so they can be reused later. To remove all options from
13838 * the select, you may wish to use the #clearItems method instead.
13839 *
13840 * @param {OO.ui.OptionWidget[]} items Items to remove
13841 * @fires remove
13842 * @chainable
13843 */
13844 OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
13845 var i, len, item;
13846
13847 // Deselect items being removed
13848 for ( i = 0, len = items.length; i < len; i++ ) {
13849 item = items[ i ];
13850 if ( item.isSelected() ) {
13851 this.selectItem( null );
13852 }
13853 }
13854
13855 // Mixin method
13856 OO.ui.GroupWidget.prototype.removeItems.call( this, items );
13857
13858 this.emit( 'remove', items );
13859
13860 return this;
13861 };
13862
13863 /**
13864 * Clear all options from the select. Options will be detached from the DOM, not removed,
13865 * so that they can be reused later. To remove a subset of options from the select, use
13866 * the #removeItems method.
13867 *
13868 * @fires remove
13869 * @chainable
13870 */
13871 OO.ui.SelectWidget.prototype.clearItems = function () {
13872 var items = this.items.slice();
13873
13874 // Mixin method
13875 OO.ui.GroupWidget.prototype.clearItems.call( this );
13876
13877 // Clear selection
13878 this.selectItem( null );
13879
13880 this.emit( 'remove', items );
13881
13882 return this;
13883 };
13884
13885 /**
13886 * ButtonSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains
13887 * button options and is used together with
13888 * OO.ui.ButtonOptionWidget. The ButtonSelectWidget provides an interface for
13889 * highlighting, choosing, and selecting mutually exclusive options. Please see
13890 * the [OOjs UI documentation on MediaWiki] [1] for more information.
13891 *
13892 * @example
13893 * // Example: A ButtonSelectWidget that contains three ButtonOptionWidgets
13894 * var option1 = new OO.ui.ButtonOptionWidget( {
13895 * data: 1,
13896 * label: 'Option 1',
13897 * title:'Button option 1'
13898 * } );
13899 *
13900 * var option2 = new OO.ui.ButtonOptionWidget( {
13901 * data: 2,
13902 * label: 'Option 2',
13903 * title:'Button option 2'
13904 * } );
13905 *
13906 * var option3 = new OO.ui.ButtonOptionWidget( {
13907 * data: 3,
13908 * label: 'Option 3',
13909 * title:'Button option 3'
13910 * } );
13911 *
13912 * var buttonSelect=new OO.ui.ButtonSelectWidget( {
13913 * items: [option1, option2, option3]
13914 * } );
13915 * $('body').append(buttonSelect.$element);
13916 *
13917 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
13918 *
13919 * @class
13920 * @extends OO.ui.SelectWidget
13921 * @mixins OO.ui.TabIndexedElement
13922 *
13923 * @constructor
13924 * @param {Object} [config] Configuration options
13925 */
13926 OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
13927 // Parent constructor
13928 OO.ui.ButtonSelectWidget.super.call( this, config );
13929
13930 // Mixin constructors
13931 OO.ui.TabIndexedElement.call( this, config );
13932
13933 // Events
13934 this.$element.on( {
13935 focus: this.bindKeyDownListener.bind( this ),
13936 blur: this.unbindKeyDownListener.bind( this )
13937 } );
13938
13939 // Initialization
13940 this.$element.addClass( 'oo-ui-buttonSelectWidget' );
13941 };
13942
13943 /* Setup */
13944
13945 OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
13946 OO.mixinClass( OO.ui.ButtonSelectWidget, OO.ui.TabIndexedElement );
13947
13948 /**
13949 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
13950 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
13951 * an interface for adding, removing and selecting options.
13952 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
13953 *
13954 * @example
13955 * // A RadioSelectWidget with RadioOptions.
13956 * var option1 = new OO.ui.RadioOptionWidget( {
13957 * data: 'a',
13958 * label: 'Selected radio option'
13959 * } );
13960 *
13961 * var option2 = new OO.ui.RadioOptionWidget( {
13962 * data: 'b',
13963 * label: 'Unselected radio option'
13964 * } );
13965 *
13966 * var radioSelect=new OO.ui.RadioSelectWidget( {
13967 * items: [option1, option2]
13968 * } );
13969 *
13970 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
13971 * radioSelect.selectItem( option1 );
13972 *
13973 * $('body').append(radioSelect.$element);
13974 *
13975 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
13976
13977 *
13978 * @class
13979 * @extends OO.ui.SelectWidget
13980 * @mixins OO.ui.TabIndexedElement
13981 *
13982 * @constructor
13983 * @param {Object} [config] Configuration options
13984 */
13985 OO.ui.RadioSelectWidget = function OoUiRadioSelectWidget( config ) {
13986 // Parent constructor
13987 OO.ui.RadioSelectWidget.super.call( this, config );
13988
13989 // Mixin constructors
13990 OO.ui.TabIndexedElement.call( this, config );
13991
13992 // Events
13993 this.$element.on( {
13994 focus: this.bindKeyDownListener.bind( this ),
13995 blur: this.unbindKeyDownListener.bind( this )
13996 } );
13997
13998 // Initialization
13999 this.$element.addClass( 'oo-ui-radioSelectWidget' );
14000 };
14001
14002 /* Setup */
14003
14004 OO.inheritClass( OO.ui.RadioSelectWidget, OO.ui.SelectWidget );
14005 OO.mixinClass( OO.ui.RadioSelectWidget, OO.ui.TabIndexedElement );
14006
14007 /**
14008 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
14009 * is used together with OO.ui.MenuOptionWidget. See {@link OO.ui.DropdownWidget DropdownWidget} and
14010 * {@link OO.ui.ComboBoxWidget ComboBoxWidget} for examples of interfaces that contain menus.
14011 * MenuSelectWidgets themselves are not designed to be instantiated directly, rather subclassed
14012 * and customized to be opened, closed, and displayed as needed.
14013 *
14014 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
14015 * mouse outside the menu.
14016 *
14017 * Menus also have support for keyboard interaction:
14018 *
14019 * - Enter/Return key: choose and select a menu option
14020 * - Up-arrow key: highlight the previous menu option
14021 * - Down-arrow key: highlight the next menu option
14022 * - Esc key: hide the menu
14023 *
14024 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
14025 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
14026 *
14027 * @class
14028 * @extends OO.ui.SelectWidget
14029 * @mixins OO.ui.ClippableElement
14030 *
14031 * @constructor
14032 * @param {Object} [config] Configuration options
14033 * @cfg {OO.ui.TextInputWidget} [input] Input to bind keyboard handlers to
14034 * @cfg {OO.ui.Widget} [widget] Widget to bind mouse handlers to
14035 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu
14036 */
14037 OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) {
14038 // Configuration initialization
14039 config = config || {};
14040
14041 // Parent constructor
14042 OO.ui.MenuSelectWidget.super.call( this, config );
14043
14044 // Mixin constructors
14045 OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
14046
14047 // Properties
14048 this.newItems = null;
14049 this.autoHide = config.autoHide === undefined || !!config.autoHide;
14050 this.$input = config.input ? config.input.$input : null;
14051 this.$widget = config.widget ? config.widget.$element : null;
14052 this.onDocumentMouseDownHandler = this.onDocumentMouseDown.bind( this );
14053
14054 // Initialization
14055 this.$element
14056 .addClass( 'oo-ui-menuSelectWidget' )
14057 .attr( 'role', 'menu' );
14058
14059 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
14060 // that reference properties not initialized at that time of parent class construction
14061 // TODO: Find a better way to handle post-constructor setup
14062 this.visible = false;
14063 this.$element.addClass( 'oo-ui-element-hidden' );
14064 };
14065
14066 /* Setup */
14067
14068 OO.inheritClass( OO.ui.MenuSelectWidget, OO.ui.SelectWidget );
14069 OO.mixinClass( OO.ui.MenuSelectWidget, OO.ui.ClippableElement );
14070
14071 /* Methods */
14072
14073 /**
14074 * Handles document mouse down events.
14075 *
14076 * @protected
14077 * @param {jQuery.Event} e Key down event
14078 */
14079 OO.ui.MenuSelectWidget.prototype.onDocumentMouseDown = function ( e ) {
14080 if (
14081 !OO.ui.contains( this.$element[ 0 ], e.target, true ) &&
14082 ( !this.$widget || !OO.ui.contains( this.$widget[ 0 ], e.target, true ) )
14083 ) {
14084 this.toggle( false );
14085 }
14086 };
14087
14088 /**
14089 * @inheritdoc
14090 */
14091 OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) {
14092 var currentItem = this.getHighlightedItem() || this.getSelectedItem();
14093
14094 if ( !this.isDisabled() && this.isVisible() ) {
14095 switch ( e.keyCode ) {
14096 case OO.ui.Keys.LEFT:
14097 case OO.ui.Keys.RIGHT:
14098 // Do nothing if a text field is associated, arrow keys will be handled natively
14099 if ( !this.$input ) {
14100 OO.ui.MenuSelectWidget.super.prototype.onKeyDown.call( this, e );
14101 }
14102 break;
14103 case OO.ui.Keys.ESCAPE:
14104 case OO.ui.Keys.TAB:
14105 if ( currentItem ) {
14106 currentItem.setHighlighted( false );
14107 }
14108 this.toggle( false );
14109 // Don't prevent tabbing away, prevent defocusing
14110 if ( e.keyCode === OO.ui.Keys.ESCAPE ) {
14111 e.preventDefault();
14112 e.stopPropagation();
14113 }
14114 break;
14115 default:
14116 OO.ui.MenuSelectWidget.super.prototype.onKeyDown.call( this, e );
14117 return;
14118 }
14119 }
14120 };
14121
14122 /**
14123 * @inheritdoc
14124 */
14125 OO.ui.MenuSelectWidget.prototype.bindKeyDownListener = function () {
14126 if ( this.$input ) {
14127 this.$input.on( 'keydown', this.onKeyDownHandler );
14128 } else {
14129 OO.ui.MenuSelectWidget.super.prototype.bindKeyDownListener.call( this );
14130 }
14131 };
14132
14133 /**
14134 * @inheritdoc
14135 */
14136 OO.ui.MenuSelectWidget.prototype.unbindKeyDownListener = function () {
14137 if ( this.$input ) {
14138 this.$input.off( 'keydown', this.onKeyDownHandler );
14139 } else {
14140 OO.ui.MenuSelectWidget.super.prototype.unbindKeyDownListener.call( this );
14141 }
14142 };
14143
14144 /**
14145 * Choose an item.
14146 *
14147 * This will close the menu, unlike #selectItem which only changes selection.
14148 *
14149 * @param {OO.ui.OptionWidget} item Item to choose
14150 * @chainable
14151 */
14152 OO.ui.MenuSelectWidget.prototype.chooseItem = function ( item ) {
14153 OO.ui.MenuSelectWidget.super.prototype.chooseItem.call( this, item );
14154 this.toggle( false );
14155 return this;
14156 };
14157
14158 /**
14159 * @inheritdoc
14160 */
14161 OO.ui.MenuSelectWidget.prototype.addItems = function ( items, index ) {
14162 var i, len, item;
14163
14164 // Parent method
14165 OO.ui.MenuSelectWidget.super.prototype.addItems.call( this, items, index );
14166
14167 // Auto-initialize
14168 if ( !this.newItems ) {
14169 this.newItems = [];
14170 }
14171
14172 for ( i = 0, len = items.length; i < len; i++ ) {
14173 item = items[ i ];
14174 if ( this.isVisible() ) {
14175 // Defer fitting label until item has been attached
14176 item.fitLabel();
14177 } else {
14178 this.newItems.push( item );
14179 }
14180 }
14181
14182 // Reevaluate clipping
14183 this.clip();
14184
14185 return this;
14186 };
14187
14188 /**
14189 * @inheritdoc
14190 */
14191 OO.ui.MenuSelectWidget.prototype.removeItems = function ( items ) {
14192 // Parent method
14193 OO.ui.MenuSelectWidget.super.prototype.removeItems.call( this, items );
14194
14195 // Reevaluate clipping
14196 this.clip();
14197
14198 return this;
14199 };
14200
14201 /**
14202 * @inheritdoc
14203 */
14204 OO.ui.MenuSelectWidget.prototype.clearItems = function () {
14205 // Parent method
14206 OO.ui.MenuSelectWidget.super.prototype.clearItems.call( this );
14207
14208 // Reevaluate clipping
14209 this.clip();
14210
14211 return this;
14212 };
14213
14214 /**
14215 * @inheritdoc
14216 */
14217 OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
14218 visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
14219
14220 var i, len,
14221 change = visible !== this.isVisible();
14222
14223 // Parent method
14224 OO.ui.MenuSelectWidget.super.prototype.toggle.call( this, visible );
14225
14226 if ( change ) {
14227 if ( visible ) {
14228 this.bindKeyDownListener();
14229
14230 if ( this.newItems && this.newItems.length ) {
14231 for ( i = 0, len = this.newItems.length; i < len; i++ ) {
14232 this.newItems[ i ].fitLabel();
14233 }
14234 this.newItems = null;
14235 }
14236 this.toggleClipping( true );
14237
14238 // Auto-hide
14239 if ( this.autoHide ) {
14240 this.getElementDocument().addEventListener(
14241 'mousedown', this.onDocumentMouseDownHandler, true
14242 );
14243 }
14244 } else {
14245 this.unbindKeyDownListener();
14246 this.getElementDocument().removeEventListener(
14247 'mousedown', this.onDocumentMouseDownHandler, true
14248 );
14249 this.toggleClipping( false );
14250 }
14251 }
14252
14253 return this;
14254 };
14255
14256 /**
14257 * TextInputMenuSelectWidget is a menu that is specially designed to be positioned beneath
14258 * a {@link OO.ui.TextInputWidget text input} field. The menu's position is automatically
14259 * calculated and maintained when the menu is toggled or the window is resized.
14260 * See OO.ui.ComboBoxWidget for an example of a widget that uses this class.
14261 *
14262 * @class
14263 * @extends OO.ui.MenuSelectWidget
14264 *
14265 * @constructor
14266 * @param {OO.ui.TextInputWidget} inputWidget Text input widget to provide menu for
14267 * @param {Object} [config] Configuration options
14268 * @cfg {jQuery} [$container=input.$element] Element to render menu under
14269 */
14270 OO.ui.TextInputMenuSelectWidget = function OoUiTextInputMenuSelectWidget( inputWidget, config ) {
14271 // Allow passing positional parameters inside the config object
14272 if ( OO.isPlainObject( inputWidget ) && config === undefined ) {
14273 config = inputWidget;
14274 inputWidget = config.inputWidget;
14275 }
14276
14277 // Configuration initialization
14278 config = config || {};
14279
14280 // Parent constructor
14281 OO.ui.TextInputMenuSelectWidget.super.call( this, config );
14282
14283 // Properties
14284 this.inputWidget = inputWidget;
14285 this.$container = config.$container || this.inputWidget.$element;
14286 this.onWindowResizeHandler = this.onWindowResize.bind( this );
14287
14288 // Initialization
14289 this.$element.addClass( 'oo-ui-textInputMenuSelectWidget' );
14290 };
14291
14292 /* Setup */
14293
14294 OO.inheritClass( OO.ui.TextInputMenuSelectWidget, OO.ui.MenuSelectWidget );
14295
14296 /* Methods */
14297
14298 /**
14299 * Handle window resize event.
14300 *
14301 * @private
14302 * @param {jQuery.Event} e Window resize event
14303 */
14304 OO.ui.TextInputMenuSelectWidget.prototype.onWindowResize = function () {
14305 this.position();
14306 };
14307
14308 /**
14309 * @inheritdoc
14310 */
14311 OO.ui.TextInputMenuSelectWidget.prototype.toggle = function ( visible ) {
14312 visible = visible === undefined ? !this.isVisible() : !!visible;
14313
14314 var change = visible !== this.isVisible();
14315
14316 if ( change && visible ) {
14317 // Make sure the width is set before the parent method runs.
14318 // After this we have to call this.position(); again to actually
14319 // position ourselves correctly.
14320 this.position();
14321 }
14322
14323 // Parent method
14324 OO.ui.TextInputMenuSelectWidget.super.prototype.toggle.call( this, visible );
14325
14326 if ( change ) {
14327 if ( this.isVisible() ) {
14328 this.position();
14329 $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
14330 } else {
14331 $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
14332 }
14333 }
14334
14335 return this;
14336 };
14337
14338 /**
14339 * Position the menu.
14340 *
14341 * @private
14342 * @chainable
14343 */
14344 OO.ui.TextInputMenuSelectWidget.prototype.position = function () {
14345 var $container = this.$container,
14346 pos = OO.ui.Element.static.getRelativePosition( $container, this.$element.offsetParent() );
14347
14348 // Position under input
14349 pos.top += $container.height();
14350 this.$element.css( pos );
14351
14352 // Set width
14353 this.setIdealSize( $container.width() );
14354 // We updated the position, so re-evaluate the clipping state
14355 this.clip();
14356
14357 return this;
14358 };
14359
14360 /**
14361 * OutlineSelectWidget is a structured list that contains {@link OO.ui.OutlineOptionWidget outline options}
14362 * A set of controls can be provided with an {@link OO.ui.OutlineControlsWidget outline controls} widget.
14363 *
14364 * ####Currently, this class is only used by {@link OO.ui.BookletLayout BookletLayouts}.####
14365 *
14366 * @class
14367 * @extends OO.ui.SelectWidget
14368 * @mixins OO.ui.TabIndexedElement
14369 *
14370 * @constructor
14371 * @param {Object} [config] Configuration options
14372 */
14373 OO.ui.OutlineSelectWidget = function OoUiOutlineSelectWidget( config ) {
14374 // Parent constructor
14375 OO.ui.OutlineSelectWidget.super.call( this, config );
14376
14377 // Mixin constructors
14378 OO.ui.TabIndexedElement.call( this, config );
14379
14380 // Events
14381 this.$element.on( {
14382 focus: this.bindKeyDownListener.bind( this ),
14383 blur: this.unbindKeyDownListener.bind( this )
14384 } );
14385
14386 // Initialization
14387 this.$element.addClass( 'oo-ui-outlineSelectWidget' );
14388 };
14389
14390 /* Setup */
14391
14392 OO.inheritClass( OO.ui.OutlineSelectWidget, OO.ui.SelectWidget );
14393 OO.mixinClass( OO.ui.OutlineSelectWidget, OO.ui.TabIndexedElement );
14394
14395 /**
14396 * ToggleSwitches are switches that slide on and off. Their state is represented by a Boolean
14397 * value (`true` for ‘on’, and `false` otherwise, the default). The ‘off’ state is represented
14398 * visually by a slider in the leftmost position.
14399 *
14400 * @example
14401 * // Toggle switches in the 'off' and 'on' position.
14402 * var toggleSwitch1 = new OO.ui.ToggleSwitchWidget({
14403 * value: false
14404 * } );
14405 * var toggleSwitch2 = new OO.ui.ToggleSwitchWidget({
14406 * value: true
14407 * } );
14408 *
14409 * // Create a FieldsetLayout to layout and label switches
14410 * var fieldset = new OO.ui.FieldsetLayout( {
14411 * label: 'Toggle switches'
14412 * } );
14413 * fieldset.addItems( [
14414 * new OO.ui.FieldLayout( toggleSwitch1, {label : 'Off', align : 'top'}),
14415 * new OO.ui.FieldLayout( toggleSwitch2, {label : 'On', align : 'top'})
14416 * ] );
14417 * $( 'body' ).append( fieldset.$element );
14418 *
14419 * @class
14420 * @extends OO.ui.Widget
14421 * @mixins OO.ui.ToggleWidget
14422 * @mixins OO.ui.TabIndexedElement
14423 *
14424 * @constructor
14425 * @param {Object} [config] Configuration options
14426 * @cfg {boolean} [value=false] The toggle switch’s initial on/off state.
14427 * By default, the toggle switch is in the 'off' position.
14428 */
14429 OO.ui.ToggleSwitchWidget = function OoUiToggleSwitchWidget( config ) {
14430 // Parent constructor
14431 OO.ui.ToggleSwitchWidget.super.call( this, config );
14432
14433 // Mixin constructors
14434 OO.ui.ToggleWidget.call( this, config );
14435 OO.ui.TabIndexedElement.call( this, config );
14436
14437 // Properties
14438 this.dragging = false;
14439 this.dragStart = null;
14440 this.sliding = false;
14441 this.$glow = $( '<span>' );
14442 this.$grip = $( '<span>' );
14443
14444 // Events
14445 this.$element.on( {
14446 click: this.onClick.bind( this ),
14447 keypress: this.onKeyPress.bind( this )
14448 } );
14449
14450 // Initialization
14451 this.$glow.addClass( 'oo-ui-toggleSwitchWidget-glow' );
14452 this.$grip.addClass( 'oo-ui-toggleSwitchWidget-grip' );
14453 this.$element
14454 .addClass( 'oo-ui-toggleSwitchWidget' )
14455 .attr( 'role', 'checkbox' )
14456 .append( this.$glow, this.$grip );
14457 };
14458
14459 /* Setup */
14460
14461 OO.inheritClass( OO.ui.ToggleSwitchWidget, OO.ui.Widget );
14462 OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.ToggleWidget );
14463 OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.TabIndexedElement );
14464
14465 /* Methods */
14466
14467 /**
14468 * Handle mouse click events.
14469 *
14470 * @private
14471 * @param {jQuery.Event} e Mouse click event
14472 */
14473 OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
14474 if ( !this.isDisabled() && e.which === 1 ) {
14475 this.setValue( !this.value );
14476 }
14477 return false;
14478 };
14479
14480 /**
14481 * Handle key press events.
14482 *
14483 * @private
14484 * @param {jQuery.Event} e Key press event
14485 */
14486 OO.ui.ToggleSwitchWidget.prototype.onKeyPress = function ( e ) {
14487 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
14488 this.setValue( !this.value );
14489 return false;
14490 }
14491 };
14492
14493 }( OO ) );