Merge "Update the Chinese conversion table for Chinese WikiProjects"
[lhc/web/wiklou.git] / resources / lib / oojs-ui / oojs-ui.js
1 /*!
2 * OOjs UI v0.9.0
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-04T23:55:34Z
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). Please see the [OOjs UI documentation on MediaWiki][1] for more information.
326 *
327 * @example
328 * // Example: An action set used in a process dialog
329 * function ProcessDialog( config ) {
330 * ProcessDialog.super.call( this, config );
331 * }
332 * OO.inheritClass( ProcessDialog, OO.ui.ProcessDialog );
333 * ProcessDialog.static.title = 'An action set in a process dialog';
334 * // An action set that uses modes ('edit' and 'help' mode, in this example).
335 * ProcessDialog.static.actions = [
336 * { action: 'continue', modes: 'edit', label: 'Continue', flags: [ 'primary', 'constructive' ] },
337 * { action: 'help', modes: 'edit', label: 'Help' },
338 * { modes: 'edit', label: 'Cancel', flags: 'safe' },
339 * { action: 'back', modes: 'help', label: 'Back', flags: 'safe' }
340 * ];
341 *
342 * ProcessDialog.prototype.initialize = function () {
343 * ProcessDialog.super.prototype.initialize.apply( this, arguments );
344 * this.panel1 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
345 * 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>' );
346 * this.panel2 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
347 * 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>' );
348 * this.stackLayout= new OO.ui.StackLayout( {
349 * items: [ this.panel1, this.panel2 ]
350 * });
351 * this.$body.append( this.stackLayout.$element );
352 * };
353 * ProcessDialog.prototype.getSetupProcess = function ( data ) {
354 * return ProcessDialog.super.prototype.getSetupProcess.call( this, data )
355 * .next( function () {
356 * this.actions.setMode('edit');
357 * }, this );
358 * };
359 * ProcessDialog.prototype.getActionProcess = function ( action ) {
360 * if ( action === 'help' ) {
361 * this.actions.setMode( 'help' );
362 * this.stackLayout.setItem( this.panel2 );
363 * } else if ( action === 'back' ) {
364 * this.actions.setMode( 'edit' );
365 * this.stackLayout.setItem( this.panel1 );
366 * } else if ( action === 'continue' ) {
367 * var dialog = this;
368 * return new OO.ui.Process( function () {
369 * dialog.close();
370 * } );
371 * }
372 * return ProcessDialog.super.prototype.getActionProcess.call( this, action );
373 * };
374 * ProcessDialog.prototype.getBodyHeight = function () {
375 * return this.panel1.$element.outerHeight( true );
376 * };
377 * var windowManager = new OO.ui.WindowManager();
378 * $( 'body' ).append( windowManager.$element );
379 * var processDialog = new ProcessDialog({
380 * size: 'medium'});
381 * windowManager.addWindows( [ processDialog ] );
382 * windowManager.openWindow( processDialog );
383 *
384 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
385 *
386 * @abstract
387 * @class
388 * @mixins OO.EventEmitter
389 *
390 * @constructor
391 * @param {Object} [config] Configuration options
392 */
393 OO.ui.ActionSet = function OoUiActionSet( config ) {
394 // Configuration initialization
395 config = config || {};
396
397 // Mixin constructors
398 OO.EventEmitter.call( this );
399
400 // Properties
401 this.list = [];
402 this.categories = {
403 actions: 'getAction',
404 flags: 'getFlags',
405 modes: 'getModes'
406 };
407 this.categorized = {};
408 this.special = {};
409 this.others = [];
410 this.organized = false;
411 this.changing = false;
412 this.changed = false;
413 };
414
415 /* Setup */
416
417 OO.mixinClass( OO.ui.ActionSet, OO.EventEmitter );
418
419 /* Static Properties */
420
421 /**
422 * Symbolic name of the flags used to identify special actions. Special actions are displayed in the
423 * header of a {@link OO.ui.ProcessDialog process dialog}.
424 * See the [OOjs UI documentation on MediaWiki][2] for more information and examples.
425 *
426 * [2]:https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
427 *
428 * @abstract
429 * @static
430 * @inheritable
431 * @property {string}
432 */
433 OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ];
434
435 /* Events */
436
437 /**
438 * @event click
439 * @param {OO.ui.ActionWidget} action Action that was clicked
440 */
441
442 /**
443 * @event resize
444 * @param {OO.ui.ActionWidget} action Action that was resized
445 */
446
447 /**
448 * @event add
449 * @param {OO.ui.ActionWidget[]} added Actions added
450 */
451
452 /**
453 * @event remove
454 * @param {OO.ui.ActionWidget[]} added Actions removed
455 */
456
457 /**
458 * @event change
459 */
460
461 /* Methods */
462
463 /**
464 * Handle action change events.
465 *
466 * @private
467 * @fires change
468 */
469 OO.ui.ActionSet.prototype.onActionChange = function () {
470 this.organized = false;
471 if ( this.changing ) {
472 this.changed = true;
473 } else {
474 this.emit( 'change' );
475 }
476 };
477
478 /**
479 * Check if a action is one of the special actions.
480 *
481 * @param {OO.ui.ActionWidget} action Action to check
482 * @return {boolean} Action is special
483 */
484 OO.ui.ActionSet.prototype.isSpecial = function ( action ) {
485 var flag;
486
487 for ( flag in this.special ) {
488 if ( action === this.special[ flag ] ) {
489 return true;
490 }
491 }
492
493 return false;
494 };
495
496 /**
497 * Get actions.
498 *
499 * @param {Object} [filters] Filters to use, omit to get all actions
500 * @param {string|string[]} [filters.actions] Actions that actions must have
501 * @param {string|string[]} [filters.flags] Flags that actions must have
502 * @param {string|string[]} [filters.modes] Modes that actions must have
503 * @param {boolean} [filters.visible] Actions must be visible
504 * @param {boolean} [filters.disabled] Actions must be disabled
505 * @return {OO.ui.ActionWidget[]} Actions matching all criteria
506 */
507 OO.ui.ActionSet.prototype.get = function ( filters ) {
508 var i, len, list, category, actions, index, match, matches;
509
510 if ( filters ) {
511 this.organize();
512
513 // Collect category candidates
514 matches = [];
515 for ( category in this.categorized ) {
516 list = filters[ category ];
517 if ( list ) {
518 if ( !Array.isArray( list ) ) {
519 list = [ list ];
520 }
521 for ( i = 0, len = list.length; i < len; i++ ) {
522 actions = this.categorized[ category ][ list[ i ] ];
523 if ( Array.isArray( actions ) ) {
524 matches.push.apply( matches, actions );
525 }
526 }
527 }
528 }
529 // Remove by boolean filters
530 for ( i = 0, len = matches.length; i < len; i++ ) {
531 match = matches[ i ];
532 if (
533 ( filters.visible !== undefined && match.isVisible() !== filters.visible ) ||
534 ( filters.disabled !== undefined && match.isDisabled() !== filters.disabled )
535 ) {
536 matches.splice( i, 1 );
537 len--;
538 i--;
539 }
540 }
541 // Remove duplicates
542 for ( i = 0, len = matches.length; i < len; i++ ) {
543 match = matches[ i ];
544 index = matches.lastIndexOf( match );
545 while ( index !== i ) {
546 matches.splice( index, 1 );
547 len--;
548 index = matches.lastIndexOf( match );
549 }
550 }
551 return matches;
552 }
553 return this.list.slice();
554 };
555
556 /**
557 * Get special actions.
558 *
559 * Special actions are the first visible actions with special flags, such as 'safe' and 'primary'.
560 * Special flags can be configured by changing #static-specialFlags in a subclass.
561 *
562 * @return {OO.ui.ActionWidget|null} Safe action
563 */
564 OO.ui.ActionSet.prototype.getSpecial = function () {
565 this.organize();
566 return $.extend( {}, this.special );
567 };
568
569 /**
570 * Get other actions.
571 *
572 * Other actions include all non-special visible actions.
573 *
574 * @return {OO.ui.ActionWidget[]} Other actions
575 */
576 OO.ui.ActionSet.prototype.getOthers = function () {
577 this.organize();
578 return this.others.slice();
579 };
580
581 /**
582 * Toggle actions based on their modes.
583 *
584 * Unlike calling toggle on actions with matching flags, this will enforce mutually exclusive
585 * visibility; matching actions will be shown, non-matching actions will be hidden.
586 *
587 * @param {string} mode Mode actions must have
588 * @chainable
589 * @fires toggle
590 * @fires change
591 */
592 OO.ui.ActionSet.prototype.setMode = function ( mode ) {
593 var i, len, action;
594
595 this.changing = true;
596 for ( i = 0, len = this.list.length; i < len; i++ ) {
597 action = this.list[ i ];
598 action.toggle( action.hasMode( mode ) );
599 }
600
601 this.organized = false;
602 this.changing = false;
603 this.emit( 'change' );
604
605 return this;
606 };
607
608 /**
609 * Change which actions are able to be performed.
610 *
611 * Actions with matching actions will be disabled/enabled. Other actions will not be changed.
612 *
613 * @param {Object.<string,boolean>} actions List of abilities, keyed by action name, values
614 * indicate actions are able to be performed
615 * @chainable
616 */
617 OO.ui.ActionSet.prototype.setAbilities = function ( actions ) {
618 var i, len, action, item;
619
620 for ( i = 0, len = this.list.length; i < len; i++ ) {
621 item = this.list[ i ];
622 action = item.getAction();
623 if ( actions[ action ] !== undefined ) {
624 item.setDisabled( !actions[ action ] );
625 }
626 }
627
628 return this;
629 };
630
631 /**
632 * Executes a function once per action.
633 *
634 * When making changes to multiple actions, use this method instead of iterating over the actions
635 * manually to defer emitting a change event until after all actions have been changed.
636 *
637 * @param {Object|null} actions Filters to use for which actions to iterate over; see #get
638 * @param {Function} callback Callback to run for each action; callback is invoked with three
639 * arguments: the action, the action's index, the list of actions being iterated over
640 * @chainable
641 */
642 OO.ui.ActionSet.prototype.forEach = function ( filter, callback ) {
643 this.changed = false;
644 this.changing = true;
645 this.get( filter ).forEach( callback );
646 this.changing = false;
647 if ( this.changed ) {
648 this.emit( 'change' );
649 }
650
651 return this;
652 };
653
654 /**
655 * Add actions.
656 *
657 * @param {OO.ui.ActionWidget[]} actions Actions to add
658 * @chainable
659 * @fires add
660 * @fires change
661 */
662 OO.ui.ActionSet.prototype.add = function ( actions ) {
663 var i, len, action;
664
665 this.changing = true;
666 for ( i = 0, len = actions.length; i < len; i++ ) {
667 action = actions[ i ];
668 action.connect( this, {
669 click: [ 'emit', 'click', action ],
670 resize: [ 'emit', 'resize', action ],
671 toggle: [ 'onActionChange' ]
672 } );
673 this.list.push( action );
674 }
675 this.organized = false;
676 this.emit( 'add', actions );
677 this.changing = false;
678 this.emit( 'change' );
679
680 return this;
681 };
682
683 /**
684 * Remove actions.
685 *
686 * @param {OO.ui.ActionWidget[]} actions Actions to remove
687 * @chainable
688 * @fires remove
689 * @fires change
690 */
691 OO.ui.ActionSet.prototype.remove = function ( actions ) {
692 var i, len, index, action;
693
694 this.changing = true;
695 for ( i = 0, len = actions.length; i < len; i++ ) {
696 action = actions[ i ];
697 index = this.list.indexOf( action );
698 if ( index !== -1 ) {
699 action.disconnect( this );
700 this.list.splice( index, 1 );
701 }
702 }
703 this.organized = false;
704 this.emit( 'remove', actions );
705 this.changing = false;
706 this.emit( 'change' );
707
708 return this;
709 };
710
711 /**
712 * Remove all actions.
713 *
714 * @chainable
715 * @fires remove
716 * @fires change
717 */
718 OO.ui.ActionSet.prototype.clear = function () {
719 var i, len, action,
720 removed = this.list.slice();
721
722 this.changing = true;
723 for ( i = 0, len = this.list.length; i < len; i++ ) {
724 action = this.list[ i ];
725 action.disconnect( this );
726 }
727
728 this.list = [];
729
730 this.organized = false;
731 this.emit( 'remove', removed );
732 this.changing = false;
733 this.emit( 'change' );
734
735 return this;
736 };
737
738 /**
739 * Organize actions.
740 *
741 * This is called whenever organized information is requested. It will only reorganize the actions
742 * if something has changed since the last time it ran.
743 *
744 * @private
745 * @chainable
746 */
747 OO.ui.ActionSet.prototype.organize = function () {
748 var i, iLen, j, jLen, flag, action, category, list, item, special,
749 specialFlags = this.constructor.static.specialFlags;
750
751 if ( !this.organized ) {
752 this.categorized = {};
753 this.special = {};
754 this.others = [];
755 for ( i = 0, iLen = this.list.length; i < iLen; i++ ) {
756 action = this.list[ i ];
757 if ( action.isVisible() ) {
758 // Populate categories
759 for ( category in this.categories ) {
760 if ( !this.categorized[ category ] ) {
761 this.categorized[ category ] = {};
762 }
763 list = action[ this.categories[ category ] ]();
764 if ( !Array.isArray( list ) ) {
765 list = [ list ];
766 }
767 for ( j = 0, jLen = list.length; j < jLen; j++ ) {
768 item = list[ j ];
769 if ( !this.categorized[ category ][ item ] ) {
770 this.categorized[ category ][ item ] = [];
771 }
772 this.categorized[ category ][ item ].push( action );
773 }
774 }
775 // Populate special/others
776 special = false;
777 for ( j = 0, jLen = specialFlags.length; j < jLen; j++ ) {
778 flag = specialFlags[ j ];
779 if ( !this.special[ flag ] && action.hasFlag( flag ) ) {
780 this.special[ flag ] = action;
781 special = true;
782 break;
783 }
784 }
785 if ( !special ) {
786 this.others.push( action );
787 }
788 }
789 }
790 this.organized = true;
791 }
792
793 return this;
794 };
795
796 /**
797 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
798 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
799 * connected to them and can't be interacted with.
800 *
801 * @abstract
802 * @class
803 *
804 * @constructor
805 * @param {Object} [config] Configuration options
806 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
807 * to the top level (e.g., the outermost div) of the element. See the [OOjs UI documentation on MediaWiki][2]
808 * for an example.
809 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#cssExample
810 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
811 * @cfg {string} [text] Text to insert
812 * @cfg {Array} [content] An array of content elements to append (after #text).
813 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
814 * Instances of OO.ui.Element will have their $element appended.
815 * @cfg {jQuery} [$content] Content elements to append (after #text)
816 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
817 * Data can also be specified with the #setData method.
818 */
819 OO.ui.Element = function OoUiElement( config ) {
820 // Configuration initialization
821 config = config || {};
822
823 // Properties
824 this.$ = $;
825 this.visible = true;
826 this.data = config.data;
827 this.$element = config.$element ||
828 $( document.createElement( this.getTagName() ) );
829 this.elementGroup = null;
830 this.debouncedUpdateThemeClassesHandler = this.debouncedUpdateThemeClasses.bind( this );
831 this.updateThemeClassesPending = false;
832
833 // Initialization
834 if ( Array.isArray( config.classes ) ) {
835 this.$element.addClass( config.classes.join( ' ' ) );
836 }
837 if ( config.id ) {
838 this.$element.attr( 'id', config.id );
839 }
840 if ( config.text ) {
841 this.$element.text( config.text );
842 }
843 if ( config.content ) {
844 // The `content` property treats plain strings as text; use an
845 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
846 // appropriate $element appended.
847 this.$element.append( config.content.map( function ( v ) {
848 if ( typeof v === 'string' ) {
849 // Escape string so it is properly represented in HTML.
850 return document.createTextNode( v );
851 } else if ( v instanceof OO.ui.HtmlSnippet ) {
852 // Bypass escaping.
853 return v.toString();
854 } else if ( v instanceof OO.ui.Element ) {
855 return v.$element;
856 }
857 return v;
858 } ) );
859 }
860 if ( config.$content ) {
861 // The `$content` property treats plain strings as HTML.
862 this.$element.append( config.$content );
863 }
864 };
865
866 /* Setup */
867
868 OO.initClass( OO.ui.Element );
869
870 /* Static Properties */
871
872 /**
873 * The name of the HTML tag used by the element.
874 *
875 * The static value may be ignored if the #getTagName method is overridden.
876 *
877 * @static
878 * @inheritable
879 * @property {string}
880 */
881 OO.ui.Element.static.tagName = 'div';
882
883 /* Static Methods */
884
885 /**
886 * Reconstitute a JavaScript object corresponding to a widget created
887 * by the PHP implementation.
888 *
889 * @param {string|HTMLElement|jQuery} idOrNode
890 * A DOM id (if a string) or node for the widget to infuse.
891 * @return {OO.ui.Element}
892 * The `OO.ui.Element` corresponding to this (infusable) document node.
893 * For `Tag` objects emitted on the HTML side (used occasionally for content)
894 * the value returned is a newly-created Element wrapping around the existing
895 * DOM node.
896 */
897 OO.ui.Element.static.infuse = function ( idOrNode ) {
898 var obj = OO.ui.Element.static.unsafeInfuse( idOrNode, true );
899 // Verify that the type matches up.
900 // FIXME: uncomment after T89721 is fixed (see T90929)
901 /*
902 if ( !( obj instanceof this['class'] ) ) {
903 throw new Error( 'Infusion type mismatch!' );
904 }
905 */
906 return obj;
907 };
908
909 /**
910 * Implementation helper for `infuse`; skips the type check and has an
911 * extra property so that only the top-level invocation touches the DOM.
912 * @private
913 * @param {string|HTMLElement|jQuery} idOrNode
914 * @param {boolean} top True only for top-level invocation.
915 * @return {OO.ui.Element}
916 */
917 OO.ui.Element.static.unsafeInfuse = function ( idOrNode, top ) {
918 // look for a cached result of a previous infusion.
919 var id, $elem, data, cls, obj;
920 if ( typeof idOrNode === 'string' ) {
921 id = idOrNode;
922 $elem = $( document.getElementById( id ) );
923 } else {
924 $elem = $( idOrNode );
925 id = $elem.attr( 'id' );
926 }
927 data = $elem.data( 'ooui-infused' );
928 if ( data ) {
929 // cached!
930 if ( data === true ) {
931 throw new Error( 'Circular dependency! ' + id );
932 }
933 return data;
934 }
935 if ( !$elem.length ) {
936 throw new Error( 'Widget not found: ' + id );
937 }
938 data = $elem.attr( 'data-ooui' );
939 if ( !data ) {
940 throw new Error( 'No infusion data found: ' + id );
941 }
942 try {
943 data = $.parseJSON( data );
944 } catch ( _ ) {
945 data = null;
946 }
947 if ( !( data && data._ ) ) {
948 throw new Error( 'No valid infusion data found: ' + id );
949 }
950 if ( data._ === 'Tag' ) {
951 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
952 return new OO.ui.Element( { $element: $elem } );
953 }
954 cls = OO.ui[data._];
955 if ( !cls ) {
956 throw new Error( 'Unknown widget type: ' + id );
957 }
958 $elem.data( 'ooui-infused', true ); // prevent loops
959 data.id = id; // implicit
960 data = OO.copy( data, null, function deserialize( value ) {
961 if ( OO.isPlainObject( value ) ) {
962 if ( value.tag ) {
963 return OO.ui.Element.static.unsafeInfuse( value.tag, false );
964 }
965 if ( value.html ) {
966 return new OO.ui.HtmlSnippet( value.html );
967 }
968 }
969 } );
970 // jscs:disable requireCapitalizedConstructors
971 obj = new cls( data ); // rebuild widget
972 // now replace old DOM with this new DOM.
973 if ( top ) {
974 $elem.replaceWith( obj.$element );
975 }
976 obj.$element.data( 'ooui-infused', obj );
977 // set the 'data-ooui' attribute so we can identify infused widgets
978 obj.$element.attr( 'data-ooui', '' );
979 return obj;
980 };
981
982 /**
983 * Get a jQuery function within a specific document.
984 *
985 * @static
986 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
987 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
988 * not in an iframe
989 * @return {Function} Bound jQuery function
990 */
991 OO.ui.Element.static.getJQuery = function ( context, $iframe ) {
992 function wrapper( selector ) {
993 return $( selector, wrapper.context );
994 }
995
996 wrapper.context = this.getDocument( context );
997
998 if ( $iframe ) {
999 wrapper.$iframe = $iframe;
1000 }
1001
1002 return wrapper;
1003 };
1004
1005 /**
1006 * Get the document of an element.
1007 *
1008 * @static
1009 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
1010 * @return {HTMLDocument|null} Document object
1011 */
1012 OO.ui.Element.static.getDocument = function ( obj ) {
1013 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
1014 return ( obj[ 0 ] && obj[ 0 ].ownerDocument ) ||
1015 // Empty jQuery selections might have a context
1016 obj.context ||
1017 // HTMLElement
1018 obj.ownerDocument ||
1019 // Window
1020 obj.document ||
1021 // HTMLDocument
1022 ( obj.nodeType === 9 && obj ) ||
1023 null;
1024 };
1025
1026 /**
1027 * Get the window of an element or document.
1028 *
1029 * @static
1030 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
1031 * @return {Window} Window object
1032 */
1033 OO.ui.Element.static.getWindow = function ( obj ) {
1034 var doc = this.getDocument( obj );
1035 return doc.parentWindow || doc.defaultView;
1036 };
1037
1038 /**
1039 * Get the direction of an element or document.
1040 *
1041 * @static
1042 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
1043 * @return {string} Text direction, either 'ltr' or 'rtl'
1044 */
1045 OO.ui.Element.static.getDir = function ( obj ) {
1046 var isDoc, isWin;
1047
1048 if ( obj instanceof jQuery ) {
1049 obj = obj[ 0 ];
1050 }
1051 isDoc = obj.nodeType === 9;
1052 isWin = obj.document !== undefined;
1053 if ( isDoc || isWin ) {
1054 if ( isWin ) {
1055 obj = obj.document;
1056 }
1057 obj = obj.body;
1058 }
1059 return $( obj ).css( 'direction' );
1060 };
1061
1062 /**
1063 * Get the offset between two frames.
1064 *
1065 * TODO: Make this function not use recursion.
1066 *
1067 * @static
1068 * @param {Window} from Window of the child frame
1069 * @param {Window} [to=window] Window of the parent frame
1070 * @param {Object} [offset] Offset to start with, used internally
1071 * @return {Object} Offset object, containing left and top properties
1072 */
1073 OO.ui.Element.static.getFrameOffset = function ( from, to, offset ) {
1074 var i, len, frames, frame, rect;
1075
1076 if ( !to ) {
1077 to = window;
1078 }
1079 if ( !offset ) {
1080 offset = { top: 0, left: 0 };
1081 }
1082 if ( from.parent === from ) {
1083 return offset;
1084 }
1085
1086 // Get iframe element
1087 frames = from.parent.document.getElementsByTagName( 'iframe' );
1088 for ( i = 0, len = frames.length; i < len; i++ ) {
1089 if ( frames[ i ].contentWindow === from ) {
1090 frame = frames[ i ];
1091 break;
1092 }
1093 }
1094
1095 // Recursively accumulate offset values
1096 if ( frame ) {
1097 rect = frame.getBoundingClientRect();
1098 offset.left += rect.left;
1099 offset.top += rect.top;
1100 if ( from !== to ) {
1101 this.getFrameOffset( from.parent, offset );
1102 }
1103 }
1104 return offset;
1105 };
1106
1107 /**
1108 * Get the offset between two elements.
1109 *
1110 * The two elements may be in a different frame, but in that case the frame $element is in must
1111 * be contained in the frame $anchor is in.
1112 *
1113 * @static
1114 * @param {jQuery} $element Element whose position to get
1115 * @param {jQuery} $anchor Element to get $element's position relative to
1116 * @return {Object} Translated position coordinates, containing top and left properties
1117 */
1118 OO.ui.Element.static.getRelativePosition = function ( $element, $anchor ) {
1119 var iframe, iframePos,
1120 pos = $element.offset(),
1121 anchorPos = $anchor.offset(),
1122 elementDocument = this.getDocument( $element ),
1123 anchorDocument = this.getDocument( $anchor );
1124
1125 // If $element isn't in the same document as $anchor, traverse up
1126 while ( elementDocument !== anchorDocument ) {
1127 iframe = elementDocument.defaultView.frameElement;
1128 if ( !iframe ) {
1129 throw new Error( '$element frame is not contained in $anchor frame' );
1130 }
1131 iframePos = $( iframe ).offset();
1132 pos.left += iframePos.left;
1133 pos.top += iframePos.top;
1134 elementDocument = iframe.ownerDocument;
1135 }
1136 pos.left -= anchorPos.left;
1137 pos.top -= anchorPos.top;
1138 return pos;
1139 };
1140
1141 /**
1142 * Get element border sizes.
1143 *
1144 * @static
1145 * @param {HTMLElement} el Element to measure
1146 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1147 */
1148 OO.ui.Element.static.getBorders = function ( el ) {
1149 var doc = el.ownerDocument,
1150 win = doc.parentWindow || doc.defaultView,
1151 style = win && win.getComputedStyle ?
1152 win.getComputedStyle( el, null ) :
1153 el.currentStyle,
1154 $el = $( el ),
1155 top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0,
1156 left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0,
1157 bottom = parseFloat( style ? style.borderBottomWidth : $el.css( 'borderBottomWidth' ) ) || 0,
1158 right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0;
1159
1160 return {
1161 top: top,
1162 left: left,
1163 bottom: bottom,
1164 right: right
1165 };
1166 };
1167
1168 /**
1169 * Get dimensions of an element or window.
1170 *
1171 * @static
1172 * @param {HTMLElement|Window} el Element to measure
1173 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1174 */
1175 OO.ui.Element.static.getDimensions = function ( el ) {
1176 var $el, $win,
1177 doc = el.ownerDocument || el.document,
1178 win = doc.parentWindow || doc.defaultView;
1179
1180 if ( win === el || el === doc.documentElement ) {
1181 $win = $( win );
1182 return {
1183 borders: { top: 0, left: 0, bottom: 0, right: 0 },
1184 scroll: {
1185 top: $win.scrollTop(),
1186 left: $win.scrollLeft()
1187 },
1188 scrollbar: { right: 0, bottom: 0 },
1189 rect: {
1190 top: 0,
1191 left: 0,
1192 bottom: $win.innerHeight(),
1193 right: $win.innerWidth()
1194 }
1195 };
1196 } else {
1197 $el = $( el );
1198 return {
1199 borders: this.getBorders( el ),
1200 scroll: {
1201 top: $el.scrollTop(),
1202 left: $el.scrollLeft()
1203 },
1204 scrollbar: {
1205 right: $el.innerWidth() - el.clientWidth,
1206 bottom: $el.innerHeight() - el.clientHeight
1207 },
1208 rect: el.getBoundingClientRect()
1209 };
1210 }
1211 };
1212
1213 /**
1214 * Get scrollable object parent
1215 *
1216 * documentElement can't be used to get or set the scrollTop
1217 * property on Blink. Changing and testing its value lets us
1218 * use 'body' or 'documentElement' based on what is working.
1219 *
1220 * https://code.google.com/p/chromium/issues/detail?id=303131
1221 *
1222 * @static
1223 * @param {HTMLElement} el Element to find scrollable parent for
1224 * @return {HTMLElement} Scrollable parent
1225 */
1226 OO.ui.Element.static.getRootScrollableElement = function ( el ) {
1227 var scrollTop, body;
1228
1229 if ( OO.ui.scrollableElement === undefined ) {
1230 body = el.ownerDocument.body;
1231 scrollTop = body.scrollTop;
1232 body.scrollTop = 1;
1233
1234 if ( body.scrollTop === 1 ) {
1235 body.scrollTop = scrollTop;
1236 OO.ui.scrollableElement = 'body';
1237 } else {
1238 OO.ui.scrollableElement = 'documentElement';
1239 }
1240 }
1241
1242 return el.ownerDocument[ OO.ui.scrollableElement ];
1243 };
1244
1245 /**
1246 * Get closest scrollable container.
1247 *
1248 * Traverses up until either a scrollable element or the root is reached, in which case the window
1249 * will be returned.
1250 *
1251 * @static
1252 * @param {HTMLElement} el Element to find scrollable container for
1253 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1254 * @return {HTMLElement} Closest scrollable container
1255 */
1256 OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) {
1257 var i, val,
1258 props = [ 'overflow' ],
1259 $parent = $( el ).parent();
1260
1261 if ( dimension === 'x' || dimension === 'y' ) {
1262 props.push( 'overflow-' + dimension );
1263 }
1264
1265 while ( $parent.length ) {
1266 if ( $parent[ 0 ] === this.getRootScrollableElement( el ) ) {
1267 return $parent[ 0 ];
1268 }
1269 i = props.length;
1270 while ( i-- ) {
1271 val = $parent.css( props[ i ] );
1272 if ( val === 'auto' || val === 'scroll' ) {
1273 return $parent[ 0 ];
1274 }
1275 }
1276 $parent = $parent.parent();
1277 }
1278 return this.getDocument( el ).body;
1279 };
1280
1281 /**
1282 * Scroll element into view.
1283 *
1284 * @static
1285 * @param {HTMLElement} el Element to scroll into view
1286 * @param {Object} [config] Configuration options
1287 * @param {string} [config.duration] jQuery animation duration value
1288 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1289 * to scroll in both directions
1290 * @param {Function} [config.complete] Function to call when scrolling completes
1291 */
1292 OO.ui.Element.static.scrollIntoView = function ( el, config ) {
1293 // Configuration initialization
1294 config = config || {};
1295
1296 var rel, anim = {},
1297 callback = typeof config.complete === 'function' && config.complete,
1298 sc = this.getClosestScrollableContainer( el, config.direction ),
1299 $sc = $( sc ),
1300 eld = this.getDimensions( el ),
1301 scd = this.getDimensions( sc ),
1302 $win = $( this.getWindow( el ) );
1303
1304 // Compute the distances between the edges of el and the edges of the scroll viewport
1305 if ( $sc.is( 'html, body' ) ) {
1306 // If the scrollable container is the root, this is easy
1307 rel = {
1308 top: eld.rect.top,
1309 bottom: $win.innerHeight() - eld.rect.bottom,
1310 left: eld.rect.left,
1311 right: $win.innerWidth() - eld.rect.right
1312 };
1313 } else {
1314 // Otherwise, we have to subtract el's coordinates from sc's coordinates
1315 rel = {
1316 top: eld.rect.top - ( scd.rect.top + scd.borders.top ),
1317 bottom: scd.rect.bottom - scd.borders.bottom - scd.scrollbar.bottom - eld.rect.bottom,
1318 left: eld.rect.left - ( scd.rect.left + scd.borders.left ),
1319 right: scd.rect.right - scd.borders.right - scd.scrollbar.right - eld.rect.right
1320 };
1321 }
1322
1323 if ( !config.direction || config.direction === 'y' ) {
1324 if ( rel.top < 0 ) {
1325 anim.scrollTop = scd.scroll.top + rel.top;
1326 } else if ( rel.top > 0 && rel.bottom < 0 ) {
1327 anim.scrollTop = scd.scroll.top + Math.min( rel.top, -rel.bottom );
1328 }
1329 }
1330 if ( !config.direction || config.direction === 'x' ) {
1331 if ( rel.left < 0 ) {
1332 anim.scrollLeft = scd.scroll.left + rel.left;
1333 } else if ( rel.left > 0 && rel.right < 0 ) {
1334 anim.scrollLeft = scd.scroll.left + Math.min( rel.left, -rel.right );
1335 }
1336 }
1337 if ( !$.isEmptyObject( anim ) ) {
1338 $sc.stop( true ).animate( anim, config.duration || 'fast' );
1339 if ( callback ) {
1340 $sc.queue( function ( next ) {
1341 callback();
1342 next();
1343 } );
1344 }
1345 } else {
1346 if ( callback ) {
1347 callback();
1348 }
1349 }
1350 };
1351
1352 /**
1353 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1354 * and reserve space for them, because it probably doesn't.
1355 *
1356 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1357 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1358 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1359 * and then reattach (or show) them back.
1360 *
1361 * @static
1362 * @param {HTMLElement} el Element to reconsider the scrollbars on
1363 */
1364 OO.ui.Element.static.reconsiderScrollbars = function ( el ) {
1365 var i, len, nodes = [];
1366 // Detach all children
1367 while ( el.firstChild ) {
1368 nodes.push( el.firstChild );
1369 el.removeChild( el.firstChild );
1370 }
1371 // Force reflow
1372 void el.offsetHeight;
1373 // Reattach all children
1374 for ( i = 0, len = nodes.length; i < len; i++ ) {
1375 el.appendChild( nodes[ i ] );
1376 }
1377 };
1378
1379 /* Methods */
1380
1381 /**
1382 * Toggle visibility of an element.
1383 *
1384 * @param {boolean} [show] Make element visible, omit to toggle visibility
1385 * @fires visible
1386 * @chainable
1387 */
1388 OO.ui.Element.prototype.toggle = function ( show ) {
1389 show = show === undefined ? !this.visible : !!show;
1390
1391 if ( show !== this.isVisible() ) {
1392 this.visible = show;
1393 this.$element.toggleClass( 'oo-ui-element-hidden', !this.visible );
1394 this.emit( 'toggle', show );
1395 }
1396
1397 return this;
1398 };
1399
1400 /**
1401 * Check if element is visible.
1402 *
1403 * @return {boolean} element is visible
1404 */
1405 OO.ui.Element.prototype.isVisible = function () {
1406 return this.visible;
1407 };
1408
1409 /**
1410 * Get element data.
1411 *
1412 * @return {Mixed} Element data
1413 */
1414 OO.ui.Element.prototype.getData = function () {
1415 return this.data;
1416 };
1417
1418 /**
1419 * Set element data.
1420 *
1421 * @param {Mixed} Element data
1422 * @chainable
1423 */
1424 OO.ui.Element.prototype.setData = function ( data ) {
1425 this.data = data;
1426 return this;
1427 };
1428
1429 /**
1430 * Check if element supports one or more methods.
1431 *
1432 * @param {string|string[]} methods Method or list of methods to check
1433 * @return {boolean} All methods are supported
1434 */
1435 OO.ui.Element.prototype.supports = function ( methods ) {
1436 var i, len,
1437 support = 0;
1438
1439 methods = Array.isArray( methods ) ? methods : [ methods ];
1440 for ( i = 0, len = methods.length; i < len; i++ ) {
1441 if ( $.isFunction( this[ methods[ i ] ] ) ) {
1442 support++;
1443 }
1444 }
1445
1446 return methods.length === support;
1447 };
1448
1449 /**
1450 * Update the theme-provided classes.
1451 *
1452 * @localdoc This is called in element mixins and widget classes any time state changes.
1453 * Updating is debounced, minimizing overhead of changing multiple attributes and
1454 * guaranteeing that theme updates do not occur within an element's constructor
1455 */
1456 OO.ui.Element.prototype.updateThemeClasses = function () {
1457 if ( !this.updateThemeClassesPending ) {
1458 this.updateThemeClassesPending = true;
1459 setTimeout( this.debouncedUpdateThemeClassesHandler );
1460 }
1461 };
1462
1463 /**
1464 * @private
1465 */
1466 OO.ui.Element.prototype.debouncedUpdateThemeClasses = function () {
1467 OO.ui.theme.updateElementClasses( this );
1468 this.updateThemeClassesPending = false;
1469 };
1470
1471 /**
1472 * Get the HTML tag name.
1473 *
1474 * Override this method to base the result on instance information.
1475 *
1476 * @return {string} HTML tag name
1477 */
1478 OO.ui.Element.prototype.getTagName = function () {
1479 return this.constructor.static.tagName;
1480 };
1481
1482 /**
1483 * Check if the element is attached to the DOM
1484 * @return {boolean} The element is attached to the DOM
1485 */
1486 OO.ui.Element.prototype.isElementAttached = function () {
1487 return $.contains( this.getElementDocument(), this.$element[ 0 ] );
1488 };
1489
1490 /**
1491 * Get the DOM document.
1492 *
1493 * @return {HTMLDocument} Document object
1494 */
1495 OO.ui.Element.prototype.getElementDocument = function () {
1496 // Don't cache this in other ways either because subclasses could can change this.$element
1497 return OO.ui.Element.static.getDocument( this.$element );
1498 };
1499
1500 /**
1501 * Get the DOM window.
1502 *
1503 * @return {Window} Window object
1504 */
1505 OO.ui.Element.prototype.getElementWindow = function () {
1506 return OO.ui.Element.static.getWindow( this.$element );
1507 };
1508
1509 /**
1510 * Get closest scrollable container.
1511 */
1512 OO.ui.Element.prototype.getClosestScrollableElementContainer = function () {
1513 return OO.ui.Element.static.getClosestScrollableContainer( this.$element[ 0 ] );
1514 };
1515
1516 /**
1517 * Get group element is in.
1518 *
1519 * @return {OO.ui.GroupElement|null} Group element, null if none
1520 */
1521 OO.ui.Element.prototype.getElementGroup = function () {
1522 return this.elementGroup;
1523 };
1524
1525 /**
1526 * Set group element is in.
1527 *
1528 * @param {OO.ui.GroupElement|null} group Group element, null if none
1529 * @chainable
1530 */
1531 OO.ui.Element.prototype.setElementGroup = function ( group ) {
1532 this.elementGroup = group;
1533 return this;
1534 };
1535
1536 /**
1537 * Scroll element into view.
1538 *
1539 * @param {Object} [config] Configuration options
1540 */
1541 OO.ui.Element.prototype.scrollElementIntoView = function ( config ) {
1542 return OO.ui.Element.static.scrollIntoView( this.$element[ 0 ], config );
1543 };
1544
1545 /**
1546 * Container for elements.
1547 *
1548 * @abstract
1549 * @class
1550 * @extends OO.ui.Element
1551 * @mixins OO.EventEmitter
1552 *
1553 * @constructor
1554 * @param {Object} [config] Configuration options
1555 */
1556 OO.ui.Layout = function OoUiLayout( config ) {
1557 // Configuration initialization
1558 config = config || {};
1559
1560 // Parent constructor
1561 OO.ui.Layout.super.call( this, config );
1562
1563 // Mixin constructors
1564 OO.EventEmitter.call( this );
1565
1566 // Initialization
1567 this.$element.addClass( 'oo-ui-layout' );
1568 };
1569
1570 /* Setup */
1571
1572 OO.inheritClass( OO.ui.Layout, OO.ui.Element );
1573 OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
1574
1575 /**
1576 * Widgets are compositions of one or more OOjs UI elements that users can both view
1577 * and interact with. All widgets can be configured and modified via a standard API,
1578 * and their state can change dynamically according to a model.
1579 *
1580 * @abstract
1581 * @class
1582 * @extends OO.ui.Element
1583 * @mixins OO.EventEmitter
1584 *
1585 * @constructor
1586 * @param {Object} [config] Configuration options
1587 * @cfg {boolean} [disabled=false] Disable
1588 */
1589 OO.ui.Widget = function OoUiWidget( config ) {
1590 // Initialize config
1591 config = $.extend( { disabled: false }, config );
1592
1593 // Parent constructor
1594 OO.ui.Widget.super.call( this, config );
1595
1596 // Mixin constructors
1597 OO.EventEmitter.call( this );
1598
1599 // Properties
1600 this.disabled = null;
1601 this.wasDisabled = null;
1602
1603 // Initialization
1604 this.$element.addClass( 'oo-ui-widget' );
1605 this.setDisabled( !!config.disabled );
1606 };
1607
1608 /* Setup */
1609
1610 OO.inheritClass( OO.ui.Widget, OO.ui.Element );
1611 OO.mixinClass( OO.ui.Widget, OO.EventEmitter );
1612
1613 /* Events */
1614
1615 /**
1616 * @event disable
1617 * @param {boolean} disabled Widget is disabled
1618 */
1619
1620 /**
1621 * @event toggle
1622 * @param {boolean} visible Widget is visible
1623 */
1624
1625 /* Methods */
1626
1627 /**
1628 * Check if the widget is disabled.
1629 *
1630 * @return {boolean} Button is disabled
1631 */
1632 OO.ui.Widget.prototype.isDisabled = function () {
1633 return this.disabled;
1634 };
1635
1636 /**
1637 * Set the disabled state of the widget.
1638 *
1639 * This should probably change the widgets' appearance and prevent it from being used.
1640 *
1641 * @param {boolean} disabled Disable widget
1642 * @chainable
1643 */
1644 OO.ui.Widget.prototype.setDisabled = function ( disabled ) {
1645 var isDisabled;
1646
1647 this.disabled = !!disabled;
1648 isDisabled = this.isDisabled();
1649 if ( isDisabled !== this.wasDisabled ) {
1650 this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled );
1651 this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled );
1652 this.$element.attr( 'aria-disabled', isDisabled.toString() );
1653 this.emit( 'disable', isDisabled );
1654 this.updateThemeClasses();
1655 }
1656 this.wasDisabled = isDisabled;
1657
1658 return this;
1659 };
1660
1661 /**
1662 * Update the disabled state, in case of changes in parent widget.
1663 *
1664 * @chainable
1665 */
1666 OO.ui.Widget.prototype.updateDisabled = function () {
1667 this.setDisabled( this.disabled );
1668 return this;
1669 };
1670
1671 /**
1672 * A window is a container for elements that are in a child frame. They are used with
1673 * a window manager (OO.ui.WindowManager), which is used to open and close the window and control
1674 * its presentation. The size of a window is specified using a symbolic name (e.g., ‘small’, ‘medium’,
1675 * ‘large’), which is interpreted by the window manager. If the requested size is not recognized,
1676 * the window manager will choose a sensible fallback.
1677 *
1678 * The lifecycle of a window has three primary stages (opening, opened, and closing) in which
1679 * different processes are executed:
1680 *
1681 * **opening**: The opening stage begins when the window manager's {@link OO.ui.WindowManager#openWindow
1682 * openWindow} or the window's {@link #open open} methods are used, and the window manager begins to open
1683 * the window.
1684 *
1685 * - {@link #getSetupProcess} method is called and its result executed
1686 * - {@link #getReadyProcess} method is called and its result executed
1687 *
1688 * **opened**: The window is now open
1689 *
1690 * **closing**: The closing stage begins when the window manager's
1691 * {@link OO.ui.WindowManager#closeWindow closeWindow}
1692 * or the window's {@link #close} methods are used, and the window manager begins to close the window.
1693 *
1694 * - {@link #getHoldProcess} method is called and its result executed
1695 * - {@link #getTeardownProcess} method is called and its result executed. The window is now closed
1696 *
1697 * Each of the window's processes (setup, ready, hold, and teardown) can be extended in subclasses
1698 * by overriding the window's #getSetupProcess, #getReadyProcess, #getHoldProcess and #getTeardownProcess
1699 * methods. Note that each {@link OO.ui.Process process} is executed in series, so asynchronous
1700 * processing can complete. Always assume window processes are executed asynchronously.
1701 *
1702 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
1703 *
1704 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows
1705 *
1706 * @abstract
1707 * @class
1708 * @extends OO.ui.Element
1709 * @mixins OO.EventEmitter
1710 *
1711 * @constructor
1712 * @param {Object} [config] Configuration options
1713 * @cfg {string} [size] Symbolic name of dialog size, `small`, `medium`, `large`, `larger` or
1714 * `full`; omit to use #static-size
1715 */
1716 OO.ui.Window = function OoUiWindow( config ) {
1717 // Configuration initialization
1718 config = config || {};
1719
1720 // Parent constructor
1721 OO.ui.Window.super.call( this, config );
1722
1723 // Mixin constructors
1724 OO.EventEmitter.call( this );
1725
1726 // Properties
1727 this.manager = null;
1728 this.size = config.size || this.constructor.static.size;
1729 this.$frame = $( '<div>' );
1730 this.$overlay = $( '<div>' );
1731 this.$content = $( '<div>' );
1732
1733 // Initialization
1734 this.$overlay.addClass( 'oo-ui-window-overlay' );
1735 this.$content
1736 .addClass( 'oo-ui-window-content' )
1737 .attr( 'tabIndex', 0 );
1738 this.$frame
1739 .addClass( 'oo-ui-window-frame' )
1740 .append( this.$content );
1741
1742 this.$element
1743 .addClass( 'oo-ui-window' )
1744 .append( this.$frame, this.$overlay );
1745
1746 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
1747 // that reference properties not initialized at that time of parent class construction
1748 // TODO: Find a better way to handle post-constructor setup
1749 this.visible = false;
1750 this.$element.addClass( 'oo-ui-element-hidden' );
1751 };
1752
1753 /* Setup */
1754
1755 OO.inheritClass( OO.ui.Window, OO.ui.Element );
1756 OO.mixinClass( OO.ui.Window, OO.EventEmitter );
1757
1758 /* Static Properties */
1759
1760 /**
1761 * Symbolic name of size.
1762 *
1763 * Size is used if no size is configured during construction.
1764 *
1765 * @static
1766 * @inheritable
1767 * @property {string}
1768 */
1769 OO.ui.Window.static.size = 'medium';
1770
1771 /* Methods */
1772
1773 /**
1774 * Handle mouse down events.
1775 *
1776 * @param {jQuery.Event} e Mouse down event
1777 */
1778 OO.ui.Window.prototype.onMouseDown = function ( e ) {
1779 // Prevent clicking on the click-block from stealing focus
1780 if ( e.target === this.$element[ 0 ] ) {
1781 return false;
1782 }
1783 };
1784
1785 /**
1786 * Check if window has been initialized.
1787 *
1788 * Initialization occurs when a window is added to a manager.
1789 *
1790 * @return {boolean} Window has been initialized
1791 */
1792 OO.ui.Window.prototype.isInitialized = function () {
1793 return !!this.manager;
1794 };
1795
1796 /**
1797 * Check if window is visible.
1798 *
1799 * @return {boolean} Window is visible
1800 */
1801 OO.ui.Window.prototype.isVisible = function () {
1802 return this.visible;
1803 };
1804
1805 /**
1806 * Check if window is opening.
1807 *
1808 * This is a wrapper around OO.ui.WindowManager#isOpening.
1809 *
1810 * @return {boolean} Window is opening
1811 */
1812 OO.ui.Window.prototype.isOpening = function () {
1813 return this.manager.isOpening( this );
1814 };
1815
1816 /**
1817 * Check if window is closing.
1818 *
1819 * This is a wrapper around OO.ui.WindowManager#isClosing.
1820 *
1821 * @return {boolean} Window is closing
1822 */
1823 OO.ui.Window.prototype.isClosing = function () {
1824 return this.manager.isClosing( this );
1825 };
1826
1827 /**
1828 * Check if window is opened.
1829 *
1830 * This is a wrapper around OO.ui.WindowManager#isOpened.
1831 *
1832 * @return {boolean} Window is opened
1833 */
1834 OO.ui.Window.prototype.isOpened = function () {
1835 return this.manager.isOpened( this );
1836 };
1837
1838 /**
1839 * Get the window manager.
1840 *
1841 * @return {OO.ui.WindowManager} Manager of window
1842 */
1843 OO.ui.Window.prototype.getManager = function () {
1844 return this.manager;
1845 };
1846
1847 /**
1848 * Get the window size.
1849 *
1850 * @return {string} Symbolic size name, e.g. `small`, `medium`, `large`, `larger`, `full`
1851 */
1852 OO.ui.Window.prototype.getSize = function () {
1853 return this.size;
1854 };
1855
1856 /**
1857 * Disable transitions on window's frame for the duration of the callback function, then enable them
1858 * back.
1859 *
1860 * @private
1861 * @param {Function} callback Function to call while transitions are disabled
1862 */
1863 OO.ui.Window.prototype.withoutSizeTransitions = function ( callback ) {
1864 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
1865 // Disable transitions first, otherwise we'll get values from when the window was animating.
1866 var oldTransition,
1867 styleObj = this.$frame[ 0 ].style;
1868 oldTransition = styleObj.transition || styleObj.OTransition || styleObj.MsTransition ||
1869 styleObj.MozTransition || styleObj.WebkitTransition;
1870 styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
1871 styleObj.MozTransition = styleObj.WebkitTransition = 'none';
1872 callback();
1873 // Force reflow to make sure the style changes done inside callback really are not transitioned
1874 this.$frame.height();
1875 styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
1876 styleObj.MozTransition = styleObj.WebkitTransition = oldTransition;
1877 };
1878
1879 /**
1880 * Get the height of the dialog contents.
1881 *
1882 * @return {number} Content height
1883 */
1884 OO.ui.Window.prototype.getContentHeight = function () {
1885 var bodyHeight,
1886 win = this,
1887 bodyStyleObj = this.$body[ 0 ].style,
1888 frameStyleObj = this.$frame[ 0 ].style;
1889
1890 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
1891 // Disable transitions first, otherwise we'll get values from when the window was animating.
1892 this.withoutSizeTransitions( function () {
1893 var oldHeight = frameStyleObj.height,
1894 oldPosition = bodyStyleObj.position;
1895 frameStyleObj.height = '1px';
1896 // Force body to resize to new width
1897 bodyStyleObj.position = 'relative';
1898 bodyHeight = win.getBodyHeight();
1899 frameStyleObj.height = oldHeight;
1900 bodyStyleObj.position = oldPosition;
1901 } );
1902
1903 return (
1904 // Add buffer for border
1905 ( this.$frame.outerHeight() - this.$frame.innerHeight() ) +
1906 // Use combined heights of children
1907 ( this.$head.outerHeight( true ) + bodyHeight + this.$foot.outerHeight( true ) )
1908 );
1909 };
1910
1911 /**
1912 * Get the height of the dialog contents.
1913 *
1914 * When this function is called, the dialog will temporarily have been resized
1915 * to height=1px, so .scrollHeight measurements can be taken accurately.
1916 *
1917 * @return {number} Height of content
1918 */
1919 OO.ui.Window.prototype.getBodyHeight = function () {
1920 return this.$body[ 0 ].scrollHeight;
1921 };
1922
1923 /**
1924 * Get the directionality of the frame
1925 *
1926 * @return {string} Directionality, 'ltr' or 'rtl'
1927 */
1928 OO.ui.Window.prototype.getDir = function () {
1929 return this.dir;
1930 };
1931
1932 /**
1933 * Get a process for setting up a window for use.
1934 *
1935 * Each time the window is opened this process will set it up for use in a particular context, based
1936 * on the `data` argument.
1937 *
1938 * When you override this method, you can add additional setup steps to the process the parent
1939 * method provides using the 'first' and 'next' methods.
1940 *
1941 * @abstract
1942 * @param {Object} [data] Window opening data
1943 * @return {OO.ui.Process} Setup process
1944 */
1945 OO.ui.Window.prototype.getSetupProcess = function () {
1946 return new OO.ui.Process();
1947 };
1948
1949 /**
1950 * Get a process for readying a window for use.
1951 *
1952 * Each time the window is open and setup, this process will ready it up for use in a particular
1953 * context, based on the `data` argument.
1954 *
1955 * When you override this method, you can add additional setup steps to the process the parent
1956 * method provides using the 'first' and 'next' methods.
1957 *
1958 * @abstract
1959 * @param {Object} [data] Window opening data
1960 * @return {OO.ui.Process} Setup process
1961 */
1962 OO.ui.Window.prototype.getReadyProcess = function () {
1963 return new OO.ui.Process();
1964 };
1965
1966 /**
1967 * Get a process for holding a window from use.
1968 *
1969 * Each time the window is closed, this process will hold it from use in a particular context, based
1970 * on the `data` argument.
1971 *
1972 * When you override this method, you can add additional setup steps to the process the parent
1973 * method provides using the 'first' and 'next' methods.
1974 *
1975 * @abstract
1976 * @param {Object} [data] Window closing data
1977 * @return {OO.ui.Process} Hold process
1978 */
1979 OO.ui.Window.prototype.getHoldProcess = function () {
1980 return new OO.ui.Process();
1981 };
1982
1983 /**
1984 * Get a process for tearing down a window after use.
1985 *
1986 * Each time the window is closed this process will tear it down and do something with the user's
1987 * interactions within the window, based on the `data` argument.
1988 *
1989 * When you override this method, you can add additional teardown steps to the process the parent
1990 * method provides using the 'first' and 'next' methods.
1991 *
1992 * @abstract
1993 * @param {Object} [data] Window closing data
1994 * @return {OO.ui.Process} Teardown process
1995 */
1996 OO.ui.Window.prototype.getTeardownProcess = function () {
1997 return new OO.ui.Process();
1998 };
1999
2000 /**
2001 * Set the window manager.
2002 *
2003 * This will cause the window to initialize. Calling it more than once will cause an error.
2004 *
2005 * @param {OO.ui.WindowManager} manager Manager for this window
2006 * @throws {Error} If called more than once
2007 * @chainable
2008 */
2009 OO.ui.Window.prototype.setManager = function ( manager ) {
2010 if ( this.manager ) {
2011 throw new Error( 'Cannot set window manager, window already has a manager' );
2012 }
2013
2014 this.manager = manager;
2015 this.initialize();
2016
2017 return this;
2018 };
2019
2020 /**
2021 * Set the window size.
2022 *
2023 * @param {string} size Symbolic size name, e.g. 'small', 'medium', 'large', 'full'
2024 * @chainable
2025 */
2026 OO.ui.Window.prototype.setSize = function ( size ) {
2027 this.size = size;
2028 this.updateSize();
2029 return this;
2030 };
2031
2032 /**
2033 * Update the window size.
2034 *
2035 * @throws {Error} If not attached to a manager
2036 * @chainable
2037 */
2038 OO.ui.Window.prototype.updateSize = function () {
2039 if ( !this.manager ) {
2040 throw new Error( 'Cannot update window size, must be attached to a manager' );
2041 }
2042
2043 this.manager.updateWindowSize( this );
2044
2045 return this;
2046 };
2047
2048 /**
2049 * Set window dimensions.
2050 *
2051 * Properties are applied to the frame container.
2052 *
2053 * @param {Object} dim CSS dimension properties
2054 * @param {string|number} [dim.width] Width
2055 * @param {string|number} [dim.minWidth] Minimum width
2056 * @param {string|number} [dim.maxWidth] Maximum width
2057 * @param {string|number} [dim.width] Height, omit to set based on height of contents
2058 * @param {string|number} [dim.minWidth] Minimum height
2059 * @param {string|number} [dim.maxWidth] Maximum height
2060 * @chainable
2061 */
2062 OO.ui.Window.prototype.setDimensions = function ( dim ) {
2063 var height,
2064 win = this,
2065 styleObj = this.$frame[ 0 ].style;
2066
2067 // Calculate the height we need to set using the correct width
2068 if ( dim.height === undefined ) {
2069 this.withoutSizeTransitions( function () {
2070 var oldWidth = styleObj.width;
2071 win.$frame.css( 'width', dim.width || '' );
2072 height = win.getContentHeight();
2073 styleObj.width = oldWidth;
2074 } );
2075 } else {
2076 height = dim.height;
2077 }
2078
2079 this.$frame.css( {
2080 width: dim.width || '',
2081 minWidth: dim.minWidth || '',
2082 maxWidth: dim.maxWidth || '',
2083 height: height || '',
2084 minHeight: dim.minHeight || '',
2085 maxHeight: dim.maxHeight || ''
2086 } );
2087
2088 return this;
2089 };
2090
2091 /**
2092 * Initialize window contents.
2093 *
2094 * The first time the window is opened, #initialize is called so that changes to the window that
2095 * will persist between openings can be made. See #getSetupProcess for a way to make changes each
2096 * time the window opens.
2097 *
2098 * @throws {Error} If not attached to a manager
2099 * @chainable
2100 */
2101 OO.ui.Window.prototype.initialize = function () {
2102 if ( !this.manager ) {
2103 throw new Error( 'Cannot initialize window, must be attached to a manager' );
2104 }
2105
2106 // Properties
2107 this.$head = $( '<div>' );
2108 this.$body = $( '<div>' );
2109 this.$foot = $( '<div>' );
2110 this.dir = OO.ui.Element.static.getDir( this.$content ) || 'ltr';
2111 this.$document = $( this.getElementDocument() );
2112
2113 // Events
2114 this.$element.on( 'mousedown', this.onMouseDown.bind( this ) );
2115
2116 // Initialization
2117 this.$head.addClass( 'oo-ui-window-head' );
2118 this.$body.addClass( 'oo-ui-window-body' );
2119 this.$foot.addClass( 'oo-ui-window-foot' );
2120 this.$content.append( this.$head, this.$body, this.$foot );
2121
2122 return this;
2123 };
2124
2125 /**
2126 * Open window.
2127 *
2128 * This is a wrapper around calling {@link OO.ui.WindowManager#openWindow} on the window manager.
2129 * To do something each time the window opens, use #getSetupProcess or #getReadyProcess.
2130 *
2131 * @param {Object} [data] Window opening data
2132 * @return {jQuery.Promise} Promise resolved when window is opened; when the promise is resolved the
2133 * first argument will be a promise which will be resolved when the window begins closing
2134 * @throws {Error} If not attached to a manager
2135 */
2136 OO.ui.Window.prototype.open = function ( data ) {
2137 if ( !this.manager ) {
2138 throw new Error( 'Cannot open window, must be attached to a manager' );
2139 }
2140
2141 return this.manager.openWindow( this, data );
2142 };
2143
2144 /**
2145 * Close window.
2146 *
2147 * This is a wrapper around calling OO.ui.WindowManager#closeWindow on the window manager.
2148 * To do something each time the window closes, use #getHoldProcess or #getTeardownProcess.
2149 *
2150 * @param {Object} [data] Window closing data
2151 * @return {jQuery.Promise} Promise resolved when window is closed
2152 * @throws {Error} If not attached to a manager
2153 */
2154 OO.ui.Window.prototype.close = function ( data ) {
2155 if ( !this.manager ) {
2156 throw new Error( 'Cannot close window, must be attached to a manager' );
2157 }
2158
2159 return this.manager.closeWindow( this, data );
2160 };
2161
2162 /**
2163 * Setup window.
2164 *
2165 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2166 * by other systems.
2167 *
2168 * @param {Object} [data] Window opening data
2169 * @return {jQuery.Promise} Promise resolved when window is setup
2170 */
2171 OO.ui.Window.prototype.setup = function ( data ) {
2172 var win = this,
2173 deferred = $.Deferred();
2174
2175 this.toggle( true );
2176
2177 this.getSetupProcess( data ).execute().done( function () {
2178 // Force redraw by asking the browser to measure the elements' widths
2179 win.$element.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2180 win.$content.addClass( 'oo-ui-window-content-setup' ).width();
2181 deferred.resolve();
2182 } );
2183
2184 return deferred.promise();
2185 };
2186
2187 /**
2188 * Ready window.
2189 *
2190 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2191 * by other systems.
2192 *
2193 * @param {Object} [data] Window opening data
2194 * @return {jQuery.Promise} Promise resolved when window is ready
2195 */
2196 OO.ui.Window.prototype.ready = function ( data ) {
2197 var win = this,
2198 deferred = $.Deferred();
2199
2200 this.$content.focus();
2201 this.getReadyProcess( data ).execute().done( function () {
2202 // Force redraw by asking the browser to measure the elements' widths
2203 win.$element.addClass( 'oo-ui-window-ready' ).width();
2204 win.$content.addClass( 'oo-ui-window-content-ready' ).width();
2205 deferred.resolve();
2206 } );
2207
2208 return deferred.promise();
2209 };
2210
2211 /**
2212 * Hold window.
2213 *
2214 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2215 * by other systems.
2216 *
2217 * @param {Object} [data] Window closing data
2218 * @return {jQuery.Promise} Promise resolved when window is held
2219 */
2220 OO.ui.Window.prototype.hold = function ( data ) {
2221 var win = this,
2222 deferred = $.Deferred();
2223
2224 this.getHoldProcess( data ).execute().done( function () {
2225 // Get the focused element within the window's content
2226 var $focus = win.$content.find( OO.ui.Element.static.getDocument( win.$content ).activeElement );
2227
2228 // Blur the focused element
2229 if ( $focus.length ) {
2230 $focus[ 0 ].blur();
2231 }
2232
2233 // Force redraw by asking the browser to measure the elements' widths
2234 win.$element.removeClass( 'oo-ui-window-ready' ).width();
2235 win.$content.removeClass( 'oo-ui-window-content-ready' ).width();
2236 deferred.resolve();
2237 } );
2238
2239 return deferred.promise();
2240 };
2241
2242 /**
2243 * Teardown window.
2244 *
2245 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2246 * by other systems.
2247 *
2248 * @param {Object} [data] Window closing data
2249 * @return {jQuery.Promise} Promise resolved when window is torn down
2250 */
2251 OO.ui.Window.prototype.teardown = function ( data ) {
2252 var win = this;
2253
2254 return this.getTeardownProcess( data ).execute()
2255 .done( function () {
2256 // Force redraw by asking the browser to measure the elements' widths
2257 win.$element.removeClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2258 win.$content.removeClass( 'oo-ui-window-content-setup' ).width();
2259 win.toggle( false );
2260 } );
2261 };
2262
2263 /**
2264 * The Dialog class serves as the base class for the other types of dialogs.
2265 * Unless extended to include controls, the rendered dialog box is a simple window
2266 * that users can close by hitting the ‘Esc’ key. Dialog windows are used with OO.ui.WindowManager,
2267 * which opens, closes, and controls the presentation of the window. See the
2268 * [OOjs UI documentation on MediaWiki] [1] for more information.
2269 *
2270 * @example
2271 * // A simple dialog window.
2272 * function MyDialog( config ) {
2273 * MyDialog.super.call( this, config );
2274 * }
2275 * OO.inheritClass( MyDialog, OO.ui.Dialog );
2276 * MyDialog.prototype.initialize = function () {
2277 * MyDialog.super.prototype.initialize.call( this );
2278 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
2279 * this.content.$element.append( '<p>A simple dialog window. Press \'Esc\' to close.</p>' );
2280 * this.$body.append( this.content.$element );
2281 * };
2282 * MyDialog.prototype.getBodyHeight = function () {
2283 * return this.content.$element.outerHeight( true );
2284 * };
2285 * var myDialog = new MyDialog( {
2286 * size: 'medium'
2287 * } );
2288 * // Create and append a window manager, which opens and closes the window.
2289 * var windowManager = new OO.ui.WindowManager();
2290 * $( 'body' ).append( windowManager.$element );
2291 * windowManager.addWindows( [ myDialog ] );
2292 * // Open the window!
2293 * windowManager.openWindow( myDialog );
2294 *
2295 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Dialogs
2296 *
2297 * @abstract
2298 * @class
2299 * @extends OO.ui.Window
2300 * @mixins OO.ui.PendingElement
2301 *
2302 * @constructor
2303 * @param {Object} [config] Configuration options
2304 */
2305 OO.ui.Dialog = function OoUiDialog( config ) {
2306 // Parent constructor
2307 OO.ui.Dialog.super.call( this, config );
2308
2309 // Mixin constructors
2310 OO.ui.PendingElement.call( this );
2311
2312 // Properties
2313 this.actions = new OO.ui.ActionSet();
2314 this.attachedActions = [];
2315 this.currentAction = null;
2316 this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
2317
2318 // Events
2319 this.actions.connect( this, {
2320 click: 'onActionClick',
2321 resize: 'onActionResize',
2322 change: 'onActionsChange'
2323 } );
2324
2325 // Initialization
2326 this.$element
2327 .addClass( 'oo-ui-dialog' )
2328 .attr( 'role', 'dialog' );
2329 };
2330
2331 /* Setup */
2332
2333 OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
2334 OO.mixinClass( OO.ui.Dialog, OO.ui.PendingElement );
2335
2336 /* Static Properties */
2337
2338 /**
2339 * Symbolic name of dialog.
2340 *
2341 * @abstract
2342 * @static
2343 * @inheritable
2344 * @property {string}
2345 */
2346 OO.ui.Dialog.static.name = '';
2347
2348 /**
2349 * Dialog title.
2350 *
2351 * @abstract
2352 * @static
2353 * @inheritable
2354 * @property {jQuery|string|Function} Label nodes, text or a function that returns nodes or text
2355 */
2356 OO.ui.Dialog.static.title = '';
2357
2358 /**
2359 * List of OO.ui.ActionWidget configuration options.
2360 *
2361 * @static
2362 * inheritable
2363 * @property {Object[]}
2364 */
2365 OO.ui.Dialog.static.actions = [];
2366
2367 /**
2368 * Close dialog when the escape key is pressed.
2369 *
2370 * @static
2371 * @abstract
2372 * @inheritable
2373 * @property {boolean}
2374 */
2375 OO.ui.Dialog.static.escapable = true;
2376
2377 /* Methods */
2378
2379 /**
2380 * Handle frame document key down events.
2381 *
2382 * @param {jQuery.Event} e Key down event
2383 */
2384 OO.ui.Dialog.prototype.onDocumentKeyDown = function ( e ) {
2385 if ( e.which === OO.ui.Keys.ESCAPE ) {
2386 this.close();
2387 e.preventDefault();
2388 e.stopPropagation();
2389 }
2390 };
2391
2392 /**
2393 * Handle action resized events.
2394 *
2395 * @param {OO.ui.ActionWidget} action Action that was resized
2396 */
2397 OO.ui.Dialog.prototype.onActionResize = function () {
2398 // Override in subclass
2399 };
2400
2401 /**
2402 * Handle action click events.
2403 *
2404 * @param {OO.ui.ActionWidget} action Action that was clicked
2405 */
2406 OO.ui.Dialog.prototype.onActionClick = function ( action ) {
2407 if ( !this.isPending() ) {
2408 this.executeAction( action.getAction() );
2409 }
2410 };
2411
2412 /**
2413 * Handle actions change event.
2414 */
2415 OO.ui.Dialog.prototype.onActionsChange = function () {
2416 this.detachActions();
2417 if ( !this.isClosing() ) {
2418 this.attachActions();
2419 }
2420 };
2421
2422 /**
2423 * Get set of actions.
2424 *
2425 * @return {OO.ui.ActionSet}
2426 */
2427 OO.ui.Dialog.prototype.getActions = function () {
2428 return this.actions;
2429 };
2430
2431 /**
2432 * Get a process for taking action.
2433 *
2434 * When you override this method, you can add additional accept steps to the process the parent
2435 * method provides using the 'first' and 'next' methods.
2436 *
2437 * @abstract
2438 * @param {string} [action] Symbolic name of action
2439 * @return {OO.ui.Process} Action process
2440 */
2441 OO.ui.Dialog.prototype.getActionProcess = function ( action ) {
2442 return new OO.ui.Process()
2443 .next( function () {
2444 if ( !action ) {
2445 // An empty action always closes the dialog without data, which should always be
2446 // safe and make no changes
2447 this.close();
2448 }
2449 }, this );
2450 };
2451
2452 /**
2453 * @inheritdoc
2454 *
2455 * @param {Object} [data] Dialog opening data
2456 * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use #static-title
2457 * @param {Object[]} [data.actions] List of OO.ui.ActionWidget configuration options for each
2458 * action item, omit to use #static-actions
2459 */
2460 OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
2461 data = data || {};
2462
2463 // Parent method
2464 return OO.ui.Dialog.super.prototype.getSetupProcess.call( this, data )
2465 .next( function () {
2466 var i, len,
2467 items = [],
2468 config = this.constructor.static,
2469 actions = data.actions !== undefined ? data.actions : config.actions;
2470
2471 this.title.setLabel(
2472 data.title !== undefined ? data.title : this.constructor.static.title
2473 );
2474 for ( i = 0, len = actions.length; i < len; i++ ) {
2475 items.push(
2476 new OO.ui.ActionWidget( actions[ i ] )
2477 );
2478 }
2479 this.actions.add( items );
2480
2481 if ( this.constructor.static.escapable ) {
2482 this.$document.on( 'keydown', this.onDocumentKeyDownHandler );
2483 }
2484 }, this );
2485 };
2486
2487 /**
2488 * @inheritdoc
2489 */
2490 OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
2491 // Parent method
2492 return OO.ui.Dialog.super.prototype.getTeardownProcess.call( this, data )
2493 .first( function () {
2494 if ( this.constructor.static.escapable ) {
2495 this.$document.off( 'keydown', this.onDocumentKeyDownHandler );
2496 }
2497
2498 this.actions.clear();
2499 this.currentAction = null;
2500 }, this );
2501 };
2502
2503 /**
2504 * @inheritdoc
2505 */
2506 OO.ui.Dialog.prototype.initialize = function () {
2507 // Parent method
2508 OO.ui.Dialog.super.prototype.initialize.call( this );
2509
2510 // Properties
2511 this.title = new OO.ui.LabelWidget();
2512
2513 // Initialization
2514 this.$content.addClass( 'oo-ui-dialog-content' );
2515 this.setPendingElement( this.$head );
2516 };
2517
2518 /**
2519 * Attach action actions.
2520 */
2521 OO.ui.Dialog.prototype.attachActions = function () {
2522 // Remember the list of potentially attached actions
2523 this.attachedActions = this.actions.get();
2524 };
2525
2526 /**
2527 * Detach action actions.
2528 *
2529 * @chainable
2530 */
2531 OO.ui.Dialog.prototype.detachActions = function () {
2532 var i, len;
2533
2534 // Detach all actions that may have been previously attached
2535 for ( i = 0, len = this.attachedActions.length; i < len; i++ ) {
2536 this.attachedActions[ i ].$element.detach();
2537 }
2538 this.attachedActions = [];
2539 };
2540
2541 /**
2542 * Execute an action.
2543 *
2544 * @param {string} action Symbolic name of action to execute
2545 * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
2546 */
2547 OO.ui.Dialog.prototype.executeAction = function ( action ) {
2548 this.pushPending();
2549 this.currentAction = action;
2550 return this.getActionProcess( action ).execute()
2551 .always( this.popPending.bind( this ) );
2552 };
2553
2554 /**
2555 * Window managers are used to open and close {@link OO.ui.Window windows} and control their presentation.
2556 * Managed windows are mutually exclusive. If a new window is opened while a current window is opening
2557 * or is opened, the current window will be closed and any ongoing {@link OO.ui.Process process} will be cancelled. Windows
2558 * themselves are persistent and—rather than being torn down when closed—can be repopulated with the
2559 * pertinent data and reused.
2560 *
2561 * Over the lifecycle of a window, the window manager makes available three promises: `opening`,
2562 * `opened`, and `closing`, which represent the primary stages of the cycle:
2563 *
2564 * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s
2565 * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window.
2566 *
2567 * - an `opening` event is emitted with an `opening` promise
2568 * - the #getSetupDelay method is called and the returned value is used to time a pause in execution before
2569 * the window’s {@link OO.ui.Window#getSetupProcess getSetupProcess} method is called on the
2570 * window and its result executed
2571 * - a `setup` progress notification is emitted from the `opening` promise
2572 * - the #getReadyDelay method is called the returned value is used to time a pause in execution before
2573 * the window’s {@link OO.ui.Window#getReadyProcess getReadyProcess} method is called on the
2574 * window and its result executed
2575 * - a `ready` progress notification is emitted from the `opening` promise
2576 * - the `opening` promise is resolved with an `opened` promise
2577 *
2578 * **Opened**: the window is now open.
2579 *
2580 * **Closing**: the closing stage begins when the window manager's #closeWindow or the
2581 * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins
2582 * to close the window.
2583 *
2584 * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted
2585 * - the #getHoldDelay method is called and the returned value is used to time a pause in execution before
2586 * the window's {@link OO.ui.Window#getHoldProcess getHoldProces} method is called on the
2587 * window and its result executed
2588 * - a `hold` progress notification is emitted from the `closing` promise
2589 * - the #getTeardownDelay() method is called and the returned value is used to time a pause in execution before
2590 * the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method is called on the
2591 * window and its result executed
2592 * - a `teardown` progress notification is emitted from the `closing` promise
2593 * - the `closing` promise is resolved. The window is now closed
2594 *
2595 * See the [OOjs UI documentation on MediaWiki][1] for more information.
2596 *
2597 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
2598 *
2599 * @class
2600 * @extends OO.ui.Element
2601 * @mixins OO.EventEmitter
2602 *
2603 * @constructor
2604 * @param {Object} [config] Configuration options
2605 * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
2606 * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
2607 */
2608 OO.ui.WindowManager = function OoUiWindowManager( config ) {
2609 // Configuration initialization
2610 config = config || {};
2611
2612 // Parent constructor
2613 OO.ui.WindowManager.super.call( this, config );
2614
2615 // Mixin constructors
2616 OO.EventEmitter.call( this );
2617
2618 // Properties
2619 this.factory = config.factory;
2620 this.modal = config.modal === undefined || !!config.modal;
2621 this.windows = {};
2622 this.opening = null;
2623 this.opened = null;
2624 this.closing = null;
2625 this.preparingToOpen = null;
2626 this.preparingToClose = null;
2627 this.currentWindow = null;
2628 this.globalEvents = false;
2629 this.$ariaHidden = null;
2630 this.onWindowResizeTimeout = null;
2631 this.onWindowResizeHandler = this.onWindowResize.bind( this );
2632 this.afterWindowResizeHandler = this.afterWindowResize.bind( this );
2633
2634 // Initialization
2635 this.$element
2636 .addClass( 'oo-ui-windowManager' )
2637 .toggleClass( 'oo-ui-windowManager-modal', this.modal );
2638 };
2639
2640 /* Setup */
2641
2642 OO.inheritClass( OO.ui.WindowManager, OO.ui.Element );
2643 OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter );
2644
2645 /* Events */
2646
2647 /**
2648 * Window is opening.
2649 *
2650 * Fired when the window begins to be opened.
2651 *
2652 * @event opening
2653 * @param {OO.ui.Window} win Window that's being opened
2654 * @param {jQuery.Promise} opening Promise resolved when window is opened; when the promise is
2655 * resolved the first argument will be a promise which will be resolved when the window begins
2656 * closing, the second argument will be the opening data; progress notifications will be fired on
2657 * the promise for `setup` and `ready` when those processes are completed respectively.
2658 * @param {Object} data Window opening data
2659 */
2660
2661 /**
2662 * Window is closing.
2663 *
2664 * Fired when the window begins to be closed.
2665 *
2666 * @event closing
2667 * @param {OO.ui.Window} win Window that's being closed
2668 * @param {jQuery.Promise} opening Promise resolved when window is closed; when the promise
2669 * is resolved the first argument will be a the closing data; progress notifications will be fired
2670 * on the promise for `hold` and `teardown` when those processes are completed respectively.
2671 * @param {Object} data Window closing data
2672 */
2673
2674 /**
2675 * Window was resized.
2676 *
2677 * @event resize
2678 * @param {OO.ui.Window} win Window that was resized
2679 */
2680
2681 /* Static Properties */
2682
2683 /**
2684 * Map of symbolic size names and CSS properties.
2685 *
2686 * @static
2687 * @inheritable
2688 * @property {Object}
2689 */
2690 OO.ui.WindowManager.static.sizes = {
2691 small: {
2692 width: 300
2693 },
2694 medium: {
2695 width: 500
2696 },
2697 large: {
2698 width: 700
2699 },
2700 larger: {
2701 width: 900
2702 },
2703 full: {
2704 // These can be non-numeric because they are never used in calculations
2705 width: '100%',
2706 height: '100%'
2707 }
2708 };
2709
2710 /**
2711 * Symbolic name of default size.
2712 *
2713 * Default size is used if the window's requested size is not recognized.
2714 *
2715 * @static
2716 * @inheritable
2717 * @property {string}
2718 */
2719 OO.ui.WindowManager.static.defaultSize = 'medium';
2720
2721 /* Methods */
2722
2723 /**
2724 * Handle window resize events.
2725 *
2726 * @param {jQuery.Event} e Window resize event
2727 */
2728 OO.ui.WindowManager.prototype.onWindowResize = function () {
2729 clearTimeout( this.onWindowResizeTimeout );
2730 this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 );
2731 };
2732
2733 /**
2734 * Handle window resize events.
2735 *
2736 * @param {jQuery.Event} e Window resize event
2737 */
2738 OO.ui.WindowManager.prototype.afterWindowResize = function () {
2739 if ( this.currentWindow ) {
2740 this.updateWindowSize( this.currentWindow );
2741 }
2742 };
2743
2744 /**
2745 * Check if window is opening.
2746 *
2747 * @return {boolean} Window is opening
2748 */
2749 OO.ui.WindowManager.prototype.isOpening = function ( win ) {
2750 return win === this.currentWindow && !!this.opening && this.opening.state() === 'pending';
2751 };
2752
2753 /**
2754 * Check if window is closing.
2755 *
2756 * @return {boolean} Window is closing
2757 */
2758 OO.ui.WindowManager.prototype.isClosing = function ( win ) {
2759 return win === this.currentWindow && !!this.closing && this.closing.state() === 'pending';
2760 };
2761
2762 /**
2763 * Check if window is opened.
2764 *
2765 * @return {boolean} Window is opened
2766 */
2767 OO.ui.WindowManager.prototype.isOpened = function ( win ) {
2768 return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending';
2769 };
2770
2771 /**
2772 * Check if a window is being managed.
2773 *
2774 * @param {OO.ui.Window} win Window to check
2775 * @return {boolean} Window is being managed
2776 */
2777 OO.ui.WindowManager.prototype.hasWindow = function ( win ) {
2778 var name;
2779
2780 for ( name in this.windows ) {
2781 if ( this.windows[ name ] === win ) {
2782 return true;
2783 }
2784 }
2785
2786 return false;
2787 };
2788
2789 /**
2790 * Get the number of milliseconds to wait between beginning opening and executing setup process.
2791 *
2792 * @param {OO.ui.Window} win Window being opened
2793 * @param {Object} [data] Window opening data
2794 * @return {number} Milliseconds to wait
2795 */
2796 OO.ui.WindowManager.prototype.getSetupDelay = function () {
2797 return 0;
2798 };
2799
2800 /**
2801 * Get the number of milliseconds to wait between finishing setup and executing ready process.
2802 *
2803 * @param {OO.ui.Window} win Window being opened
2804 * @param {Object} [data] Window opening data
2805 * @return {number} Milliseconds to wait
2806 */
2807 OO.ui.WindowManager.prototype.getReadyDelay = function () {
2808 return 0;
2809 };
2810
2811 /**
2812 * Get the number of milliseconds to wait between beginning closing and executing hold process.
2813 *
2814 * @param {OO.ui.Window} win Window being closed
2815 * @param {Object} [data] Window closing data
2816 * @return {number} Milliseconds to wait
2817 */
2818 OO.ui.WindowManager.prototype.getHoldDelay = function () {
2819 return 0;
2820 };
2821
2822 /**
2823 * Get the number of milliseconds to wait between finishing hold and executing teardown process.
2824 *
2825 * @param {OO.ui.Window} win Window being closed
2826 * @param {Object} [data] Window closing data
2827 * @return {number} Milliseconds to wait
2828 */
2829 OO.ui.WindowManager.prototype.getTeardownDelay = function () {
2830 return this.modal ? 250 : 0;
2831 };
2832
2833 /**
2834 * Get managed window by symbolic name.
2835 *
2836 * If window is not yet instantiated, it will be instantiated and added automatically.
2837 *
2838 * @param {string} name Symbolic window name
2839 * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
2840 * @throws {Error} If the symbolic name is unrecognized by the factory
2841 * @throws {Error} If the symbolic name unrecognized as a managed window
2842 */
2843 OO.ui.WindowManager.prototype.getWindow = function ( name ) {
2844 var deferred = $.Deferred(),
2845 win = this.windows[ name ];
2846
2847 if ( !( win instanceof OO.ui.Window ) ) {
2848 if ( this.factory ) {
2849 if ( !this.factory.lookup( name ) ) {
2850 deferred.reject( new OO.ui.Error(
2851 'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
2852 ) );
2853 } else {
2854 win = this.factory.create( name );
2855 this.addWindows( [ win ] );
2856 deferred.resolve( win );
2857 }
2858 } else {
2859 deferred.reject( new OO.ui.Error(
2860 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
2861 ) );
2862 }
2863 } else {
2864 deferred.resolve( win );
2865 }
2866
2867 return deferred.promise();
2868 };
2869
2870 /**
2871 * Get current window.
2872 *
2873 * @return {OO.ui.Window|null} Currently opening/opened/closing window
2874 */
2875 OO.ui.WindowManager.prototype.getCurrentWindow = function () {
2876 return this.currentWindow;
2877 };
2878
2879 /**
2880 * Open a window.
2881 *
2882 * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
2883 * @param {Object} [data] Window opening data
2884 * @return {jQuery.Promise} Promise resolved when window is done opening; see {@link #event-opening}
2885 * for more details about the `opening` promise
2886 * @fires opening
2887 */
2888 OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
2889 var manager = this,
2890 opening = $.Deferred();
2891
2892 // Argument handling
2893 if ( typeof win === 'string' ) {
2894 return this.getWindow( win ).then( function ( win ) {
2895 return manager.openWindow( win, data );
2896 } );
2897 }
2898
2899 // Error handling
2900 if ( !this.hasWindow( win ) ) {
2901 opening.reject( new OO.ui.Error(
2902 'Cannot open window: window is not attached to manager'
2903 ) );
2904 } else if ( this.preparingToOpen || this.opening || this.opened ) {
2905 opening.reject( new OO.ui.Error(
2906 'Cannot open window: another window is opening or open'
2907 ) );
2908 }
2909
2910 // Window opening
2911 if ( opening.state() !== 'rejected' ) {
2912 // If a window is currently closing, wait for it to complete
2913 this.preparingToOpen = $.when( this.closing );
2914 // Ensure handlers get called after preparingToOpen is set
2915 this.preparingToOpen.done( function () {
2916 if ( manager.modal ) {
2917 manager.toggleGlobalEvents( true );
2918 manager.toggleAriaIsolation( true );
2919 }
2920 manager.currentWindow = win;
2921 manager.opening = opening;
2922 manager.preparingToOpen = null;
2923 manager.emit( 'opening', win, opening, data );
2924 setTimeout( function () {
2925 win.setup( data ).then( function () {
2926 manager.updateWindowSize( win );
2927 manager.opening.notify( { state: 'setup' } );
2928 setTimeout( function () {
2929 win.ready( data ).then( function () {
2930 manager.opening.notify( { state: 'ready' } );
2931 manager.opening = null;
2932 manager.opened = $.Deferred();
2933 opening.resolve( manager.opened.promise(), data );
2934 } );
2935 }, manager.getReadyDelay() );
2936 } );
2937 }, manager.getSetupDelay() );
2938 } );
2939 }
2940
2941 return opening.promise();
2942 };
2943
2944 /**
2945 * Close a window.
2946 *
2947 * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
2948 * @param {Object} [data] Window closing data
2949 * @return {jQuery.Promise} Promise resolved when window is done closing; see {@link #event-closing}
2950 * for more details about the `closing` promise
2951 * @throws {Error} If no window by that name is being managed
2952 * @fires closing
2953 */
2954 OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
2955 var manager = this,
2956 closing = $.Deferred(),
2957 opened;
2958
2959 // Argument handling
2960 if ( typeof win === 'string' ) {
2961 win = this.windows[ win ];
2962 } else if ( !this.hasWindow( win ) ) {
2963 win = null;
2964 }
2965
2966 // Error handling
2967 if ( !win ) {
2968 closing.reject( new OO.ui.Error(
2969 'Cannot close window: window is not attached to manager'
2970 ) );
2971 } else if ( win !== this.currentWindow ) {
2972 closing.reject( new OO.ui.Error(
2973 'Cannot close window: window already closed with different data'
2974 ) );
2975 } else if ( this.preparingToClose || this.closing ) {
2976 closing.reject( new OO.ui.Error(
2977 'Cannot close window: window already closing with different data'
2978 ) );
2979 }
2980
2981 // Window closing
2982 if ( closing.state() !== 'rejected' ) {
2983 // If the window is currently opening, close it when it's done
2984 this.preparingToClose = $.when( this.opening );
2985 // Ensure handlers get called after preparingToClose is set
2986 this.preparingToClose.done( function () {
2987 manager.closing = closing;
2988 manager.preparingToClose = null;
2989 manager.emit( 'closing', win, closing, data );
2990 opened = manager.opened;
2991 manager.opened = null;
2992 opened.resolve( closing.promise(), data );
2993 setTimeout( function () {
2994 win.hold( data ).then( function () {
2995 closing.notify( { state: 'hold' } );
2996 setTimeout( function () {
2997 win.teardown( data ).then( function () {
2998 closing.notify( { state: 'teardown' } );
2999 if ( manager.modal ) {
3000 manager.toggleGlobalEvents( false );
3001 manager.toggleAriaIsolation( false );
3002 }
3003 manager.closing = null;
3004 manager.currentWindow = null;
3005 closing.resolve( data );
3006 } );
3007 }, manager.getTeardownDelay() );
3008 } );
3009 }, manager.getHoldDelay() );
3010 } );
3011 }
3012
3013 return closing.promise();
3014 };
3015
3016 /**
3017 * Add windows.
3018 *
3019 * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows Windows to add
3020 * @throws {Error} If one of the windows being added without an explicit symbolic name does not have
3021 * a statically configured symbolic name
3022 */
3023 OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
3024 var i, len, win, name, list;
3025
3026 if ( Array.isArray( windows ) ) {
3027 // Convert to map of windows by looking up symbolic names from static configuration
3028 list = {};
3029 for ( i = 0, len = windows.length; i < len; i++ ) {
3030 name = windows[ i ].constructor.static.name;
3031 if ( typeof name !== 'string' ) {
3032 throw new Error( 'Cannot add window' );
3033 }
3034 list[ name ] = windows[ i ];
3035 }
3036 } else if ( OO.isPlainObject( windows ) ) {
3037 list = windows;
3038 }
3039
3040 // Add windows
3041 for ( name in list ) {
3042 win = list[ name ];
3043 this.windows[ name ] = win.toggle( false );
3044 this.$element.append( win.$element );
3045 win.setManager( this );
3046 }
3047 };
3048
3049 /**
3050 * Remove windows.
3051 *
3052 * Windows will be closed before they are removed.
3053 *
3054 * @param {string[]} names Symbolic names of windows to remove
3055 * @return {jQuery.Promise} Promise resolved when window is closed and removed
3056 * @throws {Error} If windows being removed are not being managed
3057 */
3058 OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
3059 var i, len, win, name, cleanupWindow,
3060 manager = this,
3061 promises = [],
3062 cleanup = function ( name, win ) {
3063 delete manager.windows[ name ];
3064 win.$element.detach();
3065 };
3066
3067 for ( i = 0, len = names.length; i < len; i++ ) {
3068 name = names[ i ];
3069 win = this.windows[ name ];
3070 if ( !win ) {
3071 throw new Error( 'Cannot remove window' );
3072 }
3073 cleanupWindow = cleanup.bind( null, name, win );
3074 promises.push( this.closeWindow( name ).then( cleanupWindow, cleanupWindow ) );
3075 }
3076
3077 return $.when.apply( $, promises );
3078 };
3079
3080 /**
3081 * Remove all windows.
3082 *
3083 * Windows will be closed before they are removed.
3084 *
3085 * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
3086 */
3087 OO.ui.WindowManager.prototype.clearWindows = function () {
3088 return this.removeWindows( Object.keys( this.windows ) );
3089 };
3090
3091 /**
3092 * Set dialog size.
3093 *
3094 * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
3095 *
3096 * @chainable
3097 */
3098 OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) {
3099 // Bypass for non-current, and thus invisible, windows
3100 if ( win !== this.currentWindow ) {
3101 return;
3102 }
3103
3104 var viewport = OO.ui.Element.static.getDimensions( win.getElementWindow() ),
3105 sizes = this.constructor.static.sizes,
3106 size = win.getSize();
3107
3108 if ( !sizes[ size ] ) {
3109 size = this.constructor.static.defaultSize;
3110 }
3111 if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[ size ].width ) {
3112 size = 'full';
3113 }
3114
3115 this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', size === 'full' );
3116 this.$element.toggleClass( 'oo-ui-windowManager-floating', size !== 'full' );
3117 win.setDimensions( sizes[ size ] );
3118
3119 this.emit( 'resize', win );
3120
3121 return this;
3122 };
3123
3124 /**
3125 * Bind or unbind global events for scrolling.
3126 *
3127 * @param {boolean} [on] Bind global events
3128 * @chainable
3129 */
3130 OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) {
3131 on = on === undefined ? !!this.globalEvents : !!on;
3132
3133 var $body = $( this.getElementDocument().body ),
3134 // We could have multiple window managers open to only modify
3135 // the body class at the bottom of the stack
3136 stackDepth = $body.data( 'windowManagerGlobalEvents' ) || 0 ;
3137
3138 if ( on ) {
3139 if ( !this.globalEvents ) {
3140 $( this.getElementWindow() ).on( {
3141 // Start listening for top-level window dimension changes
3142 'orientationchange resize': this.onWindowResizeHandler
3143 } );
3144 if ( stackDepth === 0 ) {
3145 $body.css( 'overflow', 'hidden' );
3146 }
3147 stackDepth++;
3148 this.globalEvents = true;
3149 }
3150 } else if ( this.globalEvents ) {
3151 $( this.getElementWindow() ).off( {
3152 // Stop listening for top-level window dimension changes
3153 'orientationchange resize': this.onWindowResizeHandler
3154 } );
3155 stackDepth--;
3156 if ( stackDepth === 0 ) {
3157 $( this.getElementDocument().body ).css( 'overflow', '' );
3158 }
3159 this.globalEvents = false;
3160 }
3161 $body.data( 'windowManagerGlobalEvents', stackDepth );
3162
3163 return this;
3164 };
3165
3166 /**
3167 * Toggle screen reader visibility of content other than the window manager.
3168 *
3169 * @param {boolean} [isolate] Make only the window manager visible to screen readers
3170 * @chainable
3171 */
3172 OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) {
3173 isolate = isolate === undefined ? !this.$ariaHidden : !!isolate;
3174
3175 if ( isolate ) {
3176 if ( !this.$ariaHidden ) {
3177 // Hide everything other than the window manager from screen readers
3178 this.$ariaHidden = $( 'body' )
3179 .children()
3180 .not( this.$element.parentsUntil( 'body' ).last() )
3181 .attr( 'aria-hidden', '' );
3182 }
3183 } else if ( this.$ariaHidden ) {
3184 // Restore screen reader visibility
3185 this.$ariaHidden.removeAttr( 'aria-hidden' );
3186 this.$ariaHidden = null;
3187 }
3188
3189 return this;
3190 };
3191
3192 /**
3193 * Destroy window manager.
3194 */
3195 OO.ui.WindowManager.prototype.destroy = function () {
3196 this.toggleGlobalEvents( false );
3197 this.toggleAriaIsolation( false );
3198 this.clearWindows();
3199 this.$element.remove();
3200 };
3201
3202 /**
3203 * @class
3204 *
3205 * @constructor
3206 * @param {string|jQuery} message Description of error
3207 * @param {Object} [config] Configuration options
3208 * @cfg {boolean} [recoverable=true] Error is recoverable
3209 * @cfg {boolean} [warning=false] Whether this error is a warning or not.
3210 */
3211 OO.ui.Error = function OoUiError( message, config ) {
3212 // Allow passing positional parameters inside the config object
3213 if ( OO.isPlainObject( message ) && config === undefined ) {
3214 config = message;
3215 message = config.message;
3216 }
3217
3218 // Configuration initialization
3219 config = config || {};
3220
3221 // Properties
3222 this.message = message instanceof jQuery ? message : String( message );
3223 this.recoverable = config.recoverable === undefined || !!config.recoverable;
3224 this.warning = !!config.warning;
3225 };
3226
3227 /* Setup */
3228
3229 OO.initClass( OO.ui.Error );
3230
3231 /* Methods */
3232
3233 /**
3234 * Check if error can be recovered from.
3235 *
3236 * @return {boolean} Error is recoverable
3237 */
3238 OO.ui.Error.prototype.isRecoverable = function () {
3239 return this.recoverable;
3240 };
3241
3242 /**
3243 * Check if the error is a warning
3244 *
3245 * @return {boolean} Error is warning
3246 */
3247 OO.ui.Error.prototype.isWarning = function () {
3248 return this.warning;
3249 };
3250
3251 /**
3252 * Get error message as DOM nodes.
3253 *
3254 * @return {jQuery} Error message in DOM nodes
3255 */
3256 OO.ui.Error.prototype.getMessage = function () {
3257 return this.message instanceof jQuery ?
3258 this.message.clone() :
3259 $( '<div>' ).text( this.message ).contents();
3260 };
3261
3262 /**
3263 * Get error message as text.
3264 *
3265 * @return {string} Error message
3266 */
3267 OO.ui.Error.prototype.getMessageText = function () {
3268 return this.message instanceof jQuery ? this.message.text() : this.message;
3269 };
3270
3271 /**
3272 * Wraps an HTML snippet for use with configuration values which default
3273 * to strings. This bypasses the default html-escaping done to string
3274 * values.
3275 *
3276 * @class
3277 *
3278 * @constructor
3279 * @param {string} [content] HTML content
3280 */
3281 OO.ui.HtmlSnippet = function OoUiHtmlSnippet( content ) {
3282 // Properties
3283 this.content = content;
3284 };
3285
3286 /* Setup */
3287
3288 OO.initClass( OO.ui.HtmlSnippet );
3289
3290 /* Methods */
3291
3292 /**
3293 * Render into HTML.
3294 *
3295 * @return {string} Unchanged HTML snippet.
3296 */
3297 OO.ui.HtmlSnippet.prototype.toString = function () {
3298 return this.content;
3299 };
3300
3301 /**
3302 * A list of functions, called in sequence.
3303 *
3304 * If a function added to a process returns boolean false the process will stop; if it returns an
3305 * object with a `promise` method the process will use the promise to either continue to the next
3306 * step when the promise is resolved or stop when the promise is rejected.
3307 *
3308 * @class
3309 *
3310 * @constructor
3311 * @param {number|jQuery.Promise|Function} step Time to wait, promise to wait for or function to
3312 * call, see #createStep for more information
3313 * @param {Object} [context=null] Context to call the step function in, ignored if step is a number
3314 * or a promise
3315 * @return {Object} Step object, with `callback` and `context` properties
3316 */
3317 OO.ui.Process = function ( step, context ) {
3318 // Properties
3319 this.steps = [];
3320
3321 // Initialization
3322 if ( step !== undefined ) {
3323 this.next( step, context );
3324 }
3325 };
3326
3327 /* Setup */
3328
3329 OO.initClass( OO.ui.Process );
3330
3331 /* Methods */
3332
3333 /**
3334 * Start the process.
3335 *
3336 * @return {jQuery.Promise} Promise that is resolved when all steps have completed or rejected when
3337 * any of the steps return boolean false or a promise which gets rejected; upon stopping the
3338 * process, the remaining steps will not be taken
3339 */
3340 OO.ui.Process.prototype.execute = function () {
3341 var i, len, promise;
3342
3343 /**
3344 * Continue execution.
3345 *
3346 * @ignore
3347 * @param {Array} step A function and the context it should be called in
3348 * @return {Function} Function that continues the process
3349 */
3350 function proceed( step ) {
3351 return function () {
3352 // Execute step in the correct context
3353 var deferred,
3354 result = step.callback.call( step.context );
3355
3356 if ( result === false ) {
3357 // Use rejected promise for boolean false results
3358 return $.Deferred().reject( [] ).promise();
3359 }
3360 if ( typeof result === 'number' ) {
3361 if ( result < 0 ) {
3362 throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
3363 }
3364 // Use a delayed promise for numbers, expecting them to be in milliseconds
3365 deferred = $.Deferred();
3366 setTimeout( deferred.resolve, result );
3367 return deferred.promise();
3368 }
3369 if ( result instanceof OO.ui.Error ) {
3370 // Use rejected promise for error
3371 return $.Deferred().reject( [ result ] ).promise();
3372 }
3373 if ( Array.isArray( result ) && result.length && result[ 0 ] instanceof OO.ui.Error ) {
3374 // Use rejected promise for list of errors
3375 return $.Deferred().reject( result ).promise();
3376 }
3377 // Duck-type the object to see if it can produce a promise
3378 if ( result && $.isFunction( result.promise ) ) {
3379 // Use a promise generated from the result
3380 return result.promise();
3381 }
3382 // Use resolved promise for other results
3383 return $.Deferred().resolve().promise();
3384 };
3385 }
3386
3387 if ( this.steps.length ) {
3388 // Generate a chain reaction of promises
3389 promise = proceed( this.steps[ 0 ] )();
3390 for ( i = 1, len = this.steps.length; i < len; i++ ) {
3391 promise = promise.then( proceed( this.steps[ i ] ) );
3392 }
3393 } else {
3394 promise = $.Deferred().resolve().promise();
3395 }
3396
3397 return promise;
3398 };
3399
3400 /**
3401 * Create a process step.
3402 *
3403 * @private
3404 * @param {number|jQuery.Promise|Function} step
3405 *
3406 * - Number of milliseconds to wait; or
3407 * - Promise to wait to be resolved; or
3408 * - Function to execute
3409 * - If it returns boolean false the process will stop
3410 * - If it returns an object with a `promise` method the process will use the promise to either
3411 * continue to the next step when the promise is resolved or stop when the promise is rejected
3412 * - If it returns a number, the process will wait for that number of milliseconds before
3413 * proceeding
3414 * @param {Object} [context=null] Context to call the step function in, ignored if step is a number
3415 * or a promise
3416 * @return {Object} Step object, with `callback` and `context` properties
3417 */
3418 OO.ui.Process.prototype.createStep = function ( step, context ) {
3419 if ( typeof step === 'number' || $.isFunction( step.promise ) ) {
3420 return {
3421 callback: function () {
3422 return step;
3423 },
3424 context: null
3425 };
3426 }
3427 if ( $.isFunction( step ) ) {
3428 return {
3429 callback: step,
3430 context: context
3431 };
3432 }
3433 throw new Error( 'Cannot create process step: number, promise or function expected' );
3434 };
3435
3436 /**
3437 * Add step to the beginning of the process.
3438 *
3439 * @inheritdoc #createStep
3440 * @return {OO.ui.Process} this
3441 * @chainable
3442 */
3443 OO.ui.Process.prototype.first = function ( step, context ) {
3444 this.steps.unshift( this.createStep( step, context ) );
3445 return this;
3446 };
3447
3448 /**
3449 * Add step to the end of the process.
3450 *
3451 * @inheritdoc #createStep
3452 * @return {OO.ui.Process} this
3453 * @chainable
3454 */
3455 OO.ui.Process.prototype.next = function ( step, context ) {
3456 this.steps.push( this.createStep( step, context ) );
3457 return this;
3458 };
3459
3460 /**
3461 * Factory for tools.
3462 *
3463 * @class
3464 * @extends OO.Factory
3465 * @constructor
3466 */
3467 OO.ui.ToolFactory = function OoUiToolFactory() {
3468 // Parent constructor
3469 OO.ui.ToolFactory.super.call( this );
3470 };
3471
3472 /* Setup */
3473
3474 OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
3475
3476 /* Methods */
3477
3478 /**
3479 * Get tools from the factory
3480 *
3481 * @param {Array} include Included tools
3482 * @param {Array} exclude Excluded tools
3483 * @param {Array} promote Promoted tools
3484 * @param {Array} demote Demoted tools
3485 * @return {string[]} List of tools
3486 */
3487 OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
3488 var i, len, included, promoted, demoted,
3489 auto = [],
3490 used = {};
3491
3492 // Collect included and not excluded tools
3493 included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
3494
3495 // Promotion
3496 promoted = this.extract( promote, used );
3497 demoted = this.extract( demote, used );
3498
3499 // Auto
3500 for ( i = 0, len = included.length; i < len; i++ ) {
3501 if ( !used[ included[ i ] ] ) {
3502 auto.push( included[ i ] );
3503 }
3504 }
3505
3506 return promoted.concat( auto ).concat( demoted );
3507 };
3508
3509 /**
3510 * Get a flat list of names from a list of names or groups.
3511 *
3512 * Tools can be specified in the following ways:
3513 *
3514 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
3515 * - All tools in a group: `{ group: 'group-name' }`
3516 * - All tools: `'*'`
3517 *
3518 * @private
3519 * @param {Array|string} collection List of tools
3520 * @param {Object} [used] Object with names that should be skipped as properties; extracted
3521 * names will be added as properties
3522 * @return {string[]} List of extracted names
3523 */
3524 OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
3525 var i, len, item, name, tool,
3526 names = [];
3527
3528 if ( collection === '*' ) {
3529 for ( name in this.registry ) {
3530 tool = this.registry[ name ];
3531 if (
3532 // Only add tools by group name when auto-add is enabled
3533 tool.static.autoAddToCatchall &&
3534 // Exclude already used tools
3535 ( !used || !used[ name ] )
3536 ) {
3537 names.push( name );
3538 if ( used ) {
3539 used[ name ] = true;
3540 }
3541 }
3542 }
3543 } else if ( Array.isArray( collection ) ) {
3544 for ( i = 0, len = collection.length; i < len; i++ ) {
3545 item = collection[ i ];
3546 // Allow plain strings as shorthand for named tools
3547 if ( typeof item === 'string' ) {
3548 item = { name: item };
3549 }
3550 if ( OO.isPlainObject( item ) ) {
3551 if ( item.group ) {
3552 for ( name in this.registry ) {
3553 tool = this.registry[ name ];
3554 if (
3555 // Include tools with matching group
3556 tool.static.group === item.group &&
3557 // Only add tools by group name when auto-add is enabled
3558 tool.static.autoAddToGroup &&
3559 // Exclude already used tools
3560 ( !used || !used[ name ] )
3561 ) {
3562 names.push( name );
3563 if ( used ) {
3564 used[ name ] = true;
3565 }
3566 }
3567 }
3568 // Include tools with matching name and exclude already used tools
3569 } else if ( item.name && ( !used || !used[ item.name ] ) ) {
3570 names.push( item.name );
3571 if ( used ) {
3572 used[ item.name ] = true;
3573 }
3574 }
3575 }
3576 }
3577 }
3578 return names;
3579 };
3580
3581 /**
3582 * Factory for tool groups.
3583 *
3584 * @class
3585 * @extends OO.Factory
3586 * @constructor
3587 */
3588 OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() {
3589 // Parent constructor
3590 OO.Factory.call( this );
3591
3592 var i, l,
3593 defaultClasses = this.constructor.static.getDefaultClasses();
3594
3595 // Register default toolgroups
3596 for ( i = 0, l = defaultClasses.length; i < l; i++ ) {
3597 this.register( defaultClasses[ i ] );
3598 }
3599 };
3600
3601 /* Setup */
3602
3603 OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory );
3604
3605 /* Static Methods */
3606
3607 /**
3608 * Get a default set of classes to be registered on construction
3609 *
3610 * @return {Function[]} Default classes
3611 */
3612 OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
3613 return [
3614 OO.ui.BarToolGroup,
3615 OO.ui.ListToolGroup,
3616 OO.ui.MenuToolGroup
3617 ];
3618 };
3619
3620 /**
3621 * Theme logic.
3622 *
3623 * @abstract
3624 * @class
3625 *
3626 * @constructor
3627 * @param {Object} [config] Configuration options
3628 */
3629 OO.ui.Theme = function OoUiTheme( config ) {
3630 // Configuration initialization
3631 config = config || {};
3632 };
3633
3634 /* Setup */
3635
3636 OO.initClass( OO.ui.Theme );
3637
3638 /* Methods */
3639
3640 /**
3641 * Get a list of classes to be applied to a widget.
3642 *
3643 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
3644 * otherwise state transitions will not work properly.
3645 *
3646 * @param {OO.ui.Element} element Element for which to get classes
3647 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
3648 */
3649 OO.ui.Theme.prototype.getElementClasses = function ( /* element */ ) {
3650 return { on: [], off: [] };
3651 };
3652
3653 /**
3654 * Update CSS classes provided by the theme.
3655 *
3656 * For elements with theme logic hooks, this should be called any time there's a state change.
3657 *
3658 * @param {OO.ui.Element} element Element for which to update classes
3659 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
3660 */
3661 OO.ui.Theme.prototype.updateElementClasses = function ( element ) {
3662 var classes = this.getElementClasses( element );
3663
3664 element.$element
3665 .removeClass( classes.off.join( ' ' ) )
3666 .addClass( classes.on.join( ' ' ) );
3667 };
3668
3669 /**
3670 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
3671 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
3672 * order in which users will navigate through the focusable elements via the "tab" key.
3673 *
3674 * @example
3675 * // TabIndexedElement is mixed into the ButtonWidget class
3676 * // to provide a tabIndex property.
3677 * var button1 = new OO.ui.ButtonWidget( {
3678 * label : 'fourth',
3679 * tabIndex : 4
3680 * } );
3681 * var button2 = new OO.ui.ButtonWidget( {
3682 * label : 'second',
3683 * tabIndex : 2
3684 * } );
3685 * var button3 = new OO.ui.ButtonWidget( {
3686 * label : 'third',
3687 * tabIndex : 3
3688 * } );
3689 * var button4 = new OO.ui.ButtonWidget( {
3690 * label : 'first',
3691 * tabIndex : 1
3692 * } );
3693 * $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
3694 *
3695 * @abstract
3696 * @class
3697 *
3698 * @constructor
3699 * @param {Object} [config] Configuration options
3700 * @cfg {jQuery} [$tabIndexed] tabIndexed node, assigned to #$tabIndexed, omit to use #$element
3701 * @cfg {number|null} [tabIndex=0] Tab index value. Use 0 to use default ordering, use -1 to
3702 * prevent tab focusing, use null to suppress the `tabindex` attribute.
3703 */
3704 OO.ui.TabIndexedElement = function OoUiTabIndexedElement( config ) {
3705 // Configuration initialization
3706 config = $.extend( { tabIndex: 0 }, config );
3707
3708 // Properties
3709 this.$tabIndexed = null;
3710 this.tabIndex = null;
3711
3712 // Events
3713 this.connect( this, { disable: 'onDisable' } );
3714
3715 // Initialization
3716 this.setTabIndex( config.tabIndex );
3717 this.setTabIndexedElement( config.$tabIndexed || this.$element );
3718 };
3719
3720 /* Setup */
3721
3722 OO.initClass( OO.ui.TabIndexedElement );
3723
3724 /* Methods */
3725
3726 /**
3727 * Set the element with `tabindex` attribute.
3728 *
3729 * If an element is already set, it will be cleaned up before setting up the new element.
3730 *
3731 * @param {jQuery} $tabIndexed Element to set tab index on
3732 * @chainable
3733 */
3734 OO.ui.TabIndexedElement.prototype.setTabIndexedElement = function ( $tabIndexed ) {
3735 var tabIndex = this.tabIndex;
3736 // Remove attributes from old $tabIndexed
3737 this.setTabIndex( null );
3738 // Force update of new $tabIndexed
3739 this.$tabIndexed = $tabIndexed;
3740 this.tabIndex = tabIndex;
3741 return this.updateTabIndex();
3742 };
3743
3744 /**
3745 * Set tab index value.
3746 *
3747 * @param {number|null} tabIndex Tab index value or null for no tab index
3748 * @chainable
3749 */
3750 OO.ui.TabIndexedElement.prototype.setTabIndex = function ( tabIndex ) {
3751 tabIndex = typeof tabIndex === 'number' ? tabIndex : null;
3752
3753 if ( this.tabIndex !== tabIndex ) {
3754 this.tabIndex = tabIndex;
3755 this.updateTabIndex();
3756 }
3757
3758 return this;
3759 };
3760
3761 /**
3762 * Update the `tabindex` attribute, in case of changes to tab index or
3763 * disabled state.
3764 *
3765 * @chainable
3766 */
3767 OO.ui.TabIndexedElement.prototype.updateTabIndex = function () {
3768 if ( this.$tabIndexed ) {
3769 if ( this.tabIndex !== null ) {
3770 // Do not index over disabled elements
3771 this.$tabIndexed.attr( {
3772 tabindex: this.isDisabled() ? -1 : this.tabIndex,
3773 // ChromeVox and NVDA do not seem to inherit this from parent elements
3774 'aria-disabled': this.isDisabled().toString()
3775 } );
3776 } else {
3777 this.$tabIndexed.removeAttr( 'tabindex aria-disabled' );
3778 }
3779 }
3780 return this;
3781 };
3782
3783 /**
3784 * Handle disable events.
3785 *
3786 * @private
3787 * @param {boolean} disabled Element is disabled
3788 */
3789 OO.ui.TabIndexedElement.prototype.onDisable = function () {
3790 this.updateTabIndex();
3791 };
3792
3793 /**
3794 * Get tab index value.
3795 *
3796 * @return {number|null} Tab index value
3797 */
3798 OO.ui.TabIndexedElement.prototype.getTabIndex = function () {
3799 return this.tabIndex;
3800 };
3801
3802 /**
3803 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
3804 * interface element that can be configured with access keys for accessibility.
3805 * See the [OOjs UI documentation on MediaWiki] [1] for examples.
3806 *
3807 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
3808 * @abstract
3809 * @class
3810 *
3811 * @constructor
3812 * @param {Object} [config] Configuration options
3813 * @cfg {jQuery} [$button] Button node, assigned to #$button, omit to use a generated `<a>`
3814 * @cfg {boolean} [framed=true] Render button with a frame
3815 * @cfg {string} [accessKey] Button's access key
3816 */
3817 OO.ui.ButtonElement = function OoUiButtonElement( config ) {
3818 // Configuration initialization
3819 config = config || {};
3820
3821 // Properties
3822 this.$button = config.$button || $( '<a>' );
3823 this.framed = null;
3824 this.accessKey = null;
3825 this.active = false;
3826 this.onMouseUpHandler = this.onMouseUp.bind( this );
3827 this.onMouseDownHandler = this.onMouseDown.bind( this );
3828 this.onKeyDownHandler = this.onKeyDown.bind( this );
3829 this.onKeyUpHandler = this.onKeyUp.bind( this );
3830 this.onClickHandler = this.onClick.bind( this );
3831 this.onKeyPressHandler = this.onKeyPress.bind( this );
3832
3833 // Initialization
3834 this.$element.addClass( 'oo-ui-buttonElement' );
3835 this.toggleFramed( config.framed === undefined || config.framed );
3836 this.setAccessKey( config.accessKey );
3837 this.setButtonElement( this.$button );
3838 };
3839
3840 /* Setup */
3841
3842 OO.initClass( OO.ui.ButtonElement );
3843
3844 /* Static Properties */
3845
3846 /**
3847 * Cancel mouse down events.
3848 *
3849 * @static
3850 * @inheritable
3851 * @property {boolean}
3852 */
3853 OO.ui.ButtonElement.static.cancelButtonMouseDownEvents = true;
3854
3855 /* Events */
3856
3857 /**
3858 * @event click
3859 */
3860
3861 /* Methods */
3862
3863 /**
3864 * Set the button element.
3865 *
3866 * If an element is already set, it will be cleaned up before setting up the new element.
3867 *
3868 * @param {jQuery} $button Element to use as button
3869 */
3870 OO.ui.ButtonElement.prototype.setButtonElement = function ( $button ) {
3871 if ( this.$button ) {
3872 this.$button
3873 .removeClass( 'oo-ui-buttonElement-button' )
3874 .removeAttr( 'role accesskey' )
3875 .off( {
3876 mousedown: this.onMouseDownHandler,
3877 keydown: this.onKeyDownHandler,
3878 click: this.onClickHandler,
3879 keypress: this.onKeyPressHandler
3880 } );
3881 }
3882
3883 this.$button = $button
3884 .addClass( 'oo-ui-buttonElement-button' )
3885 .attr( { role: 'button', accesskey: this.accessKey } )
3886 .on( {
3887 mousedown: this.onMouseDownHandler,
3888 keydown: this.onKeyDownHandler,
3889 click: this.onClickHandler,
3890 keypress: this.onKeyPressHandler
3891 } );
3892 };
3893
3894 /**
3895 * Handles mouse down events.
3896 *
3897 * @protected
3898 * @param {jQuery.Event} e Mouse down event
3899 */
3900 OO.ui.ButtonElement.prototype.onMouseDown = function ( e ) {
3901 if ( this.isDisabled() || e.which !== 1 ) {
3902 return;
3903 }
3904 this.$element.addClass( 'oo-ui-buttonElement-pressed' );
3905 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
3906 // reliably remove the pressed class
3907 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
3908 // Prevent change of focus unless specifically configured otherwise
3909 if ( this.constructor.static.cancelButtonMouseDownEvents ) {
3910 return false;
3911 }
3912 };
3913
3914 /**
3915 * Handles mouse up events.
3916 *
3917 * @protected
3918 * @param {jQuery.Event} e Mouse up event
3919 */
3920 OO.ui.ButtonElement.prototype.onMouseUp = function ( e ) {
3921 if ( this.isDisabled() || e.which !== 1 ) {
3922 return;
3923 }
3924 this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
3925 // Stop listening for mouseup, since we only needed this once
3926 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
3927 };
3928
3929 /**
3930 * Handles mouse click events.
3931 *
3932 * @protected
3933 * @param {jQuery.Event} e Mouse click event
3934 * @fires click
3935 */
3936 OO.ui.ButtonElement.prototype.onClick = function ( e ) {
3937 if ( !this.isDisabled() && e.which === 1 ) {
3938 this.emit( 'click' );
3939 }
3940 return false;
3941 };
3942
3943 /**
3944 * Handles key down events.
3945 *
3946 * @protected
3947 * @param {jQuery.Event} e Key down event
3948 */
3949 OO.ui.ButtonElement.prototype.onKeyDown = function ( e ) {
3950 if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
3951 return;
3952 }
3953 this.$element.addClass( 'oo-ui-buttonElement-pressed' );
3954 // Run the keyup handler no matter where the key is when the button is let go, so we can
3955 // reliably remove the pressed class
3956 this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler, true );
3957 };
3958
3959 /**
3960 * Handles key up events.
3961 *
3962 * @protected
3963 * @param {jQuery.Event} e Key up event
3964 */
3965 OO.ui.ButtonElement.prototype.onKeyUp = function ( e ) {
3966 if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
3967 return;
3968 }
3969 this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
3970 // Stop listening for keyup, since we only needed this once
3971 this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler, true );
3972 };
3973
3974 /**
3975 * Handles key press events.
3976 *
3977 * @protected
3978 * @param {jQuery.Event} e Key press event
3979 * @fires click
3980 */
3981 OO.ui.ButtonElement.prototype.onKeyPress = function ( e ) {
3982 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
3983 this.emit( 'click' );
3984 }
3985 return false;
3986 };
3987
3988 /**
3989 * Check if button has a frame.
3990 *
3991 * @return {boolean} Button is framed
3992 */
3993 OO.ui.ButtonElement.prototype.isFramed = function () {
3994 return this.framed;
3995 };
3996
3997 /**
3998 * Toggle frame.
3999 *
4000 * @param {boolean} [framed] Make button framed, omit to toggle
4001 * @chainable
4002 */
4003 OO.ui.ButtonElement.prototype.toggleFramed = function ( framed ) {
4004 framed = framed === undefined ? !this.framed : !!framed;
4005 if ( framed !== this.framed ) {
4006 this.framed = framed;
4007 this.$element
4008 .toggleClass( 'oo-ui-buttonElement-frameless', !framed )
4009 .toggleClass( 'oo-ui-buttonElement-framed', framed );
4010 this.updateThemeClasses();
4011 }
4012
4013 return this;
4014 };
4015
4016 /**
4017 * Set access key.
4018 *
4019 * @param {string} accessKey Button's access key, use empty string to remove
4020 * @chainable
4021 */
4022 OO.ui.ButtonElement.prototype.setAccessKey = function ( accessKey ) {
4023 accessKey = typeof accessKey === 'string' && accessKey.length ? accessKey : null;
4024
4025 if ( this.accessKey !== accessKey ) {
4026 if ( this.$button ) {
4027 if ( accessKey !== null ) {
4028 this.$button.attr( 'accesskey', accessKey );
4029 } else {
4030 this.$button.removeAttr( 'accesskey' );
4031 }
4032 }
4033 this.accessKey = accessKey;
4034 }
4035
4036 return this;
4037 };
4038
4039 /**
4040 * Set active state.
4041 *
4042 * @param {boolean} [value] Make button active
4043 * @chainable
4044 */
4045 OO.ui.ButtonElement.prototype.setActive = function ( value ) {
4046 this.$element.toggleClass( 'oo-ui-buttonElement-active', !!value );
4047 return this;
4048 };
4049
4050 /**
4051 * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
4052 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
4053 * items from the group is done through the interface the class provides.
4054 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
4055 *
4056 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
4057 *
4058 * @abstract
4059 * @class
4060 *
4061 * @constructor
4062 * @param {Object} [config] Configuration options
4063 * @cfg {jQuery} [$group] Container node, assigned to #$group, omit to use a generated `<div>`
4064 */
4065 OO.ui.GroupElement = function OoUiGroupElement( config ) {
4066 // Configuration initialization
4067 config = config || {};
4068
4069 // Properties
4070 this.$group = null;
4071 this.items = [];
4072 this.aggregateItemEvents = {};
4073
4074 // Initialization
4075 this.setGroupElement( config.$group || $( '<div>' ) );
4076 };
4077
4078 /* Methods */
4079
4080 /**
4081 * Set the group element.
4082 *
4083 * If an element is already set, items will be moved to the new element.
4084 *
4085 * @param {jQuery} $group Element to use as group
4086 */
4087 OO.ui.GroupElement.prototype.setGroupElement = function ( $group ) {
4088 var i, len;
4089
4090 this.$group = $group;
4091 for ( i = 0, len = this.items.length; i < len; i++ ) {
4092 this.$group.append( this.items[ i ].$element );
4093 }
4094 };
4095
4096 /**
4097 * Check if there are no items.
4098 *
4099 * @return {boolean} Group is empty
4100 */
4101 OO.ui.GroupElement.prototype.isEmpty = function () {
4102 return !this.items.length;
4103 };
4104
4105 /**
4106 * Get items.
4107 *
4108 * @return {OO.ui.Element[]} Items
4109 */
4110 OO.ui.GroupElement.prototype.getItems = function () {
4111 return this.items.slice( 0 );
4112 };
4113
4114 /**
4115 * Get an item by its data.
4116 *
4117 * Data is compared by a hash of its value. Only the first item with matching data will be returned.
4118 *
4119 * @param {Object} data Item data to search for
4120 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
4121 */
4122 OO.ui.GroupElement.prototype.getItemFromData = function ( data ) {
4123 var i, len, item,
4124 hash = OO.getHash( data );
4125
4126 for ( i = 0, len = this.items.length; i < len; i++ ) {
4127 item = this.items[ i ];
4128 if ( hash === OO.getHash( item.getData() ) ) {
4129 return item;
4130 }
4131 }
4132
4133 return null;
4134 };
4135
4136 /**
4137 * Get items by their data.
4138 *
4139 * Data is compared by a hash of its value. All items with matching data will be returned.
4140 *
4141 * @param {Object} data Item data to search for
4142 * @return {OO.ui.Element[]} Items with equivalent data
4143 */
4144 OO.ui.GroupElement.prototype.getItemsFromData = function ( data ) {
4145 var i, len, item,
4146 hash = OO.getHash( data ),
4147 items = [];
4148
4149 for ( i = 0, len = this.items.length; i < len; i++ ) {
4150 item = this.items[ i ];
4151 if ( hash === OO.getHash( item.getData() ) ) {
4152 items.push( item );
4153 }
4154 }
4155
4156 return items;
4157 };
4158
4159 /**
4160 * Add an aggregate item event.
4161 *
4162 * Aggregated events are listened to on each item and then emitted by the group under a new name,
4163 * and with an additional leading parameter containing the item that emitted the original event.
4164 * Other arguments that were emitted from the original event are passed through.
4165 *
4166 * @param {Object.<string,string|null>} events Aggregate events emitted by group, keyed by item
4167 * event, use null value to remove aggregation
4168 * @throws {Error} If aggregation already exists
4169 */
4170 OO.ui.GroupElement.prototype.aggregate = function ( events ) {
4171 var i, len, item, add, remove, itemEvent, groupEvent;
4172
4173 for ( itemEvent in events ) {
4174 groupEvent = events[ itemEvent ];
4175
4176 // Remove existing aggregated event
4177 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4178 // Don't allow duplicate aggregations
4179 if ( groupEvent ) {
4180 throw new Error( 'Duplicate item event aggregation for ' + itemEvent );
4181 }
4182 // Remove event aggregation from existing items
4183 for ( i = 0, len = this.items.length; i < len; i++ ) {
4184 item = this.items[ i ];
4185 if ( item.connect && item.disconnect ) {
4186 remove = {};
4187 remove[ itemEvent ] = [ 'emit', groupEvent, item ];
4188 item.disconnect( this, remove );
4189 }
4190 }
4191 // Prevent future items from aggregating event
4192 delete this.aggregateItemEvents[ itemEvent ];
4193 }
4194
4195 // Add new aggregate event
4196 if ( groupEvent ) {
4197 // Make future items aggregate event
4198 this.aggregateItemEvents[ itemEvent ] = groupEvent;
4199 // Add event aggregation to existing items
4200 for ( i = 0, len = this.items.length; i < len; i++ ) {
4201 item = this.items[ i ];
4202 if ( item.connect && item.disconnect ) {
4203 add = {};
4204 add[ itemEvent ] = [ 'emit', groupEvent, item ];
4205 item.connect( this, add );
4206 }
4207 }
4208 }
4209 }
4210 };
4211
4212 /**
4213 * Add items.
4214 *
4215 * Adding an existing item will move it.
4216 *
4217 * @param {OO.ui.Element[]} items Items
4218 * @param {number} [index] Index to insert items at
4219 * @chainable
4220 */
4221 OO.ui.GroupElement.prototype.addItems = function ( items, index ) {
4222 var i, len, item, event, events, currentIndex,
4223 itemElements = [];
4224
4225 for ( i = 0, len = items.length; i < len; i++ ) {
4226 item = items[ i ];
4227
4228 // Check if item exists then remove it first, effectively "moving" it
4229 currentIndex = $.inArray( item, this.items );
4230 if ( currentIndex >= 0 ) {
4231 this.removeItems( [ item ] );
4232 // Adjust index to compensate for removal
4233 if ( currentIndex < index ) {
4234 index--;
4235 }
4236 }
4237 // Add the item
4238 if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
4239 events = {};
4240 for ( event in this.aggregateItemEvents ) {
4241 events[ event ] = [ 'emit', this.aggregateItemEvents[ event ], item ];
4242 }
4243 item.connect( this, events );
4244 }
4245 item.setElementGroup( this );
4246 itemElements.push( item.$element.get( 0 ) );
4247 }
4248
4249 if ( index === undefined || index < 0 || index >= this.items.length ) {
4250 this.$group.append( itemElements );
4251 this.items.push.apply( this.items, items );
4252 } else if ( index === 0 ) {
4253 this.$group.prepend( itemElements );
4254 this.items.unshift.apply( this.items, items );
4255 } else {
4256 this.items[ index ].$element.before( itemElements );
4257 this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
4258 }
4259
4260 return this;
4261 };
4262
4263 /**
4264 * Remove items.
4265 *
4266 * Items will be detached, not removed, so they can be used later.
4267 *
4268 * @param {OO.ui.Element[]} items Items to remove
4269 * @chainable
4270 */
4271 OO.ui.GroupElement.prototype.removeItems = function ( items ) {
4272 var i, len, item, index, remove, itemEvent;
4273
4274 // Remove specific items
4275 for ( i = 0, len = items.length; i < len; i++ ) {
4276 item = items[ i ];
4277 index = $.inArray( item, this.items );
4278 if ( index !== -1 ) {
4279 if (
4280 item.connect && item.disconnect &&
4281 !$.isEmptyObject( this.aggregateItemEvents )
4282 ) {
4283 remove = {};
4284 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4285 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
4286 }
4287 item.disconnect( this, remove );
4288 }
4289 item.setElementGroup( null );
4290 this.items.splice( index, 1 );
4291 item.$element.detach();
4292 }
4293 }
4294
4295 return this;
4296 };
4297
4298 /**
4299 * Clear all items.
4300 *
4301 * Items will be detached, not removed, so they can be used later.
4302 *
4303 * @chainable
4304 */
4305 OO.ui.GroupElement.prototype.clearItems = function () {
4306 var i, len, item, remove, itemEvent;
4307
4308 // Remove all items
4309 for ( i = 0, len = this.items.length; i < len; i++ ) {
4310 item = this.items[ i ];
4311 if (
4312 item.connect && item.disconnect &&
4313 !$.isEmptyObject( this.aggregateItemEvents )
4314 ) {
4315 remove = {};
4316 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4317 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
4318 }
4319 item.disconnect( this, remove );
4320 }
4321 item.setElementGroup( null );
4322 item.$element.detach();
4323 }
4324
4325 this.items = [];
4326 return this;
4327 };
4328
4329 /**
4330 * DraggableElement is a mixin class used to create elements that can be clicked
4331 * and dragged by a mouse to a new position within a group. This class must be used
4332 * in conjunction with OO.ui.DraggableGroupElement, which provides a container for
4333 * the draggable elements.
4334 *
4335 * @abstract
4336 * @class
4337 *
4338 * @constructor
4339 */
4340 OO.ui.DraggableElement = function OoUiDraggableElement() {
4341 // Properties
4342 this.index = null;
4343
4344 // Initialize and events
4345 this.$element
4346 .attr( 'draggable', true )
4347 .addClass( 'oo-ui-draggableElement' )
4348 .on( {
4349 dragstart: this.onDragStart.bind( this ),
4350 dragover: this.onDragOver.bind( this ),
4351 dragend: this.onDragEnd.bind( this ),
4352 drop: this.onDrop.bind( this )
4353 } );
4354 };
4355
4356 OO.initClass( OO.ui.DraggableElement );
4357
4358 /* Events */
4359
4360 /**
4361 * @event dragstart
4362 *
4363 * A dragstart event is emitted when the user clicks and begins dragging an item.
4364 * @param {OO.ui.DraggableElement} item The item the user has clicked and is dragging with the mouse.
4365 */
4366
4367 /**
4368 * @event dragend
4369 * A dragend event is emitted when the user drags an item and releases the mouse,
4370 * thus terminating the drag operation.
4371 */
4372
4373 /**
4374 * @event drop
4375 * A drop event is emitted when the user drags an item and then releases the mouse button
4376 * over a valid target.
4377 */
4378
4379 /* Static Properties */
4380
4381 /**
4382 * @inheritdoc OO.ui.ButtonElement
4383 */
4384 OO.ui.DraggableElement.static.cancelButtonMouseDownEvents = false;
4385
4386 /* Methods */
4387
4388 /**
4389 * Respond to dragstart event.
4390 *
4391 * @private
4392 * @param {jQuery.Event} event jQuery event
4393 * @fires dragstart
4394 */
4395 OO.ui.DraggableElement.prototype.onDragStart = function ( e ) {
4396 var dataTransfer = e.originalEvent.dataTransfer;
4397 // Define drop effect
4398 dataTransfer.dropEffect = 'none';
4399 dataTransfer.effectAllowed = 'move';
4400 // We must set up a dataTransfer data property or Firefox seems to
4401 // ignore the fact the element is draggable.
4402 try {
4403 dataTransfer.setData( 'application-x/OOjs-UI-draggable', this.getIndex() );
4404 } catch ( err ) {
4405 // The above is only for firefox. No need to set a catch clause
4406 // if it fails, move on.
4407 }
4408 // Add dragging class
4409 this.$element.addClass( 'oo-ui-draggableElement-dragging' );
4410 // Emit event
4411 this.emit( 'dragstart', this );
4412 return true;
4413 };
4414
4415 /**
4416 * Respond to dragend event.
4417 *
4418 * @private
4419 * @fires dragend
4420 */
4421 OO.ui.DraggableElement.prototype.onDragEnd = function () {
4422 this.$element.removeClass( 'oo-ui-draggableElement-dragging' );
4423 this.emit( 'dragend' );
4424 };
4425
4426 /**
4427 * Handle drop event.
4428 *
4429 * @private
4430 * @param {jQuery.Event} event jQuery event
4431 * @fires drop
4432 */
4433 OO.ui.DraggableElement.prototype.onDrop = function ( e ) {
4434 e.preventDefault();
4435 this.emit( 'drop', e );
4436 };
4437
4438 /**
4439 * In order for drag/drop to work, the dragover event must
4440 * return false and stop propogation.
4441 *
4442 * @private
4443 */
4444 OO.ui.DraggableElement.prototype.onDragOver = function ( e ) {
4445 e.preventDefault();
4446 };
4447
4448 /**
4449 * Set item index.
4450 * Store it in the DOM so we can access from the widget drag event
4451 *
4452 * @private
4453 * @param {number} Item index
4454 */
4455 OO.ui.DraggableElement.prototype.setIndex = function ( index ) {
4456 if ( this.index !== index ) {
4457 this.index = index;
4458 this.$element.data( 'index', index );
4459 }
4460 };
4461
4462 /**
4463 * Get item index
4464 *
4465 * @private
4466 * @return {number} Item index
4467 */
4468 OO.ui.DraggableElement.prototype.getIndex = function () {
4469 return this.index;
4470 };
4471
4472 /**
4473 * DraggableGroupElement is a mixin class used to create a group element to
4474 * contain draggable elements, which are items that can be clicked and dragged by a mouse.
4475 * The class is used with OO.ui.DraggableElement.
4476 *
4477 * @abstract
4478 * @class
4479 *
4480 * @constructor
4481 * @param {Object} [config] Configuration options
4482 * @cfg {jQuery} [$group] Container node, assigned to #$group, omit to use a generated `<div>`
4483 * @cfg {string} [orientation] Item orientation, 'horizontal' or 'vertical'. Defaults to 'vertical'
4484 */
4485 OO.ui.DraggableGroupElement = function OoUiDraggableGroupElement( config ) {
4486 // Configuration initialization
4487 config = config || {};
4488
4489 // Parent constructor
4490 OO.ui.GroupElement.call( this, config );
4491
4492 // Properties
4493 this.orientation = config.orientation || 'vertical';
4494 this.dragItem = null;
4495 this.itemDragOver = null;
4496 this.itemKeys = {};
4497 this.sideInsertion = '';
4498
4499 // Events
4500 this.aggregate( {
4501 dragstart: 'itemDragStart',
4502 dragend: 'itemDragEnd',
4503 drop: 'itemDrop'
4504 } );
4505 this.connect( this, {
4506 itemDragStart: 'onItemDragStart',
4507 itemDrop: 'onItemDrop',
4508 itemDragEnd: 'onItemDragEnd'
4509 } );
4510 this.$element.on( {
4511 dragover: $.proxy( this.onDragOver, this ),
4512 dragleave: $.proxy( this.onDragLeave, this )
4513 } );
4514
4515 // Initialize
4516 if ( Array.isArray( config.items ) ) {
4517 this.addItems( config.items );
4518 }
4519 this.$placeholder = $( '<div>' )
4520 .addClass( 'oo-ui-draggableGroupElement-placeholder' );
4521 this.$element
4522 .addClass( 'oo-ui-draggableGroupElement' )
4523 .append( this.$status )
4524 .toggleClass( 'oo-ui-draggableGroupElement-horizontal', this.orientation === 'horizontal' )
4525 .prepend( this.$placeholder );
4526 };
4527
4528 /* Setup */
4529 OO.mixinClass( OO.ui.DraggableGroupElement, OO.ui.GroupElement );
4530
4531 /* Events */
4532
4533 /**
4534 * @event reorder
4535 * @param {OO.ui.DraggableElement} item Reordered item
4536 * @param {number} [newIndex] New index for the item
4537 */
4538
4539 /* Methods */
4540
4541 /**
4542 * Respond to item drag start event
4543 * @param {OO.ui.DraggableElement} item Dragged item
4544 */
4545 OO.ui.DraggableGroupElement.prototype.onItemDragStart = function ( item ) {
4546 var i, len;
4547
4548 // Map the index of each object
4549 for ( i = 0, len = this.items.length; i < len; i++ ) {
4550 this.items[ i ].setIndex( i );
4551 }
4552
4553 if ( this.orientation === 'horizontal' ) {
4554 // Set the height of the indicator
4555 this.$placeholder.css( {
4556 height: item.$element.outerHeight(),
4557 width: 2
4558 } );
4559 } else {
4560 // Set the width of the indicator
4561 this.$placeholder.css( {
4562 height: 2,
4563 width: item.$element.outerWidth()
4564 } );
4565 }
4566 this.setDragItem( item );
4567 };
4568
4569 /**
4570 * Respond to item drag end event
4571 */
4572 OO.ui.DraggableGroupElement.prototype.onItemDragEnd = function () {
4573 this.unsetDragItem();
4574 return false;
4575 };
4576
4577 /**
4578 * Handle drop event and switch the order of the items accordingly
4579 * @param {OO.ui.DraggableElement} item Dropped item
4580 * @fires reorder
4581 */
4582 OO.ui.DraggableGroupElement.prototype.onItemDrop = function ( item ) {
4583 var toIndex = item.getIndex();
4584 // Check if the dropped item is from the current group
4585 // TODO: Figure out a way to configure a list of legally droppable
4586 // elements even if they are not yet in the list
4587 if ( this.getDragItem() ) {
4588 // If the insertion point is 'after', the insertion index
4589 // is shifted to the right (or to the left in RTL, hence 'after')
4590 if ( this.sideInsertion === 'after' ) {
4591 toIndex++;
4592 }
4593 // Emit change event
4594 this.emit( 'reorder', this.getDragItem(), toIndex );
4595 }
4596 this.unsetDragItem();
4597 // Return false to prevent propogation
4598 return false;
4599 };
4600
4601 /**
4602 * Handle dragleave event.
4603 */
4604 OO.ui.DraggableGroupElement.prototype.onDragLeave = function () {
4605 // This means the item was dragged outside the widget
4606 this.$placeholder
4607 .css( 'left', 0 )
4608 .addClass( 'oo-ui-element-hidden' );
4609 };
4610
4611 /**
4612 * Respond to dragover event
4613 * @param {jQuery.Event} event Event details
4614 */
4615 OO.ui.DraggableGroupElement.prototype.onDragOver = function ( e ) {
4616 var dragOverObj, $optionWidget, itemOffset, itemMidpoint, itemBoundingRect,
4617 itemSize, cssOutput, dragPosition, itemIndex, itemPosition,
4618 clientX = e.originalEvent.clientX,
4619 clientY = e.originalEvent.clientY;
4620
4621 // Get the OptionWidget item we are dragging over
4622 dragOverObj = this.getElementDocument().elementFromPoint( clientX, clientY );
4623 $optionWidget = $( dragOverObj ).closest( '.oo-ui-draggableElement' );
4624 if ( $optionWidget[ 0 ] ) {
4625 itemOffset = $optionWidget.offset();
4626 itemBoundingRect = $optionWidget[ 0 ].getBoundingClientRect();
4627 itemPosition = $optionWidget.position();
4628 itemIndex = $optionWidget.data( 'index' );
4629 }
4630
4631 if (
4632 itemOffset &&
4633 this.isDragging() &&
4634 itemIndex !== this.getDragItem().getIndex()
4635 ) {
4636 if ( this.orientation === 'horizontal' ) {
4637 // Calculate where the mouse is relative to the item width
4638 itemSize = itemBoundingRect.width;
4639 itemMidpoint = itemBoundingRect.left + itemSize / 2;
4640 dragPosition = clientX;
4641 // Which side of the item we hover over will dictate
4642 // where the placeholder will appear, on the left or
4643 // on the right
4644 cssOutput = {
4645 left: dragPosition < itemMidpoint ? itemPosition.left : itemPosition.left + itemSize,
4646 top: itemPosition.top
4647 };
4648 } else {
4649 // Calculate where the mouse is relative to the item height
4650 itemSize = itemBoundingRect.height;
4651 itemMidpoint = itemBoundingRect.top + itemSize / 2;
4652 dragPosition = clientY;
4653 // Which side of the item we hover over will dictate
4654 // where the placeholder will appear, on the top or
4655 // on the bottom
4656 cssOutput = {
4657 top: dragPosition < itemMidpoint ? itemPosition.top : itemPosition.top + itemSize,
4658 left: itemPosition.left
4659 };
4660 }
4661 // Store whether we are before or after an item to rearrange
4662 // For horizontal layout, we need to account for RTL, as this is flipped
4663 if ( this.orientation === 'horizontal' && this.$element.css( 'direction' ) === 'rtl' ) {
4664 this.sideInsertion = dragPosition < itemMidpoint ? 'after' : 'before';
4665 } else {
4666 this.sideInsertion = dragPosition < itemMidpoint ? 'before' : 'after';
4667 }
4668 // Add drop indicator between objects
4669 this.$placeholder
4670 .css( cssOutput )
4671 .removeClass( 'oo-ui-element-hidden' );
4672 } else {
4673 // This means the item was dragged outside the widget
4674 this.$placeholder
4675 .css( 'left', 0 )
4676 .addClass( 'oo-ui-element-hidden' );
4677 }
4678 // Prevent default
4679 e.preventDefault();
4680 };
4681
4682 /**
4683 * Set a dragged item
4684 * @param {OO.ui.DraggableElement} item Dragged item
4685 */
4686 OO.ui.DraggableGroupElement.prototype.setDragItem = function ( item ) {
4687 this.dragItem = item;
4688 };
4689
4690 /**
4691 * Unset the current dragged item
4692 */
4693 OO.ui.DraggableGroupElement.prototype.unsetDragItem = function () {
4694 this.dragItem = null;
4695 this.itemDragOver = null;
4696 this.$placeholder.addClass( 'oo-ui-element-hidden' );
4697 this.sideInsertion = '';
4698 };
4699
4700 /**
4701 * Get the current dragged item
4702 * @return {OO.ui.DraggableElement|null} item Dragged item or null if no item is dragged
4703 */
4704 OO.ui.DraggableGroupElement.prototype.getDragItem = function () {
4705 return this.dragItem;
4706 };
4707
4708 /**
4709 * Check if there's an item being dragged.
4710 * @return {Boolean} Item is being dragged
4711 */
4712 OO.ui.DraggableGroupElement.prototype.isDragging = function () {
4713 return this.getDragItem() !== null;
4714 };
4715
4716 /**
4717 * IconElement is often mixed into other classes to generate an icon.
4718 * Icons are graphics, about the size of normal text. They are used to aid the user
4719 * in locating a control or to convey information in a space-efficient way. See the
4720 * [OOjs UI documentation on MediaWiki] [1] for a list of icons
4721 * included in the library.
4722 *
4723 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
4724 *
4725 * @abstract
4726 * @class
4727 *
4728 * @constructor
4729 * @param {Object} [config] Configuration options
4730 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
4731 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
4732 * the icon element be set to an existing icon instead of the one generated by this class, set a
4733 * value using a jQuery selection. For example:
4734 *
4735 * // Use a <div> tag instead of a <span>
4736 * $icon: $("<div>")
4737 * // Use an existing icon element instead of the one generated by the class
4738 * $icon: this.$element
4739 * // Use an icon element from a child widget
4740 * $icon: this.childwidget.$element
4741 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
4742 * symbolic names. A map is used for i18n purposes and contains a `default` icon
4743 * name and additional names keyed by language code. The `default` name is used when no icon is keyed
4744 * by the user's language.
4745 *
4746 * Example of an i18n map:
4747 *
4748 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
4749 * See the [OOjs UI documentation on MediaWiki] [2] for a list of icons included in the library.
4750 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
4751 * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
4752 * text. The icon title is displayed when users move the mouse over the icon.
4753 */
4754 OO.ui.IconElement = function OoUiIconElement( config ) {
4755 // Configuration initialization
4756 config = config || {};
4757
4758 // Properties
4759 this.$icon = null;
4760 this.icon = null;
4761 this.iconTitle = null;
4762
4763 // Initialization
4764 this.setIcon( config.icon || this.constructor.static.icon );
4765 this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle );
4766 this.setIconElement( config.$icon || $( '<span>' ) );
4767 };
4768
4769 /* Setup */
4770
4771 OO.initClass( OO.ui.IconElement );
4772
4773 /* Static Properties */
4774
4775 /**
4776 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
4777 * for i18n purposes and contains a `default` icon name and additional names keyed by
4778 * language code. The `default` name is used when no icon is keyed by the user's language.
4779 *
4780 * Example of an i18n map:
4781 *
4782 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
4783 *
4784 * Note: the static property will be overridden if the #icon configuration is used.
4785 *
4786 * @static
4787 * @inheritable
4788 * @property {Object|string}
4789 */
4790 OO.ui.IconElement.static.icon = null;
4791
4792 /**
4793 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
4794 * function that returns title text, or `null` for no title.
4795 *
4796 * The static property will be overridden if the #iconTitle configuration is used.
4797 *
4798 * @static
4799 * @inheritable
4800 * @property {string|Function|null}
4801 */
4802 OO.ui.IconElement.static.iconTitle = null;
4803
4804 /* Methods */
4805
4806 /**
4807 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
4808 * applies to the specified icon element instead of the one created by the class. If an icon
4809 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
4810 * and mixin methods will no longer affect the element.
4811 *
4812 * @param {jQuery} $icon Element to use as icon
4813 */
4814 OO.ui.IconElement.prototype.setIconElement = function ( $icon ) {
4815 if ( this.$icon ) {
4816 this.$icon
4817 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon )
4818 .removeAttr( 'title' );
4819 }
4820
4821 this.$icon = $icon
4822 .addClass( 'oo-ui-iconElement-icon' )
4823 .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon );
4824 if ( this.iconTitle !== null ) {
4825 this.$icon.attr( 'title', this.iconTitle );
4826 }
4827 };
4828
4829 /**
4830 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
4831 * The icon parameter can also be set to a map of icon names. See the #icon config setting
4832 * for an example.
4833 *
4834 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
4835 * by language code, or `null` to remove the icon.
4836 * @chainable
4837 */
4838 OO.ui.IconElement.prototype.setIcon = function ( icon ) {
4839 icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
4840 icon = typeof icon === 'string' && icon.trim().length ? icon.trim() : null;
4841
4842 if ( this.icon !== icon ) {
4843 if ( this.$icon ) {
4844 if ( this.icon !== null ) {
4845 this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
4846 }
4847 if ( icon !== null ) {
4848 this.$icon.addClass( 'oo-ui-icon-' + icon );
4849 }
4850 }
4851 this.icon = icon;
4852 }
4853
4854 this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon );
4855 this.updateThemeClasses();
4856
4857 return this;
4858 };
4859
4860 /**
4861 * Set the icon title. Use `null` to remove the title.
4862 *
4863 * @param {string|Function|null} iconTitle A text string used as the icon title,
4864 * a function that returns title text, or `null` for no title.
4865 * @chainable
4866 */
4867 OO.ui.IconElement.prototype.setIconTitle = function ( iconTitle ) {
4868 iconTitle = typeof iconTitle === 'function' ||
4869 ( typeof iconTitle === 'string' && iconTitle.length ) ?
4870 OO.ui.resolveMsg( iconTitle ) : null;
4871
4872 if ( this.iconTitle !== iconTitle ) {
4873 this.iconTitle = iconTitle;
4874 if ( this.$icon ) {
4875 if ( this.iconTitle !== null ) {
4876 this.$icon.attr( 'title', iconTitle );
4877 } else {
4878 this.$icon.removeAttr( 'title' );
4879 }
4880 }
4881 }
4882
4883 return this;
4884 };
4885
4886 /**
4887 * Get the symbolic name of the icon.
4888 *
4889 * @return {string} Icon name
4890 */
4891 OO.ui.IconElement.prototype.getIcon = function () {
4892 return this.icon;
4893 };
4894
4895 /**
4896 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
4897 *
4898 * @return {string} Icon title text
4899 */
4900 OO.ui.IconElement.prototype.getIconTitle = function () {
4901 return this.iconTitle;
4902 };
4903
4904 /**
4905 * IndicatorElement is often mixed into other classes to generate an indicator.
4906 * Indicators are small graphics that are generally used in two ways:
4907 *
4908 * - To draw attention to the status of an item. For example, an indicator might be
4909 * used to show that an item in a list has errors that need to be resolved.
4910 * - To clarify the function of a control that acts in an exceptional way (a button
4911 * that opens a menu instead of performing an action directly, for example).
4912 *
4913 * For a list of indicators included in the library, please see the
4914 * [OOjs UI documentation on MediaWiki] [1].
4915 *
4916 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
4917 *
4918 * @abstract
4919 * @class
4920 *
4921 * @constructor
4922 * @param {Object} [config] Configuration options
4923 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
4924 * configuration is omitted, the indicator element will use a generated `<span>`.
4925 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
4926 * See the [OOjs UI documentation on MediaWiki][2] for a list of indicators included
4927 * in the library.
4928 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
4929 * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
4930 * or a function that returns title text. The indicator title is displayed when users move
4931 * the mouse over the indicator.
4932 */
4933 OO.ui.IndicatorElement = function OoUiIndicatorElement( config ) {
4934 // Configuration initialization
4935 config = config || {};
4936
4937 // Properties
4938 this.$indicator = null;
4939 this.indicator = null;
4940 this.indicatorTitle = null;
4941
4942 // Initialization
4943 this.setIndicator( config.indicator || this.constructor.static.indicator );
4944 this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle );
4945 this.setIndicatorElement( config.$indicator || $( '<span>' ) );
4946 };
4947
4948 /* Setup */
4949
4950 OO.initClass( OO.ui.IndicatorElement );
4951
4952 /* Static Properties */
4953
4954 /**
4955 * Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
4956 * The static property will be overridden if the #indicator configuration is used.
4957 *
4958 * @static
4959 * @inheritable
4960 * @property {string|null}
4961 */
4962 OO.ui.IndicatorElement.static.indicator = null;
4963
4964 /**
4965 * A text string used as the indicator title, a function that returns title text, or `null`
4966 * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
4967 *
4968 * @static
4969 * @inheritable
4970 * @property {string|Function|null}
4971 */
4972 OO.ui.IndicatorElement.static.indicatorTitle = null;
4973
4974 /* Methods */
4975
4976 /**
4977 * Set the indicator element.
4978 *
4979 * If an element is already set, it will be cleaned up before setting up the new element.
4980 *
4981 * @param {jQuery} $indicator Element to use as indicator
4982 */
4983 OO.ui.IndicatorElement.prototype.setIndicatorElement = function ( $indicator ) {
4984 if ( this.$indicator ) {
4985 this.$indicator
4986 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator )
4987 .removeAttr( 'title' );
4988 }
4989
4990 this.$indicator = $indicator
4991 .addClass( 'oo-ui-indicatorElement-indicator' )
4992 .toggleClass( 'oo-ui-indicator-' + this.indicator, !!this.indicator );
4993 if ( this.indicatorTitle !== null ) {
4994 this.$indicator.attr( 'title', this.indicatorTitle );
4995 }
4996 };
4997
4998 /**
4999 * Set indicator name.
5000 *
5001 * @param {string|null} indicator Symbolic name of indicator to use or null for no indicator
5002 * @chainable
5003 */
5004 OO.ui.IndicatorElement.prototype.setIndicator = function ( indicator ) {
5005 indicator = typeof indicator === 'string' && indicator.length ? indicator.trim() : null;
5006
5007 if ( this.indicator !== indicator ) {
5008 if ( this.$indicator ) {
5009 if ( this.indicator !== null ) {
5010 this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
5011 }
5012 if ( indicator !== null ) {
5013 this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
5014 }
5015 }
5016 this.indicator = indicator;
5017 }
5018
5019 this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator );
5020 this.updateThemeClasses();
5021
5022 return this;
5023 };
5024
5025 /**
5026 * Set indicator title.
5027 *
5028 * @param {string|Function|null} indicator Indicator title text, a function that returns text or
5029 * null for no indicator title
5030 * @chainable
5031 */
5032 OO.ui.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
5033 indicatorTitle = typeof indicatorTitle === 'function' ||
5034 ( typeof indicatorTitle === 'string' && indicatorTitle.length ) ?
5035 OO.ui.resolveMsg( indicatorTitle ) : null;
5036
5037 if ( this.indicatorTitle !== indicatorTitle ) {
5038 this.indicatorTitle = indicatorTitle;
5039 if ( this.$indicator ) {
5040 if ( this.indicatorTitle !== null ) {
5041 this.$indicator.attr( 'title', indicatorTitle );
5042 } else {
5043 this.$indicator.removeAttr( 'title' );
5044 }
5045 }
5046 }
5047
5048 return this;
5049 };
5050
5051 /**
5052 * Get indicator name.
5053 *
5054 * @return {string} Symbolic name of indicator
5055 */
5056 OO.ui.IndicatorElement.prototype.getIndicator = function () {
5057 return this.indicator;
5058 };
5059
5060 /**
5061 * Get indicator title.
5062 *
5063 * @return {string} Indicator title text
5064 */
5065 OO.ui.IndicatorElement.prototype.getIndicatorTitle = function () {
5066 return this.indicatorTitle;
5067 };
5068
5069 /**
5070 * LabelElement is often mixed into other classes to generate a label, which
5071 * helps identify the function of an interface element.
5072 * See the [OOjs UI documentation on MediaWiki] [1] for more information.
5073 *
5074 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
5075 *
5076 * @abstract
5077 * @class
5078 *
5079 * @constructor
5080 * @param {Object} [config] Configuration options
5081 * @cfg {jQuery} [$label] The label element created by the class. If this
5082 * configuration is omitted, the label element will use a generated `<span>`.
5083 * @cfg {jQuery|string|Function} [label] The label text. The label can be specified as a plaintext string,
5084 * a jQuery selection of elements, or a function that will produce a string in the future. See the
5085 * [OOjs UI documentation on MediaWiki] [2] for examples.
5086 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
5087 * @cfg {boolean} [autoFitLabel=true] Fit the label to the width of the parent element.
5088 * The label will be truncated to fit if necessary.
5089 */
5090 OO.ui.LabelElement = function OoUiLabelElement( config ) {
5091 // Configuration initialization
5092 config = config || {};
5093
5094 // Properties
5095 this.$label = null;
5096 this.label = null;
5097 this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel;
5098
5099 // Initialization
5100 this.setLabel( config.label || this.constructor.static.label );
5101 this.setLabelElement( config.$label || $( '<span>' ) );
5102 };
5103
5104 /* Setup */
5105
5106 OO.initClass( OO.ui.LabelElement );
5107
5108 /* Events */
5109
5110 /**
5111 * @event labelChange
5112 * @param {string} value
5113 */
5114
5115 /* Static Properties */
5116
5117 /**
5118 * The label text. The label can be specified as a plaintext string, a function that will
5119 * produce a string in the future, or `null` for no label. The static value will
5120 * be overridden if a label is specified with the #label config option.
5121 *
5122 * @static
5123 * @inheritable
5124 * @property {string|Function|null}
5125 */
5126 OO.ui.LabelElement.static.label = null;
5127
5128 /* Methods */
5129
5130 /**
5131 * Set the label element.
5132 *
5133 * If an element is already set, it will be cleaned up before setting up the new element.
5134 *
5135 * @param {jQuery} $label Element to use as label
5136 */
5137 OO.ui.LabelElement.prototype.setLabelElement = function ( $label ) {
5138 if ( this.$label ) {
5139 this.$label.removeClass( 'oo-ui-labelElement-label' ).empty();
5140 }
5141
5142 this.$label = $label.addClass( 'oo-ui-labelElement-label' );
5143 this.setLabelContent( this.label );
5144 };
5145
5146 /**
5147 * Set the label.
5148 *
5149 * An empty string will result in the label being hidden. A string containing only whitespace will
5150 * be converted to a single `&nbsp;`.
5151 *
5152 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
5153 * text; or null for no label
5154 * @chainable
5155 */
5156 OO.ui.LabelElement.prototype.setLabel = function ( label ) {
5157 label = typeof label === 'function' ? OO.ui.resolveMsg( label ) : label;
5158 label = ( ( typeof label === 'string' && label.length ) || label instanceof jQuery || label instanceof OO.ui.HtmlSnippet ) ? label : null;
5159
5160 this.$element.toggleClass( 'oo-ui-labelElement', !!label );
5161
5162 if ( this.label !== label ) {
5163 if ( this.$label ) {
5164 this.setLabelContent( label );
5165 }
5166 this.label = label;
5167 this.emit( 'labelChange' );
5168 }
5169
5170 return this;
5171 };
5172
5173 /**
5174 * Get the label.
5175 *
5176 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
5177 * text; or null for no label
5178 */
5179 OO.ui.LabelElement.prototype.getLabel = function () {
5180 return this.label;
5181 };
5182
5183 /**
5184 * Fit the label.
5185 *
5186 * @chainable
5187 */
5188 OO.ui.LabelElement.prototype.fitLabel = function () {
5189 if ( this.$label && this.$label.autoEllipsis && this.autoFitLabel ) {
5190 this.$label.autoEllipsis( { hasSpan: false, tooltip: true } );
5191 }
5192
5193 return this;
5194 };
5195
5196 /**
5197 * Set the content of the label.
5198 *
5199 * Do not call this method until after the label element has been set by #setLabelElement.
5200 *
5201 * @private
5202 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
5203 * text; or null for no label
5204 */
5205 OO.ui.LabelElement.prototype.setLabelContent = function ( label ) {
5206 if ( typeof label === 'string' ) {
5207 if ( label.match( /^\s*$/ ) ) {
5208 // Convert whitespace only string to a single non-breaking space
5209 this.$label.html( '&nbsp;' );
5210 } else {
5211 this.$label.text( label );
5212 }
5213 } else if ( label instanceof OO.ui.HtmlSnippet ) {
5214 this.$label.html( label.toString() );
5215 } else if ( label instanceof jQuery ) {
5216 this.$label.empty().append( label );
5217 } else {
5218 this.$label.empty();
5219 }
5220 };
5221
5222 /**
5223 * Mixin that adds a menu showing suggested values for a OO.ui.TextInputWidget.
5224 *
5225 * Subclasses that set the value of #lookupInput from #onLookupMenuItemChoose should
5226 * be aware that this will cause new suggestions to be looked up for the new value. If this is
5227 * not desired, disable lookups with #setLookupsDisabled, then set the value, then re-enable lookups.
5228 *
5229 * @class
5230 * @abstract
5231 *
5232 * @constructor
5233 * @param {Object} [config] Configuration options
5234 * @cfg {jQuery} [$overlay] Overlay for dropdown; defaults to relative positioning
5235 * @cfg {jQuery} [$container=this.$element] Element to render menu under
5236 */
5237 OO.ui.LookupElement = function OoUiLookupElement( config ) {
5238 // Configuration initialization
5239 config = config || {};
5240
5241 // Properties
5242 this.$overlay = config.$overlay || this.$element;
5243 this.lookupMenu = new OO.ui.TextInputMenuSelectWidget( this, {
5244 widget: this,
5245 input: this,
5246 $container: config.$container
5247 } );
5248 this.lookupCache = {};
5249 this.lookupQuery = null;
5250 this.lookupRequest = null;
5251 this.lookupsDisabled = false;
5252 this.lookupInputFocused = false;
5253
5254 // Events
5255 this.$input.on( {
5256 focus: this.onLookupInputFocus.bind( this ),
5257 blur: this.onLookupInputBlur.bind( this ),
5258 mousedown: this.onLookupInputMouseDown.bind( this )
5259 } );
5260 this.connect( this, { change: 'onLookupInputChange' } );
5261 this.lookupMenu.connect( this, {
5262 toggle: 'onLookupMenuToggle',
5263 choose: 'onLookupMenuItemChoose'
5264 } );
5265
5266 // Initialization
5267 this.$element.addClass( 'oo-ui-lookupElement' );
5268 this.lookupMenu.$element.addClass( 'oo-ui-lookupElement-menu' );
5269 this.$overlay.append( this.lookupMenu.$element );
5270 };
5271
5272 /* Methods */
5273
5274 /**
5275 * Handle input focus event.
5276 *
5277 * @param {jQuery.Event} e Input focus event
5278 */
5279 OO.ui.LookupElement.prototype.onLookupInputFocus = function () {
5280 this.lookupInputFocused = true;
5281 this.populateLookupMenu();
5282 };
5283
5284 /**
5285 * Handle input blur event.
5286 *
5287 * @param {jQuery.Event} e Input blur event
5288 */
5289 OO.ui.LookupElement.prototype.onLookupInputBlur = function () {
5290 this.closeLookupMenu();
5291 this.lookupInputFocused = false;
5292 };
5293
5294 /**
5295 * Handle input mouse down event.
5296 *
5297 * @param {jQuery.Event} e Input mouse down event
5298 */
5299 OO.ui.LookupElement.prototype.onLookupInputMouseDown = function () {
5300 // Only open the menu if the input was already focused.
5301 // This way we allow the user to open the menu again after closing it with Esc
5302 // by clicking in the input. Opening (and populating) the menu when initially
5303 // clicking into the input is handled by the focus handler.
5304 if ( this.lookupInputFocused && !this.lookupMenu.isVisible() ) {
5305 this.populateLookupMenu();
5306 }
5307 };
5308
5309 /**
5310 * Handle input change event.
5311 *
5312 * @param {string} value New input value
5313 */
5314 OO.ui.LookupElement.prototype.onLookupInputChange = function () {
5315 if ( this.lookupInputFocused ) {
5316 this.populateLookupMenu();
5317 }
5318 };
5319
5320 /**
5321 * Handle the lookup menu being shown/hidden.
5322 *
5323 * @param {boolean} visible Whether the lookup menu is now visible.
5324 */
5325 OO.ui.LookupElement.prototype.onLookupMenuToggle = function ( visible ) {
5326 if ( !visible ) {
5327 // When the menu is hidden, abort any active request and clear the menu.
5328 // This has to be done here in addition to closeLookupMenu(), because
5329 // MenuSelectWidget will close itself when the user presses Esc.
5330 this.abortLookupRequest();
5331 this.lookupMenu.clearItems();
5332 }
5333 };
5334
5335 /**
5336 * Handle menu item 'choose' event, updating the text input value to the value of the clicked item.
5337 *
5338 * @param {OO.ui.MenuOptionWidget|null} item Selected item
5339 */
5340 OO.ui.LookupElement.prototype.onLookupMenuItemChoose = function ( item ) {
5341 if ( item ) {
5342 this.setValue( item.getData() );
5343 }
5344 };
5345
5346 /**
5347 * Get lookup menu.
5348 *
5349 * @return {OO.ui.TextInputMenuSelectWidget}
5350 */
5351 OO.ui.LookupElement.prototype.getLookupMenu = function () {
5352 return this.lookupMenu;
5353 };
5354
5355 /**
5356 * Disable or re-enable lookups.
5357 *
5358 * When lookups are disabled, calls to #populateLookupMenu will be ignored.
5359 *
5360 * @param {boolean} disabled Disable lookups
5361 */
5362 OO.ui.LookupElement.prototype.setLookupsDisabled = function ( disabled ) {
5363 this.lookupsDisabled = !!disabled;
5364 };
5365
5366 /**
5367 * Open the menu. If there are no entries in the menu, this does nothing.
5368 *
5369 * @chainable
5370 */
5371 OO.ui.LookupElement.prototype.openLookupMenu = function () {
5372 if ( !this.lookupMenu.isEmpty() ) {
5373 this.lookupMenu.toggle( true );
5374 }
5375 return this;
5376 };
5377
5378 /**
5379 * Close the menu, empty it, and abort any pending request.
5380 *
5381 * @chainable
5382 */
5383 OO.ui.LookupElement.prototype.closeLookupMenu = function () {
5384 this.lookupMenu.toggle( false );
5385 this.abortLookupRequest();
5386 this.lookupMenu.clearItems();
5387 return this;
5388 };
5389
5390 /**
5391 * Request menu items based on the input's current value, and when they arrive,
5392 * populate the menu with these items and show the menu.
5393 *
5394 * If lookups have been disabled with #setLookupsDisabled, this function does nothing.
5395 *
5396 * @chainable
5397 */
5398 OO.ui.LookupElement.prototype.populateLookupMenu = function () {
5399 var widget = this,
5400 value = this.getValue();
5401
5402 if ( this.lookupsDisabled ) {
5403 return;
5404 }
5405
5406 // If the input is empty, clear the menu
5407 if ( value === '' ) {
5408 this.closeLookupMenu();
5409 // Skip population if there is already a request pending for the current value
5410 } else if ( value !== this.lookupQuery ) {
5411 this.getLookupMenuItems()
5412 .done( function ( items ) {
5413 widget.lookupMenu.clearItems();
5414 if ( items.length ) {
5415 widget.lookupMenu
5416 .addItems( items )
5417 .toggle( true );
5418 widget.initializeLookupMenuSelection();
5419 } else {
5420 widget.lookupMenu.toggle( false );
5421 }
5422 } )
5423 .fail( function () {
5424 widget.lookupMenu.clearItems();
5425 } );
5426 }
5427
5428 return this;
5429 };
5430
5431 /**
5432 * Select and highlight the first selectable item in the menu.
5433 *
5434 * @chainable
5435 */
5436 OO.ui.LookupElement.prototype.initializeLookupMenuSelection = function () {
5437 if ( !this.lookupMenu.getSelectedItem() ) {
5438 this.lookupMenu.selectItem( this.lookupMenu.getFirstSelectableItem() );
5439 }
5440 this.lookupMenu.highlightItem( this.lookupMenu.getSelectedItem() );
5441 };
5442
5443 /**
5444 * Get lookup menu items for the current query.
5445 *
5446 * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument of
5447 * the done event. If the request was aborted to make way for a subsequent request, this promise
5448 * will not be rejected: it will remain pending forever.
5449 */
5450 OO.ui.LookupElement.prototype.getLookupMenuItems = function () {
5451 var widget = this,
5452 value = this.getValue(),
5453 deferred = $.Deferred(),
5454 ourRequest;
5455
5456 this.abortLookupRequest();
5457 if ( Object.prototype.hasOwnProperty.call( this.lookupCache, value ) ) {
5458 deferred.resolve( this.getLookupMenuOptionsFromData( this.lookupCache[ value ] ) );
5459 } else {
5460 this.pushPending();
5461 this.lookupQuery = value;
5462 ourRequest = this.lookupRequest = this.getLookupRequest();
5463 ourRequest
5464 .always( function () {
5465 // We need to pop pending even if this is an old request, otherwise
5466 // the widget will remain pending forever.
5467 // TODO: this assumes that an aborted request will fail or succeed soon after
5468 // being aborted, or at least eventually. It would be nice if we could popPending()
5469 // at abort time, but only if we knew that we hadn't already called popPending()
5470 // for that request.
5471 widget.popPending();
5472 } )
5473 .done( function ( data ) {
5474 // If this is an old request (and aborting it somehow caused it to still succeed),
5475 // ignore its success completely
5476 if ( ourRequest === widget.lookupRequest ) {
5477 widget.lookupQuery = null;
5478 widget.lookupRequest = null;
5479 widget.lookupCache[ value ] = widget.getLookupCacheDataFromResponse( data );
5480 deferred.resolve( widget.getLookupMenuOptionsFromData( widget.lookupCache[ value ] ) );
5481 }
5482 } )
5483 .fail( function () {
5484 // If this is an old request (or a request failing because it's being aborted),
5485 // ignore its failure completely
5486 if ( ourRequest === widget.lookupRequest ) {
5487 widget.lookupQuery = null;
5488 widget.lookupRequest = null;
5489 deferred.reject();
5490 }
5491 } );
5492 }
5493 return deferred.promise();
5494 };
5495
5496 /**
5497 * Abort the currently pending lookup request, if any.
5498 */
5499 OO.ui.LookupElement.prototype.abortLookupRequest = function () {
5500 var oldRequest = this.lookupRequest;
5501 if ( oldRequest ) {
5502 // First unset this.lookupRequest to the fail handler will notice
5503 // that the request is no longer current
5504 this.lookupRequest = null;
5505 this.lookupQuery = null;
5506 oldRequest.abort();
5507 }
5508 };
5509
5510 /**
5511 * Get a new request object of the current lookup query value.
5512 *
5513 * @abstract
5514 * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
5515 */
5516 OO.ui.LookupElement.prototype.getLookupRequest = function () {
5517 // Stub, implemented in subclass
5518 return null;
5519 };
5520
5521 /**
5522 * Pre-process data returned by the request from #getLookupRequest.
5523 *
5524 * The return value of this function will be cached, and any further queries for the given value
5525 * will use the cache rather than doing API requests.
5526 *
5527 * @abstract
5528 * @param {Mixed} data Response from server
5529 * @return {Mixed} Cached result data
5530 */
5531 OO.ui.LookupElement.prototype.getLookupCacheDataFromResponse = function () {
5532 // Stub, implemented in subclass
5533 return [];
5534 };
5535
5536 /**
5537 * Get a list of menu option widgets from the (possibly cached) data returned by
5538 * #getLookupCacheDataFromResponse.
5539 *
5540 * @abstract
5541 * @param {Mixed} data Cached result data, usually an array
5542 * @return {OO.ui.MenuOptionWidget[]} Menu items
5543 */
5544 OO.ui.LookupElement.prototype.getLookupMenuOptionsFromData = function () {
5545 // Stub, implemented in subclass
5546 return [];
5547 };
5548
5549 /**
5550 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
5551 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
5552 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
5553 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
5554 *
5555 * @abstract
5556 * @class
5557 *
5558 * @constructor
5559 * @param {Object} [config] Configuration options
5560 * @cfg {Object} [popup] Configuration to pass to popup
5561 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
5562 */
5563 OO.ui.PopupElement = function OoUiPopupElement( config ) {
5564 // Configuration initialization
5565 config = config || {};
5566
5567 // Properties
5568 this.popup = new OO.ui.PopupWidget( $.extend(
5569 { autoClose: true },
5570 config.popup,
5571 { $autoCloseIgnore: this.$element }
5572 ) );
5573 };
5574
5575 /* Methods */
5576
5577 /**
5578 * Get popup.
5579 *
5580 * @return {OO.ui.PopupWidget} Popup widget
5581 */
5582 OO.ui.PopupElement.prototype.getPopup = function () {
5583 return this.popup;
5584 };
5585
5586 /**
5587 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
5588 * additional functionality to an element created by another class. The class provides
5589 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
5590 * which are used to customize the look and feel of a widget to better describe its
5591 * importance and functionality.
5592 *
5593 * The library currently contains the following styling flags for general use:
5594 *
5595 * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
5596 * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
5597 * - **constructive**: Constructive styling is applied to convey that the widget will create something.
5598 *
5599 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
5600 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
5601 *
5602 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
5603 *
5604 * @abstract
5605 * @class
5606 *
5607 * @constructor
5608 * @param {Object} [config] Configuration options
5609 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'constructive' or 'primary') to apply.
5610 * Please see the [OOjs UI documentation on MediaWiki] [2] for more information about available flags.
5611 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
5612 * @cfg {jQuery} [$flagged] Flagged node, assigned to $flagged, omit to use $element
5613 */
5614 OO.ui.FlaggedElement = function OoUiFlaggedElement( config ) {
5615 // Configuration initialization
5616 config = config || {};
5617
5618 // Properties
5619 this.flags = {};
5620 this.$flagged = null;
5621
5622 // Initialization
5623 this.setFlags( config.flags );
5624 this.setFlaggedElement( config.$flagged || this.$element );
5625 };
5626
5627 /* Events */
5628
5629 /**
5630 * @event flag
5631 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
5632 * parameter contains the name of each modified flag and indicates whether it was
5633 * added or removed.
5634 *
5635 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
5636 * that the flag was added, `false` that the flag was removed.
5637 */
5638
5639 /* Methods */
5640
5641 /**
5642 * Set the flagged element.
5643 *
5644 * If an element is already set, it will be cleaned up before setting up the new element.
5645 *
5646 * @param {jQuery} $flagged Element to add flags to
5647 */
5648 OO.ui.FlaggedElement.prototype.setFlaggedElement = function ( $flagged ) {
5649 var classNames = Object.keys( this.flags ).map( function ( flag ) {
5650 return 'oo-ui-flaggedElement-' + flag;
5651 } ).join( ' ' );
5652
5653 if ( this.$flagged ) {
5654 this.$flagged.removeClass( classNames );
5655 }
5656
5657 this.$flagged = $flagged.addClass( classNames );
5658 };
5659
5660 /**
5661 * Check if a flag is set.
5662 *
5663 * @param {string} flag Name of flag
5664 * @return {boolean} Has flag
5665 */
5666 OO.ui.FlaggedElement.prototype.hasFlag = function ( flag ) {
5667 return flag in this.flags;
5668 };
5669
5670 /**
5671 * Get the names of all flags set.
5672 *
5673 * @return {string[]} Flag names
5674 */
5675 OO.ui.FlaggedElement.prototype.getFlags = function () {
5676 return Object.keys( this.flags );
5677 };
5678
5679 /**
5680 * Clear all flags.
5681 *
5682 * @chainable
5683 * @fires flag
5684 */
5685 OO.ui.FlaggedElement.prototype.clearFlags = function () {
5686 var flag, className,
5687 changes = {},
5688 remove = [],
5689 classPrefix = 'oo-ui-flaggedElement-';
5690
5691 for ( flag in this.flags ) {
5692 className = classPrefix + flag;
5693 changes[ flag ] = false;
5694 delete this.flags[ flag ];
5695 remove.push( className );
5696 }
5697
5698 if ( this.$flagged ) {
5699 this.$flagged.removeClass( remove.join( ' ' ) );
5700 }
5701
5702 this.updateThemeClasses();
5703 this.emit( 'flag', changes );
5704
5705 return this;
5706 };
5707
5708 /**
5709 * Add one or more flags.
5710 *
5711 * @param {string|string[]|Object.<string, boolean>} flags One or more flags to add, or an object
5712 * keyed by flag name containing boolean set/remove instructions.
5713 * @chainable
5714 * @fires flag
5715 */
5716 OO.ui.FlaggedElement.prototype.setFlags = function ( flags ) {
5717 var i, len, flag, className,
5718 changes = {},
5719 add = [],
5720 remove = [],
5721 classPrefix = 'oo-ui-flaggedElement-';
5722
5723 if ( typeof flags === 'string' ) {
5724 className = classPrefix + flags;
5725 // Set
5726 if ( !this.flags[ flags ] ) {
5727 this.flags[ flags ] = true;
5728 add.push( className );
5729 }
5730 } else if ( Array.isArray( flags ) ) {
5731 for ( i = 0, len = flags.length; i < len; i++ ) {
5732 flag = flags[ i ];
5733 className = classPrefix + flag;
5734 // Set
5735 if ( !this.flags[ flag ] ) {
5736 changes[ flag ] = true;
5737 this.flags[ flag ] = true;
5738 add.push( className );
5739 }
5740 }
5741 } else if ( OO.isPlainObject( flags ) ) {
5742 for ( flag in flags ) {
5743 className = classPrefix + flag;
5744 if ( flags[ flag ] ) {
5745 // Set
5746 if ( !this.flags[ flag ] ) {
5747 changes[ flag ] = true;
5748 this.flags[ flag ] = true;
5749 add.push( className );
5750 }
5751 } else {
5752 // Remove
5753 if ( this.flags[ flag ] ) {
5754 changes[ flag ] = false;
5755 delete this.flags[ flag ];
5756 remove.push( className );
5757 }
5758 }
5759 }
5760 }
5761
5762 if ( this.$flagged ) {
5763 this.$flagged
5764 .addClass( add.join( ' ' ) )
5765 .removeClass( remove.join( ' ' ) );
5766 }
5767
5768 this.updateThemeClasses();
5769 this.emit( 'flag', changes );
5770
5771 return this;
5772 };
5773
5774 /**
5775 * TitledElement is mixed into other classes to provide a `title` attribute.
5776 * Titles are rendered by the browser and are made visible when the user moves
5777 * the mouse over the element. Titles are not visible on touch devices.
5778 *
5779 * @example
5780 * // TitledElement provides a 'title' attribute to the
5781 * // ButtonWidget class
5782 * var button = new OO.ui.ButtonWidget( {
5783 * label : 'Button with Title',
5784 * title : 'I am a button'
5785 * } );
5786 * $( 'body' ).append( button.$element );
5787 *
5788 * @abstract
5789 * @class
5790 *
5791 * @constructor
5792 * @param {Object} [config] Configuration options
5793 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
5794 * If this config is omitted, the title functionality is applied to $element, the
5795 * element created by the class.
5796 * @cfg {string|Function} [title] The title text or a function that returns text. If
5797 * this config is omitted, the value of the static `title` property is used.
5798 */
5799 OO.ui.TitledElement = function OoUiTitledElement( config ) {
5800 // Configuration initialization
5801 config = config || {};
5802
5803 // Properties
5804 this.$titled = null;
5805 this.title = null;
5806
5807 // Initialization
5808 this.setTitle( config.title || this.constructor.static.title );
5809 this.setTitledElement( config.$titled || this.$element );
5810 };
5811
5812 /* Setup */
5813
5814 OO.initClass( OO.ui.TitledElement );
5815
5816 /* Static Properties */
5817
5818 /**
5819 * The title text, a function that returns text, or `null` for no title. The value of the static property
5820 * is overridden if the #title config option is used.
5821 *
5822 * @static
5823 * @inheritable
5824 * @property {string|Function|null}
5825 */
5826 OO.ui.TitledElement.static.title = null;
5827
5828 /* Methods */
5829
5830 /**
5831 * Set the titled element.
5832 *
5833 * If an element is already set, it will be cleaned up before setting up the new element.
5834 *
5835 * @param {jQuery} $titled Element to set title on
5836 */
5837 OO.ui.TitledElement.prototype.setTitledElement = function ( $titled ) {
5838 if ( this.$titled ) {
5839 this.$titled.removeAttr( 'title' );
5840 }
5841
5842 this.$titled = $titled;
5843 if ( this.title ) {
5844 this.$titled.attr( 'title', this.title );
5845 }
5846 };
5847
5848 /**
5849 * Set title.
5850 *
5851 * @param {string|Function|null} title Title text, a function that returns text or null for no title
5852 * @chainable
5853 */
5854 OO.ui.TitledElement.prototype.setTitle = function ( title ) {
5855 title = typeof title === 'string' ? OO.ui.resolveMsg( title ) : null;
5856
5857 if ( this.title !== title ) {
5858 if ( this.$titled ) {
5859 if ( title !== null ) {
5860 this.$titled.attr( 'title', title );
5861 } else {
5862 this.$titled.removeAttr( 'title' );
5863 }
5864 }
5865 this.title = title;
5866 }
5867
5868 return this;
5869 };
5870
5871 /**
5872 * Get title.
5873 *
5874 * @return {string} Title string
5875 */
5876 OO.ui.TitledElement.prototype.getTitle = function () {
5877 return this.title;
5878 };
5879
5880 /**
5881 * Element that can be automatically clipped to visible boundaries.
5882 *
5883 * Whenever the element's natural height changes, you have to call
5884 * #clip to make sure it's still clipping correctly.
5885 *
5886 * @abstract
5887 * @class
5888 *
5889 * @constructor
5890 * @param {Object} [config] Configuration options
5891 * @cfg {jQuery} [$clippable] Nodes to clip, assigned to #$clippable, omit to use #$element
5892 */
5893 OO.ui.ClippableElement = function OoUiClippableElement( config ) {
5894 // Configuration initialization
5895 config = config || {};
5896
5897 // Properties
5898 this.$clippable = null;
5899 this.clipping = false;
5900 this.clippedHorizontally = false;
5901 this.clippedVertically = false;
5902 this.$clippableContainer = null;
5903 this.$clippableScroller = null;
5904 this.$clippableWindow = null;
5905 this.idealWidth = null;
5906 this.idealHeight = null;
5907 this.onClippableContainerScrollHandler = this.clip.bind( this );
5908 this.onClippableWindowResizeHandler = this.clip.bind( this );
5909
5910 // Initialization
5911 this.setClippableElement( config.$clippable || this.$element );
5912 };
5913
5914 /* Methods */
5915
5916 /**
5917 * Set clippable element.
5918 *
5919 * If an element is already set, it will be cleaned up before setting up the new element.
5920 *
5921 * @param {jQuery} $clippable Element to make clippable
5922 */
5923 OO.ui.ClippableElement.prototype.setClippableElement = function ( $clippable ) {
5924 if ( this.$clippable ) {
5925 this.$clippable.removeClass( 'oo-ui-clippableElement-clippable' );
5926 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
5927 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
5928 }
5929
5930 this.$clippable = $clippable.addClass( 'oo-ui-clippableElement-clippable' );
5931 this.clip();
5932 };
5933
5934 /**
5935 * Toggle clipping.
5936 *
5937 * Do not turn clipping on until after the element is attached to the DOM and visible.
5938 *
5939 * @param {boolean} [clipping] Enable clipping, omit to toggle
5940 * @chainable
5941 */
5942 OO.ui.ClippableElement.prototype.toggleClipping = function ( clipping ) {
5943 clipping = clipping === undefined ? !this.clipping : !!clipping;
5944
5945 if ( this.clipping !== clipping ) {
5946 this.clipping = clipping;
5947 if ( clipping ) {
5948 this.$clippableContainer = $( this.getClosestScrollableElementContainer() );
5949 // If the clippable container is the root, we have to listen to scroll events and check
5950 // jQuery.scrollTop on the window because of browser inconsistencies
5951 this.$clippableScroller = this.$clippableContainer.is( 'html, body' ) ?
5952 $( OO.ui.Element.static.getWindow( this.$clippableContainer ) ) :
5953 this.$clippableContainer;
5954 this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler );
5955 this.$clippableWindow = $( this.getElementWindow() )
5956 .on( 'resize', this.onClippableWindowResizeHandler );
5957 // Initial clip after visible
5958 this.clip();
5959 } else {
5960 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
5961 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
5962
5963 this.$clippableContainer = null;
5964 this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler );
5965 this.$clippableScroller = null;
5966 this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
5967 this.$clippableWindow = null;
5968 }
5969 }
5970
5971 return this;
5972 };
5973
5974 /**
5975 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
5976 *
5977 * @return {boolean} Element will be clipped to the visible area
5978 */
5979 OO.ui.ClippableElement.prototype.isClipping = function () {
5980 return this.clipping;
5981 };
5982
5983 /**
5984 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
5985 *
5986 * @return {boolean} Part of the element is being clipped
5987 */
5988 OO.ui.ClippableElement.prototype.isClipped = function () {
5989 return this.clippedHorizontally || this.clippedVertically;
5990 };
5991
5992 /**
5993 * Check if the right of the element is being clipped by the nearest scrollable container.
5994 *
5995 * @return {boolean} Part of the element is being clipped
5996 */
5997 OO.ui.ClippableElement.prototype.isClippedHorizontally = function () {
5998 return this.clippedHorizontally;
5999 };
6000
6001 /**
6002 * Check if the bottom of the element is being clipped by the nearest scrollable container.
6003 *
6004 * @return {boolean} Part of the element is being clipped
6005 */
6006 OO.ui.ClippableElement.prototype.isClippedVertically = function () {
6007 return this.clippedVertically;
6008 };
6009
6010 /**
6011 * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
6012 *
6013 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
6014 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
6015 */
6016 OO.ui.ClippableElement.prototype.setIdealSize = function ( width, height ) {
6017 this.idealWidth = width;
6018 this.idealHeight = height;
6019
6020 if ( !this.clipping ) {
6021 // Update dimensions
6022 this.$clippable.css( { width: width, height: height } );
6023 }
6024 // While clipping, idealWidth and idealHeight are not considered
6025 };
6026
6027 /**
6028 * Clip element to visible boundaries and allow scrolling when needed. Call this method when
6029 * the element's natural height changes.
6030 *
6031 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
6032 * overlapped by, the visible area of the nearest scrollable container.
6033 *
6034 * @chainable
6035 */
6036 OO.ui.ClippableElement.prototype.clip = function () {
6037 if ( !this.clipping ) {
6038 // this.$clippableContainer and this.$clippableWindow are null, so the below will fail
6039 return this;
6040 }
6041
6042 var buffer = 7, // Chosen by fair dice roll
6043 cOffset = this.$clippable.offset(),
6044 $container = this.$clippableContainer.is( 'html, body' ) ?
6045 this.$clippableWindow : this.$clippableContainer,
6046 ccOffset = $container.offset() || { top: 0, left: 0 },
6047 ccHeight = $container.innerHeight() - buffer,
6048 ccWidth = $container.innerWidth() - buffer,
6049 cHeight = this.$clippable.outerHeight() + buffer,
6050 cWidth = this.$clippable.outerWidth() + buffer,
6051 scrollTop = this.$clippableScroller.scrollTop(),
6052 scrollLeft = this.$clippableScroller.scrollLeft(),
6053 desiredWidth = cOffset.left < 0 ?
6054 cWidth + cOffset.left :
6055 ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left,
6056 desiredHeight = cOffset.top < 0 ?
6057 cHeight + cOffset.top :
6058 ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top,
6059 naturalWidth = this.$clippable.prop( 'scrollWidth' ),
6060 naturalHeight = this.$clippable.prop( 'scrollHeight' ),
6061 clipWidth = desiredWidth < naturalWidth,
6062 clipHeight = desiredHeight < naturalHeight;
6063
6064 if ( clipWidth ) {
6065 this.$clippable.css( { overflowX: 'scroll', width: desiredWidth } );
6066 } else {
6067 this.$clippable.css( { width: this.idealWidth || '', overflowX: '' } );
6068 }
6069 if ( clipHeight ) {
6070 this.$clippable.css( { overflowY: 'scroll', height: desiredHeight } );
6071 } else {
6072 this.$clippable.css( { height: this.idealHeight || '', overflowY: '' } );
6073 }
6074
6075 // If we stopped clipping in at least one of the dimensions
6076 if ( !clipWidth || !clipHeight ) {
6077 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
6078 }
6079
6080 this.clippedHorizontally = clipWidth;
6081 this.clippedVertically = clipHeight;
6082
6083 return this;
6084 };
6085
6086 /**
6087 * Generic toolbar tool.
6088 *
6089 * @abstract
6090 * @class
6091 * @extends OO.ui.Widget
6092 * @mixins OO.ui.IconElement
6093 * @mixins OO.ui.FlaggedElement
6094 *
6095 * @constructor
6096 * @param {OO.ui.ToolGroup} toolGroup
6097 * @param {Object} [config] Configuration options
6098 * @cfg {string|Function} [title] Title text or a function that returns text
6099 */
6100 OO.ui.Tool = function OoUiTool( toolGroup, config ) {
6101 // Allow passing positional parameters inside the config object
6102 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
6103 config = toolGroup;
6104 toolGroup = config.toolGroup;
6105 }
6106
6107 // Configuration initialization
6108 config = config || {};
6109
6110 // Parent constructor
6111 OO.ui.Tool.super.call( this, config );
6112
6113 // Mixin constructors
6114 OO.ui.IconElement.call( this, config );
6115 OO.ui.FlaggedElement.call( this, config );
6116
6117 // Properties
6118 this.toolGroup = toolGroup;
6119 this.toolbar = this.toolGroup.getToolbar();
6120 this.active = false;
6121 this.$title = $( '<span>' );
6122 this.$accel = $( '<span>' );
6123 this.$link = $( '<a>' );
6124 this.title = null;
6125
6126 // Events
6127 this.toolbar.connect( this, { updateState: 'onUpdateState' } );
6128
6129 // Initialization
6130 this.$title.addClass( 'oo-ui-tool-title' );
6131 this.$accel
6132 .addClass( 'oo-ui-tool-accel' )
6133 .prop( {
6134 // This may need to be changed if the key names are ever localized,
6135 // but for now they are essentially written in English
6136 dir: 'ltr',
6137 lang: 'en'
6138 } );
6139 this.$link
6140 .addClass( 'oo-ui-tool-link' )
6141 .append( this.$icon, this.$title, this.$accel )
6142 .prop( 'tabIndex', 0 )
6143 .attr( 'role', 'button' );
6144 this.$element
6145 .data( 'oo-ui-tool', this )
6146 .addClass(
6147 'oo-ui-tool ' + 'oo-ui-tool-name-' +
6148 this.constructor.static.name.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' )
6149 )
6150 .append( this.$link );
6151 this.setTitle( config.title || this.constructor.static.title );
6152 };
6153
6154 /* Setup */
6155
6156 OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
6157 OO.mixinClass( OO.ui.Tool, OO.ui.IconElement );
6158 OO.mixinClass( OO.ui.Tool, OO.ui.FlaggedElement );
6159
6160 /* Events */
6161
6162 /**
6163 * @event select
6164 */
6165
6166 /* Static Properties */
6167
6168 /**
6169 * @static
6170 * @inheritdoc
6171 */
6172 OO.ui.Tool.static.tagName = 'span';
6173
6174 /**
6175 * Symbolic name of tool.
6176 *
6177 * @abstract
6178 * @static
6179 * @inheritable
6180 * @property {string}
6181 */
6182 OO.ui.Tool.static.name = '';
6183
6184 /**
6185 * Tool group.
6186 *
6187 * @abstract
6188 * @static
6189 * @inheritable
6190 * @property {string}
6191 */
6192 OO.ui.Tool.static.group = '';
6193
6194 /**
6195 * Tool title.
6196 *
6197 * Title is used as a tooltip when the tool is part of a bar tool group, or a label when the tool
6198 * is part of a list or menu tool group. If a trigger is associated with an action by the same name
6199 * as the tool, a description of its keyboard shortcut for the appropriate platform will be
6200 * appended to the title if the tool is part of a bar tool group.
6201 *
6202 * @abstract
6203 * @static
6204 * @inheritable
6205 * @property {string|Function} Title text or a function that returns text
6206 */
6207 OO.ui.Tool.static.title = '';
6208
6209 /**
6210 * Tool can be automatically added to catch-all groups.
6211 *
6212 * @static
6213 * @inheritable
6214 * @property {boolean}
6215 */
6216 OO.ui.Tool.static.autoAddToCatchall = true;
6217
6218 /**
6219 * Tool can be automatically added to named groups.
6220 *
6221 * @static
6222 * @property {boolean}
6223 * @inheritable
6224 */
6225 OO.ui.Tool.static.autoAddToGroup = true;
6226
6227 /**
6228 * Check if this tool is compatible with given data.
6229 *
6230 * @static
6231 * @inheritable
6232 * @param {Mixed} data Data to check
6233 * @return {boolean} Tool can be used with data
6234 */
6235 OO.ui.Tool.static.isCompatibleWith = function () {
6236 return false;
6237 };
6238
6239 /* Methods */
6240
6241 /**
6242 * Handle the toolbar state being updated.
6243 *
6244 * This is an abstract method that must be overridden in a concrete subclass.
6245 *
6246 * @abstract
6247 */
6248 OO.ui.Tool.prototype.onUpdateState = function () {
6249 throw new Error(
6250 'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor
6251 );
6252 };
6253
6254 /**
6255 * Handle the tool being selected.
6256 *
6257 * This is an abstract method that must be overridden in a concrete subclass.
6258 *
6259 * @abstract
6260 */
6261 OO.ui.Tool.prototype.onSelect = function () {
6262 throw new Error(
6263 'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor
6264 );
6265 };
6266
6267 /**
6268 * Check if the button is active.
6269 *
6270 * @return {boolean} Button is active
6271 */
6272 OO.ui.Tool.prototype.isActive = function () {
6273 return this.active;
6274 };
6275
6276 /**
6277 * Make the button appear active or inactive.
6278 *
6279 * @param {boolean} state Make button appear active
6280 */
6281 OO.ui.Tool.prototype.setActive = function ( state ) {
6282 this.active = !!state;
6283 if ( this.active ) {
6284 this.$element.addClass( 'oo-ui-tool-active' );
6285 } else {
6286 this.$element.removeClass( 'oo-ui-tool-active' );
6287 }
6288 };
6289
6290 /**
6291 * Get the tool title.
6292 *
6293 * @param {string|Function} title Title text or a function that returns text
6294 * @chainable
6295 */
6296 OO.ui.Tool.prototype.setTitle = function ( title ) {
6297 this.title = OO.ui.resolveMsg( title );
6298 this.updateTitle();
6299 return this;
6300 };
6301
6302 /**
6303 * Get the tool title.
6304 *
6305 * @return {string} Title text
6306 */
6307 OO.ui.Tool.prototype.getTitle = function () {
6308 return this.title;
6309 };
6310
6311 /**
6312 * Get the tool's symbolic name.
6313 *
6314 * @return {string} Symbolic name of tool
6315 */
6316 OO.ui.Tool.prototype.getName = function () {
6317 return this.constructor.static.name;
6318 };
6319
6320 /**
6321 * Update the title.
6322 */
6323 OO.ui.Tool.prototype.updateTitle = function () {
6324 var titleTooltips = this.toolGroup.constructor.static.titleTooltips,
6325 accelTooltips = this.toolGroup.constructor.static.accelTooltips,
6326 accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
6327 tooltipParts = [];
6328
6329 this.$title.text( this.title );
6330 this.$accel.text( accel );
6331
6332 if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
6333 tooltipParts.push( this.title );
6334 }
6335 if ( accelTooltips && typeof accel === 'string' && accel.length ) {
6336 tooltipParts.push( accel );
6337 }
6338 if ( tooltipParts.length ) {
6339 this.$link.attr( 'title', tooltipParts.join( ' ' ) );
6340 } else {
6341 this.$link.removeAttr( 'title' );
6342 }
6343 };
6344
6345 /**
6346 * Destroy tool.
6347 */
6348 OO.ui.Tool.prototype.destroy = function () {
6349 this.toolbar.disconnect( this );
6350 this.$element.remove();
6351 };
6352
6353 /**
6354 * Collection of tool groups.
6355 *
6356 * @class
6357 * @extends OO.ui.Element
6358 * @mixins OO.EventEmitter
6359 * @mixins OO.ui.GroupElement
6360 *
6361 * @constructor
6362 * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
6363 * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating tool groups
6364 * @param {Object} [config] Configuration options
6365 * @cfg {boolean} [actions] Add an actions section opposite to the tools
6366 * @cfg {boolean} [shadow] Add a shadow below the toolbar
6367 */
6368 OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
6369 // Allow passing positional parameters inside the config object
6370 if ( OO.isPlainObject( toolFactory ) && config === undefined ) {
6371 config = toolFactory;
6372 toolFactory = config.toolFactory;
6373 toolGroupFactory = config.toolGroupFactory;
6374 }
6375
6376 // Configuration initialization
6377 config = config || {};
6378
6379 // Parent constructor
6380 OO.ui.Toolbar.super.call( this, config );
6381
6382 // Mixin constructors
6383 OO.EventEmitter.call( this );
6384 OO.ui.GroupElement.call( this, config );
6385
6386 // Properties
6387 this.toolFactory = toolFactory;
6388 this.toolGroupFactory = toolGroupFactory;
6389 this.groups = [];
6390 this.tools = {};
6391 this.$bar = $( '<div>' );
6392 this.$actions = $( '<div>' );
6393 this.initialized = false;
6394
6395 // Events
6396 this.$element
6397 .add( this.$bar ).add( this.$group ).add( this.$actions )
6398 .on( 'mousedown touchstart', this.onPointerDown.bind( this ) );
6399
6400 // Initialization
6401 this.$group.addClass( 'oo-ui-toolbar-tools' );
6402 if ( config.actions ) {
6403 this.$bar.append( this.$actions.addClass( 'oo-ui-toolbar-actions' ) );
6404 }
6405 this.$bar
6406 .addClass( 'oo-ui-toolbar-bar' )
6407 .append( this.$group, '<div style="clear:both"></div>' );
6408 if ( config.shadow ) {
6409 this.$bar.append( '<div class="oo-ui-toolbar-shadow"></div>' );
6410 }
6411 this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar );
6412 };
6413
6414 /* Setup */
6415
6416 OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
6417 OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
6418 OO.mixinClass( OO.ui.Toolbar, OO.ui.GroupElement );
6419
6420 /* Methods */
6421
6422 /**
6423 * Get the tool factory.
6424 *
6425 * @return {OO.ui.ToolFactory} Tool factory
6426 */
6427 OO.ui.Toolbar.prototype.getToolFactory = function () {
6428 return this.toolFactory;
6429 };
6430
6431 /**
6432 * Get the tool group factory.
6433 *
6434 * @return {OO.Factory} Tool group factory
6435 */
6436 OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
6437 return this.toolGroupFactory;
6438 };
6439
6440 /**
6441 * Handles mouse down events.
6442 *
6443 * @param {jQuery.Event} e Mouse down event
6444 */
6445 OO.ui.Toolbar.prototype.onPointerDown = function ( e ) {
6446 var $closestWidgetToEvent = $( e.target ).closest( '.oo-ui-widget' ),
6447 $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
6448 if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[ 0 ] === $closestWidgetToToolbar[ 0 ] ) {
6449 return false;
6450 }
6451 };
6452
6453 /**
6454 * Sets up handles and preloads required information for the toolbar to work.
6455 * This must be called after it is attached to a visible document and before doing anything else.
6456 */
6457 OO.ui.Toolbar.prototype.initialize = function () {
6458 this.initialized = true;
6459 };
6460
6461 /**
6462 * Setup toolbar.
6463 *
6464 * Tools can be specified in the following ways:
6465 *
6466 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
6467 * - All tools in a group: `{ group: 'group-name' }`
6468 * - All tools: `'*'` - Using this will make the group a list with a "More" label by default
6469 *
6470 * @param {Object.<string,Array>} groups List of tool group configurations
6471 * @param {Array|string} [groups.include] Tools to include
6472 * @param {Array|string} [groups.exclude] Tools to exclude
6473 * @param {Array|string} [groups.promote] Tools to promote to the beginning
6474 * @param {Array|string} [groups.demote] Tools to demote to the end
6475 */
6476 OO.ui.Toolbar.prototype.setup = function ( groups ) {
6477 var i, len, type, group,
6478 items = [],
6479 defaultType = 'bar';
6480
6481 // Cleanup previous groups
6482 this.reset();
6483
6484 // Build out new groups
6485 for ( i = 0, len = groups.length; i < len; i++ ) {
6486 group = groups[ i ];
6487 if ( group.include === '*' ) {
6488 // Apply defaults to catch-all groups
6489 if ( group.type === undefined ) {
6490 group.type = 'list';
6491 }
6492 if ( group.label === undefined ) {
6493 group.label = OO.ui.msg( 'ooui-toolbar-more' );
6494 }
6495 }
6496 // Check type has been registered
6497 type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType;
6498 items.push(
6499 this.getToolGroupFactory().create( type, this, group )
6500 );
6501 }
6502 this.addItems( items );
6503 };
6504
6505 /**
6506 * Remove all tools and groups from the toolbar.
6507 */
6508 OO.ui.Toolbar.prototype.reset = function () {
6509 var i, len;
6510
6511 this.groups = [];
6512 this.tools = {};
6513 for ( i = 0, len = this.items.length; i < len; i++ ) {
6514 this.items[ i ].destroy();
6515 }
6516 this.clearItems();
6517 };
6518
6519 /**
6520 * Destroys toolbar, removing event handlers and DOM elements.
6521 *
6522 * Call this whenever you are done using a toolbar.
6523 */
6524 OO.ui.Toolbar.prototype.destroy = function () {
6525 this.reset();
6526 this.$element.remove();
6527 };
6528
6529 /**
6530 * Check if tool has not been used yet.
6531 *
6532 * @param {string} name Symbolic name of tool
6533 * @return {boolean} Tool is available
6534 */
6535 OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
6536 return !this.tools[ name ];
6537 };
6538
6539 /**
6540 * Prevent tool from being used again.
6541 *
6542 * @param {OO.ui.Tool} tool Tool to reserve
6543 */
6544 OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
6545 this.tools[ tool.getName() ] = tool;
6546 };
6547
6548 /**
6549 * Allow tool to be used again.
6550 *
6551 * @param {OO.ui.Tool} tool Tool to release
6552 */
6553 OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
6554 delete this.tools[ tool.getName() ];
6555 };
6556
6557 /**
6558 * Get accelerator label for tool.
6559 *
6560 * This is a stub that should be overridden to provide access to accelerator information.
6561 *
6562 * @param {string} name Symbolic name of tool
6563 * @return {string|undefined} Tool accelerator label if available
6564 */
6565 OO.ui.Toolbar.prototype.getToolAccelerator = function () {
6566 return undefined;
6567 };
6568
6569 /**
6570 * Collection of tools.
6571 *
6572 * Tools can be specified in the following ways:
6573 *
6574 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
6575 * - All tools in a group: `{ group: 'group-name' }`
6576 * - All tools: `'*'`
6577 *
6578 * @abstract
6579 * @class
6580 * @extends OO.ui.Widget
6581 * @mixins OO.ui.GroupElement
6582 *
6583 * @constructor
6584 * @param {OO.ui.Toolbar} toolbar
6585 * @param {Object} [config] Configuration options
6586 * @cfg {Array|string} [include=[]] List of tools to include
6587 * @cfg {Array|string} [exclude=[]] List of tools to exclude
6588 * @cfg {Array|string} [promote=[]] List of tools to promote to the beginning
6589 * @cfg {Array|string} [demote=[]] List of tools to demote to the end
6590 */
6591 OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
6592 // Allow passing positional parameters inside the config object
6593 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
6594 config = toolbar;
6595 toolbar = config.toolbar;
6596 }
6597
6598 // Configuration initialization
6599 config = config || {};
6600
6601 // Parent constructor
6602 OO.ui.ToolGroup.super.call( this, config );
6603
6604 // Mixin constructors
6605 OO.ui.GroupElement.call( this, config );
6606
6607 // Properties
6608 this.toolbar = toolbar;
6609 this.tools = {};
6610 this.pressed = null;
6611 this.autoDisabled = false;
6612 this.include = config.include || [];
6613 this.exclude = config.exclude || [];
6614 this.promote = config.promote || [];
6615 this.demote = config.demote || [];
6616 this.onCapturedMouseUpHandler = this.onCapturedMouseUp.bind( this );
6617
6618 // Events
6619 this.$element.on( {
6620 'mousedown touchstart': this.onPointerDown.bind( this ),
6621 'mouseup touchend': this.onPointerUp.bind( this ),
6622 mouseover: this.onMouseOver.bind( this ),
6623 mouseout: this.onMouseOut.bind( this )
6624 } );
6625 this.toolbar.getToolFactory().connect( this, { register: 'onToolFactoryRegister' } );
6626 this.aggregate( { disable: 'itemDisable' } );
6627 this.connect( this, { itemDisable: 'updateDisabled' } );
6628
6629 // Initialization
6630 this.$group.addClass( 'oo-ui-toolGroup-tools' );
6631 this.$element
6632 .addClass( 'oo-ui-toolGroup' )
6633 .append( this.$group );
6634 this.populate();
6635 };
6636
6637 /* Setup */
6638
6639 OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget );
6640 OO.mixinClass( OO.ui.ToolGroup, OO.ui.GroupElement );
6641
6642 /* Events */
6643
6644 /**
6645 * @event update
6646 */
6647
6648 /* Static Properties */
6649
6650 /**
6651 * Show labels in tooltips.
6652 *
6653 * @static
6654 * @inheritable
6655 * @property {boolean}
6656 */
6657 OO.ui.ToolGroup.static.titleTooltips = false;
6658
6659 /**
6660 * Show acceleration labels in tooltips.
6661 *
6662 * @static
6663 * @inheritable
6664 * @property {boolean}
6665 */
6666 OO.ui.ToolGroup.static.accelTooltips = false;
6667
6668 /**
6669 * Automatically disable the toolgroup when all tools are disabled
6670 *
6671 * @static
6672 * @inheritable
6673 * @property {boolean}
6674 */
6675 OO.ui.ToolGroup.static.autoDisable = true;
6676
6677 /* Methods */
6678
6679 /**
6680 * @inheritdoc
6681 */
6682 OO.ui.ToolGroup.prototype.isDisabled = function () {
6683 return this.autoDisabled || OO.ui.ToolGroup.super.prototype.isDisabled.apply( this, arguments );
6684 };
6685
6686 /**
6687 * @inheritdoc
6688 */
6689 OO.ui.ToolGroup.prototype.updateDisabled = function () {
6690 var i, item, allDisabled = true;
6691
6692 if ( this.constructor.static.autoDisable ) {
6693 for ( i = this.items.length - 1; i >= 0; i-- ) {
6694 item = this.items[ i ];
6695 if ( !item.isDisabled() ) {
6696 allDisabled = false;
6697 break;
6698 }
6699 }
6700 this.autoDisabled = allDisabled;
6701 }
6702 OO.ui.ToolGroup.super.prototype.updateDisabled.apply( this, arguments );
6703 };
6704
6705 /**
6706 * Handle mouse down events.
6707 *
6708 * @param {jQuery.Event} e Mouse down event
6709 */
6710 OO.ui.ToolGroup.prototype.onPointerDown = function ( e ) {
6711 // e.which is 0 for touch events, 1 for left mouse button
6712 if ( !this.isDisabled() && e.which <= 1 ) {
6713 this.pressed = this.getTargetTool( e );
6714 if ( this.pressed ) {
6715 this.pressed.setActive( true );
6716 this.getElementDocument().addEventListener(
6717 'mouseup', this.onCapturedMouseUpHandler, true
6718 );
6719 }
6720 }
6721 return false;
6722 };
6723
6724 /**
6725 * Handle captured mouse up events.
6726 *
6727 * @param {Event} e Mouse up event
6728 */
6729 OO.ui.ToolGroup.prototype.onCapturedMouseUp = function ( e ) {
6730 this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseUpHandler, true );
6731 // onPointerUp may be called a second time, depending on where the mouse is when the button is
6732 // released, but since `this.pressed` will no longer be true, the second call will be ignored.
6733 this.onPointerUp( e );
6734 };
6735
6736 /**
6737 * Handle mouse up events.
6738 *
6739 * @param {jQuery.Event} e Mouse up event
6740 */
6741 OO.ui.ToolGroup.prototype.onPointerUp = function ( e ) {
6742 var tool = this.getTargetTool( e );
6743
6744 // e.which is 0 for touch events, 1 for left mouse button
6745 if ( !this.isDisabled() && e.which <= 1 && this.pressed && this.pressed === tool ) {
6746 this.pressed.onSelect();
6747 }
6748
6749 this.pressed = null;
6750 return false;
6751 };
6752
6753 /**
6754 * Handle mouse over events.
6755 *
6756 * @param {jQuery.Event} e Mouse over event
6757 */
6758 OO.ui.ToolGroup.prototype.onMouseOver = function ( e ) {
6759 var tool = this.getTargetTool( e );
6760
6761 if ( this.pressed && this.pressed === tool ) {
6762 this.pressed.setActive( true );
6763 }
6764 };
6765
6766 /**
6767 * Handle mouse out events.
6768 *
6769 * @param {jQuery.Event} e Mouse out event
6770 */
6771 OO.ui.ToolGroup.prototype.onMouseOut = function ( e ) {
6772 var tool = this.getTargetTool( e );
6773
6774 if ( this.pressed && this.pressed === tool ) {
6775 this.pressed.setActive( false );
6776 }
6777 };
6778
6779 /**
6780 * Get the closest tool to a jQuery.Event.
6781 *
6782 * Only tool links are considered, which prevents other elements in the tool such as popups from
6783 * triggering tool group interactions.
6784 *
6785 * @private
6786 * @param {jQuery.Event} e
6787 * @return {OO.ui.Tool|null} Tool, `null` if none was found
6788 */
6789 OO.ui.ToolGroup.prototype.getTargetTool = function ( e ) {
6790 var tool,
6791 $item = $( e.target ).closest( '.oo-ui-tool-link' );
6792
6793 if ( $item.length ) {
6794 tool = $item.parent().data( 'oo-ui-tool' );
6795 }
6796
6797 return tool && !tool.isDisabled() ? tool : null;
6798 };
6799
6800 /**
6801 * Handle tool registry register events.
6802 *
6803 * If a tool is registered after the group is created, we must repopulate the list to account for:
6804 *
6805 * - a tool being added that may be included
6806 * - a tool already included being overridden
6807 *
6808 * @param {string} name Symbolic name of tool
6809 */
6810 OO.ui.ToolGroup.prototype.onToolFactoryRegister = function () {
6811 this.populate();
6812 };
6813
6814 /**
6815 * Get the toolbar this group is in.
6816 *
6817 * @return {OO.ui.Toolbar} Toolbar of group
6818 */
6819 OO.ui.ToolGroup.prototype.getToolbar = function () {
6820 return this.toolbar;
6821 };
6822
6823 /**
6824 * Add and remove tools based on configuration.
6825 */
6826 OO.ui.ToolGroup.prototype.populate = function () {
6827 var i, len, name, tool,
6828 toolFactory = this.toolbar.getToolFactory(),
6829 names = {},
6830 add = [],
6831 remove = [],
6832 list = this.toolbar.getToolFactory().getTools(
6833 this.include, this.exclude, this.promote, this.demote
6834 );
6835
6836 // Build a list of needed tools
6837 for ( i = 0, len = list.length; i < len; i++ ) {
6838 name = list[ i ];
6839 if (
6840 // Tool exists
6841 toolFactory.lookup( name ) &&
6842 // Tool is available or is already in this group
6843 ( this.toolbar.isToolAvailable( name ) || this.tools[ name ] )
6844 ) {
6845 // Hack to prevent infinite recursion via ToolGroupTool. We need to reserve the tool before
6846 // creating it, but we can't call reserveTool() yet because we haven't created the tool.
6847 this.toolbar.tools[ name ] = true;
6848 tool = this.tools[ name ];
6849 if ( !tool ) {
6850 // Auto-initialize tools on first use
6851 this.tools[ name ] = tool = toolFactory.create( name, this );
6852 tool.updateTitle();
6853 }
6854 this.toolbar.reserveTool( tool );
6855 add.push( tool );
6856 names[ name ] = true;
6857 }
6858 }
6859 // Remove tools that are no longer needed
6860 for ( name in this.tools ) {
6861 if ( !names[ name ] ) {
6862 this.tools[ name ].destroy();
6863 this.toolbar.releaseTool( this.tools[ name ] );
6864 remove.push( this.tools[ name ] );
6865 delete this.tools[ name ];
6866 }
6867 }
6868 if ( remove.length ) {
6869 this.removeItems( remove );
6870 }
6871 // Update emptiness state
6872 if ( add.length ) {
6873 this.$element.removeClass( 'oo-ui-toolGroup-empty' );
6874 } else {
6875 this.$element.addClass( 'oo-ui-toolGroup-empty' );
6876 }
6877 // Re-add tools (moving existing ones to new locations)
6878 this.addItems( add );
6879 // Disabled state may depend on items
6880 this.updateDisabled();
6881 };
6882
6883 /**
6884 * Destroy tool group.
6885 */
6886 OO.ui.ToolGroup.prototype.destroy = function () {
6887 var name;
6888
6889 this.clearItems();
6890 this.toolbar.getToolFactory().disconnect( this );
6891 for ( name in this.tools ) {
6892 this.toolbar.releaseTool( this.tools[ name ] );
6893 this.tools[ name ].disconnect( this ).destroy();
6894 delete this.tools[ name ];
6895 }
6896 this.$element.remove();
6897 };
6898
6899 /**
6900 * Dialog for showing a message.
6901 *
6902 * User interface:
6903 * - Registers two actions by default (safe and primary).
6904 * - Renders action widgets in the footer.
6905 *
6906 * @class
6907 * @extends OO.ui.Dialog
6908 *
6909 * @constructor
6910 * @param {Object} [config] Configuration options
6911 */
6912 OO.ui.MessageDialog = function OoUiMessageDialog( config ) {
6913 // Parent constructor
6914 OO.ui.MessageDialog.super.call( this, config );
6915
6916 // Properties
6917 this.verticalActionLayout = null;
6918
6919 // Initialization
6920 this.$element.addClass( 'oo-ui-messageDialog' );
6921 };
6922
6923 /* Inheritance */
6924
6925 OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog );
6926
6927 /* Static Properties */
6928
6929 OO.ui.MessageDialog.static.name = 'message';
6930
6931 OO.ui.MessageDialog.static.size = 'small';
6932
6933 OO.ui.MessageDialog.static.verbose = false;
6934
6935 /**
6936 * Dialog title.
6937 *
6938 * A confirmation dialog's title should describe what the progressive action will do. An alert
6939 * dialog's title should describe what event occurred.
6940 *
6941 * @static
6942 * inheritable
6943 * @property {jQuery|string|Function|null}
6944 */
6945 OO.ui.MessageDialog.static.title = null;
6946
6947 /**
6948 * A confirmation dialog's message should describe the consequences of the progressive action. An
6949 * alert dialog's message should describe why the event occurred.
6950 *
6951 * @static
6952 * inheritable
6953 * @property {jQuery|string|Function|null}
6954 */
6955 OO.ui.MessageDialog.static.message = null;
6956
6957 OO.ui.MessageDialog.static.actions = [
6958 { action: 'accept', label: OO.ui.deferMsg( 'ooui-dialog-message-accept' ), flags: 'primary' },
6959 { action: 'reject', label: OO.ui.deferMsg( 'ooui-dialog-message-reject' ), flags: 'safe' }
6960 ];
6961
6962 /* Methods */
6963
6964 /**
6965 * @inheritdoc
6966 */
6967 OO.ui.MessageDialog.prototype.setManager = function ( manager ) {
6968 OO.ui.MessageDialog.super.prototype.setManager.call( this, manager );
6969
6970 // Events
6971 this.manager.connect( this, {
6972 resize: 'onResize'
6973 } );
6974
6975 return this;
6976 };
6977
6978 /**
6979 * @inheritdoc
6980 */
6981 OO.ui.MessageDialog.prototype.onActionResize = function ( action ) {
6982 this.fitActions();
6983 return OO.ui.MessageDialog.super.prototype.onActionResize.call( this, action );
6984 };
6985
6986 /**
6987 * Handle window resized events.
6988 */
6989 OO.ui.MessageDialog.prototype.onResize = function () {
6990 var dialog = this;
6991 dialog.fitActions();
6992 // Wait for CSS transition to finish and do it again :(
6993 setTimeout( function () {
6994 dialog.fitActions();
6995 }, 300 );
6996 };
6997
6998 /**
6999 * Toggle action layout between vertical and horizontal.
7000 *
7001 * @param {boolean} [value] Layout actions vertically, omit to toggle
7002 * @chainable
7003 */
7004 OO.ui.MessageDialog.prototype.toggleVerticalActionLayout = function ( value ) {
7005 value = value === undefined ? !this.verticalActionLayout : !!value;
7006
7007 if ( value !== this.verticalActionLayout ) {
7008 this.verticalActionLayout = value;
7009 this.$actions
7010 .toggleClass( 'oo-ui-messageDialog-actions-vertical', value )
7011 .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value );
7012 }
7013
7014 return this;
7015 };
7016
7017 /**
7018 * @inheritdoc
7019 */
7020 OO.ui.MessageDialog.prototype.getActionProcess = function ( action ) {
7021 if ( action ) {
7022 return new OO.ui.Process( function () {
7023 this.close( { action: action } );
7024 }, this );
7025 }
7026 return OO.ui.MessageDialog.super.prototype.getActionProcess.call( this, action );
7027 };
7028
7029 /**
7030 * @inheritdoc
7031 *
7032 * @param {Object} [data] Dialog opening data
7033 * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
7034 * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
7035 * @param {boolean} [data.verbose] Message is verbose and should be styled as a long message
7036 * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each
7037 * action item
7038 */
7039 OO.ui.MessageDialog.prototype.getSetupProcess = function ( data ) {
7040 data = data || {};
7041
7042 // Parent method
7043 return OO.ui.MessageDialog.super.prototype.getSetupProcess.call( this, data )
7044 .next( function () {
7045 this.title.setLabel(
7046 data.title !== undefined ? data.title : this.constructor.static.title
7047 );
7048 this.message.setLabel(
7049 data.message !== undefined ? data.message : this.constructor.static.message
7050 );
7051 this.message.$element.toggleClass(
7052 'oo-ui-messageDialog-message-verbose',
7053 data.verbose !== undefined ? data.verbose : this.constructor.static.verbose
7054 );
7055 }, this );
7056 };
7057
7058 /**
7059 * @inheritdoc
7060 */
7061 OO.ui.MessageDialog.prototype.getBodyHeight = function () {
7062 var bodyHeight, oldOverflow,
7063 $scrollable = this.container.$element;
7064
7065 oldOverflow = $scrollable[ 0 ].style.overflow;
7066 $scrollable[ 0 ].style.overflow = 'hidden';
7067
7068 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
7069
7070 bodyHeight = this.text.$element.outerHeight( true );
7071 $scrollable[ 0 ].style.overflow = oldOverflow;
7072
7073 return bodyHeight;
7074 };
7075
7076 /**
7077 * @inheritdoc
7078 */
7079 OO.ui.MessageDialog.prototype.setDimensions = function ( dim ) {
7080 var $scrollable = this.container.$element;
7081 OO.ui.MessageDialog.super.prototype.setDimensions.call( this, dim );
7082
7083 // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
7084 // Need to do it after transition completes (250ms), add 50ms just in case.
7085 setTimeout( function () {
7086 var oldOverflow = $scrollable[ 0 ].style.overflow;
7087 $scrollable[ 0 ].style.overflow = 'hidden';
7088
7089 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
7090
7091 $scrollable[ 0 ].style.overflow = oldOverflow;
7092 }, 300 );
7093
7094 return this;
7095 };
7096
7097 /**
7098 * @inheritdoc
7099 */
7100 OO.ui.MessageDialog.prototype.initialize = function () {
7101 // Parent method
7102 OO.ui.MessageDialog.super.prototype.initialize.call( this );
7103
7104 // Properties
7105 this.$actions = $( '<div>' );
7106 this.container = new OO.ui.PanelLayout( {
7107 scrollable: true, classes: [ 'oo-ui-messageDialog-container' ]
7108 } );
7109 this.text = new OO.ui.PanelLayout( {
7110 padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ]
7111 } );
7112 this.message = new OO.ui.LabelWidget( {
7113 classes: [ 'oo-ui-messageDialog-message' ]
7114 } );
7115
7116 // Initialization
7117 this.title.$element.addClass( 'oo-ui-messageDialog-title' );
7118 this.$content.addClass( 'oo-ui-messageDialog-content' );
7119 this.container.$element.append( this.text.$element );
7120 this.text.$element.append( this.title.$element, this.message.$element );
7121 this.$body.append( this.container.$element );
7122 this.$actions.addClass( 'oo-ui-messageDialog-actions' );
7123 this.$foot.append( this.$actions );
7124 };
7125
7126 /**
7127 * @inheritdoc
7128 */
7129 OO.ui.MessageDialog.prototype.attachActions = function () {
7130 var i, len, other, special, others;
7131
7132 // Parent method
7133 OO.ui.MessageDialog.super.prototype.attachActions.call( this );
7134
7135 special = this.actions.getSpecial();
7136 others = this.actions.getOthers();
7137 if ( special.safe ) {
7138 this.$actions.append( special.safe.$element );
7139 special.safe.toggleFramed( false );
7140 }
7141 if ( others.length ) {
7142 for ( i = 0, len = others.length; i < len; i++ ) {
7143 other = others[ i ];
7144 this.$actions.append( other.$element );
7145 other.toggleFramed( false );
7146 }
7147 }
7148 if ( special.primary ) {
7149 this.$actions.append( special.primary.$element );
7150 special.primary.toggleFramed( false );
7151 }
7152
7153 if ( !this.isOpening() ) {
7154 // If the dialog is currently opening, this will be called automatically soon.
7155 // This also calls #fitActions.
7156 this.updateSize();
7157 }
7158 };
7159
7160 /**
7161 * Fit action actions into columns or rows.
7162 *
7163 * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
7164 */
7165 OO.ui.MessageDialog.prototype.fitActions = function () {
7166 var i, len, action,
7167 previous = this.verticalActionLayout,
7168 actions = this.actions.get();
7169
7170 // Detect clipping
7171 this.toggleVerticalActionLayout( false );
7172 for ( i = 0, len = actions.length; i < len; i++ ) {
7173 action = actions[ i ];
7174 if ( action.$element.innerWidth() < action.$label.outerWidth( true ) ) {
7175 this.toggleVerticalActionLayout( true );
7176 break;
7177 }
7178 }
7179
7180 // Move the body out of the way of the foot
7181 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
7182
7183 if ( this.verticalActionLayout !== previous ) {
7184 // We changed the layout, window height might need to be updated.
7185 this.updateSize();
7186 }
7187 };
7188
7189 /**
7190 * Navigation dialog window.
7191 *
7192 * Logic:
7193 * - Show and hide errors.
7194 * - Retry an action.
7195 *
7196 * User interface:
7197 * - Renders header with dialog title and one action widget on either side
7198 * (a 'safe' button on the left, and a 'primary' button on the right, both of
7199 * which close the dialog).
7200 * - Displays any action widgets in the footer (none by default).
7201 * - Ability to dismiss errors.
7202 *
7203 * Subclass responsibilities:
7204 * - Register a 'safe' action.
7205 * - Register a 'primary' action.
7206 * - Add content to the dialog.
7207 *
7208 * @abstract
7209 * @class
7210 * @extends OO.ui.Dialog
7211 *
7212 * @constructor
7213 * @param {Object} [config] Configuration options
7214 */
7215 OO.ui.ProcessDialog = function OoUiProcessDialog( config ) {
7216 // Parent constructor
7217 OO.ui.ProcessDialog.super.call( this, config );
7218
7219 // Initialization
7220 this.$element.addClass( 'oo-ui-processDialog' );
7221 };
7222
7223 /* Setup */
7224
7225 OO.inheritClass( OO.ui.ProcessDialog, OO.ui.Dialog );
7226
7227 /* Methods */
7228
7229 /**
7230 * Handle dismiss button click events.
7231 *
7232 * Hides errors.
7233 */
7234 OO.ui.ProcessDialog.prototype.onDismissErrorButtonClick = function () {
7235 this.hideErrors();
7236 };
7237
7238 /**
7239 * Handle retry button click events.
7240 *
7241 * Hides errors and then tries again.
7242 */
7243 OO.ui.ProcessDialog.prototype.onRetryButtonClick = function () {
7244 this.hideErrors();
7245 this.executeAction( this.currentAction );
7246 };
7247
7248 /**
7249 * @inheritdoc
7250 */
7251 OO.ui.ProcessDialog.prototype.onActionResize = function ( action ) {
7252 if ( this.actions.isSpecial( action ) ) {
7253 this.fitLabel();
7254 }
7255 return OO.ui.ProcessDialog.super.prototype.onActionResize.call( this, action );
7256 };
7257
7258 /**
7259 * @inheritdoc
7260 */
7261 OO.ui.ProcessDialog.prototype.initialize = function () {
7262 // Parent method
7263 OO.ui.ProcessDialog.super.prototype.initialize.call( this );
7264
7265 // Properties
7266 this.$navigation = $( '<div>' );
7267 this.$location = $( '<div>' );
7268 this.$safeActions = $( '<div>' );
7269 this.$primaryActions = $( '<div>' );
7270 this.$otherActions = $( '<div>' );
7271 this.dismissButton = new OO.ui.ButtonWidget( {
7272 label: OO.ui.msg( 'ooui-dialog-process-dismiss' )
7273 } );
7274 this.retryButton = new OO.ui.ButtonWidget();
7275 this.$errors = $( '<div>' );
7276 this.$errorsTitle = $( '<div>' );
7277
7278 // Events
7279 this.dismissButton.connect( this, { click: 'onDismissErrorButtonClick' } );
7280 this.retryButton.connect( this, { click: 'onRetryButtonClick' } );
7281
7282 // Initialization
7283 this.title.$element.addClass( 'oo-ui-processDialog-title' );
7284 this.$location
7285 .append( this.title.$element )
7286 .addClass( 'oo-ui-processDialog-location' );
7287 this.$safeActions.addClass( 'oo-ui-processDialog-actions-safe' );
7288 this.$primaryActions.addClass( 'oo-ui-processDialog-actions-primary' );
7289 this.$otherActions.addClass( 'oo-ui-processDialog-actions-other' );
7290 this.$errorsTitle
7291 .addClass( 'oo-ui-processDialog-errors-title' )
7292 .text( OO.ui.msg( 'ooui-dialog-process-error' ) );
7293 this.$errors
7294 .addClass( 'oo-ui-processDialog-errors oo-ui-element-hidden' )
7295 .append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element );
7296 this.$content
7297 .addClass( 'oo-ui-processDialog-content' )
7298 .append( this.$errors );
7299 this.$navigation
7300 .addClass( 'oo-ui-processDialog-navigation' )
7301 .append( this.$safeActions, this.$location, this.$primaryActions );
7302 this.$head.append( this.$navigation );
7303 this.$foot.append( this.$otherActions );
7304 };
7305
7306 /**
7307 * @inheritdoc
7308 */
7309 OO.ui.ProcessDialog.prototype.attachActions = function () {
7310 var i, len, other, special, others;
7311
7312 // Parent method
7313 OO.ui.ProcessDialog.super.prototype.attachActions.call( this );
7314
7315 special = this.actions.getSpecial();
7316 others = this.actions.getOthers();
7317 if ( special.primary ) {
7318 this.$primaryActions.append( special.primary.$element );
7319 special.primary.toggleFramed( true );
7320 }
7321 for ( i = 0, len = others.length; i < len; i++ ) {
7322 other = others[ i ];
7323 this.$otherActions.append( other.$element );
7324 other.toggleFramed( true );
7325 }
7326 if ( special.safe ) {
7327 this.$safeActions.append( special.safe.$element );
7328 special.safe.toggleFramed( true );
7329 }
7330
7331 this.fitLabel();
7332 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
7333 };
7334
7335 /**
7336 * @inheritdoc
7337 */
7338 OO.ui.ProcessDialog.prototype.executeAction = function ( action ) {
7339 OO.ui.ProcessDialog.super.prototype.executeAction.call( this, action )
7340 .fail( this.showErrors.bind( this ) );
7341 };
7342
7343 /**
7344 * Fit label between actions.
7345 *
7346 * @chainable
7347 */
7348 OO.ui.ProcessDialog.prototype.fitLabel = function () {
7349 var width = Math.max(
7350 this.$safeActions.is( ':visible' ) ? this.$safeActions.width() : 0,
7351 this.$primaryActions.is( ':visible' ) ? this.$primaryActions.width() : 0
7352 );
7353 this.$location.css( { paddingLeft: width, paddingRight: width } );
7354
7355 return this;
7356 };
7357
7358 /**
7359 * Handle errors that occurred during accept or reject processes.
7360 *
7361 * @param {OO.ui.Error[]} errors Errors to be handled
7362 */
7363 OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
7364 var i, len, $item, actions,
7365 items = [],
7366 abilities = {},
7367 recoverable = true,
7368 warning = false;
7369
7370 for ( i = 0, len = errors.length; i < len; i++ ) {
7371 if ( !errors[ i ].isRecoverable() ) {
7372 recoverable = false;
7373 }
7374 if ( errors[ i ].isWarning() ) {
7375 warning = true;
7376 }
7377 $item = $( '<div>' )
7378 .addClass( 'oo-ui-processDialog-error' )
7379 .append( errors[ i ].getMessage() );
7380 items.push( $item[ 0 ] );
7381 }
7382 this.$errorItems = $( items );
7383 if ( recoverable ) {
7384 abilities[this.currentAction] = true;
7385 // Copy the flags from the first matching action
7386 actions = this.actions.get( { actions: this.currentAction } );
7387 if ( actions.length ) {
7388 this.retryButton.clearFlags().setFlags( actions[0].getFlags() );
7389 }
7390 } else {
7391 abilities[this.currentAction] = false;
7392 this.actions.setAbilities( abilities );
7393 }
7394 if ( warning ) {
7395 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-continue' ) );
7396 } else {
7397 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-retry' ) );
7398 }
7399 this.retryButton.toggle( recoverable );
7400 this.$errorsTitle.after( this.$errorItems );
7401 this.$errors.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 );
7402 };
7403
7404 /**
7405 * Hide errors.
7406 */
7407 OO.ui.ProcessDialog.prototype.hideErrors = function () {
7408 this.$errors.addClass( 'oo-ui-element-hidden' );
7409 if ( this.$errorItems ) {
7410 this.$errorItems.remove();
7411 this.$errorItems = null;
7412 }
7413 };
7414
7415 /**
7416 * @inheritdoc
7417 */
7418 OO.ui.ProcessDialog.prototype.getTeardownProcess = function ( data ) {
7419 // Parent method
7420 return OO.ui.ProcessDialog.super.prototype.getTeardownProcess.call( this, data )
7421 .first( function () {
7422 // Make sure to hide errors
7423 this.hideErrors();
7424 }, this );
7425 };
7426
7427 /**
7428 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
7429 * which is a widget that is specified by reference before any optional configuration settings.
7430 *
7431 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
7432 *
7433 * - **left**: The label is placed before the field-widget and aligned with the left margin.
7434 * A left-alignment is used for forms with many fields.
7435 * - **right**: The label is placed before the field-widget and aligned to the right margin.
7436 * A right-alignment is used for long but familiar forms which users tab through,
7437 * verifying the current field with a quick glance at the label.
7438 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
7439 * that users fill out from top to bottom.
7440 * - **inline**: The label is placed after the field-widget and aligned to the left.
7441 * An inline-alignment is best used with checkboxes or radio buttons.
7442 *
7443 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
7444 * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
7445 *
7446 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
7447 * @class
7448 * @extends OO.ui.Layout
7449 * @mixins OO.ui.LabelElement
7450 *
7451 * @constructor
7452 * @param {OO.ui.Widget} fieldWidget Field widget
7453 * @param {Object} [config] Configuration options
7454 * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline'
7455 * @cfg {string} [help] Explanatory text shown as a '?' icon.
7456 */
7457 OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
7458 // Allow passing positional parameters inside the config object
7459 if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
7460 config = fieldWidget;
7461 fieldWidget = config.fieldWidget;
7462 }
7463
7464 var hasInputWidget = fieldWidget instanceof OO.ui.InputWidget;
7465
7466 // Configuration initialization
7467 config = $.extend( { align: 'left' }, config );
7468
7469 // Parent constructor
7470 OO.ui.FieldLayout.super.call( this, config );
7471
7472 // Mixin constructors
7473 OO.ui.LabelElement.call( this, config );
7474
7475 // Properties
7476 this.fieldWidget = fieldWidget;
7477 this.$field = $( '<div>' );
7478 this.$body = $( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
7479 this.align = null;
7480 if ( config.help ) {
7481 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
7482 classes: [ 'oo-ui-fieldLayout-help' ],
7483 framed: false,
7484 icon: 'info'
7485 } );
7486
7487 this.popupButtonWidget.getPopup().$body.append(
7488 $( '<div>' )
7489 .text( config.help )
7490 .addClass( 'oo-ui-fieldLayout-help-content' )
7491 );
7492 this.$help = this.popupButtonWidget.$element;
7493 } else {
7494 this.$help = $( [] );
7495 }
7496
7497 // Events
7498 if ( hasInputWidget ) {
7499 this.$label.on( 'click', this.onLabelClick.bind( this ) );
7500 }
7501 this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
7502
7503 // Initialization
7504 this.$element
7505 .addClass( 'oo-ui-fieldLayout' )
7506 .append( this.$help, this.$body );
7507 this.$body.addClass( 'oo-ui-fieldLayout-body' );
7508 this.$field
7509 .addClass( 'oo-ui-fieldLayout-field' )
7510 .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget.isDisabled() )
7511 .append( this.fieldWidget.$element );
7512
7513 this.setAlignment( config.align );
7514 };
7515
7516 /* Setup */
7517
7518 OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
7519 OO.mixinClass( OO.ui.FieldLayout, OO.ui.LabelElement );
7520
7521 /* Methods */
7522
7523 /**
7524 * Handle field disable events.
7525 *
7526 * @param {boolean} value Field is disabled
7527 */
7528 OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
7529 this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
7530 };
7531
7532 /**
7533 * Handle label mouse click events.
7534 *
7535 * @param {jQuery.Event} e Mouse click event
7536 */
7537 OO.ui.FieldLayout.prototype.onLabelClick = function () {
7538 this.fieldWidget.simulateLabelClick();
7539 return false;
7540 };
7541
7542 /**
7543 * Get the field.
7544 *
7545 * @return {OO.ui.Widget} Field widget
7546 */
7547 OO.ui.FieldLayout.prototype.getField = function () {
7548 return this.fieldWidget;
7549 };
7550
7551 /**
7552 * Set the field alignment mode.
7553 *
7554 * @private
7555 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
7556 * @chainable
7557 */
7558 OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
7559 if ( value !== this.align ) {
7560 // Default to 'left'
7561 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) {
7562 value = 'left';
7563 }
7564 // Reorder elements
7565 if ( value === 'inline' ) {
7566 this.$body.append( this.$field, this.$label );
7567 } else {
7568 this.$body.append( this.$label, this.$field );
7569 }
7570 // Set classes. The following classes can be used here:
7571 // * oo-ui-fieldLayout-align-left
7572 // * oo-ui-fieldLayout-align-right
7573 // * oo-ui-fieldLayout-align-top
7574 // * oo-ui-fieldLayout-align-inline
7575 if ( this.align ) {
7576 this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
7577 }
7578 this.$element.addClass( 'oo-ui-fieldLayout-align-' + value );
7579 this.align = value;
7580 }
7581
7582 return this;
7583 };
7584
7585 /**
7586 * Layout made of a field, a button, and an optional label.
7587 *
7588 * @class
7589 * @extends OO.ui.FieldLayout
7590 *
7591 * @constructor
7592 * @param {OO.ui.Widget} fieldWidget Field widget
7593 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
7594 * @param {Object} [config] Configuration options
7595 * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline'
7596 * @cfg {string} [help] Explanatory text shown as a '?' icon.
7597 */
7598 OO.ui.ActionFieldLayout = function OoUiActionFieldLayout( fieldWidget, buttonWidget, config ) {
7599 // Allow passing positional parameters inside the config object
7600 if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
7601 config = fieldWidget;
7602 fieldWidget = config.fieldWidget;
7603 buttonWidget = config.buttonWidget;
7604 }
7605
7606 // Configuration initialization
7607 config = $.extend( { align: 'left' }, config );
7608
7609 // Parent constructor
7610 OO.ui.ActionFieldLayout.super.call( this, fieldWidget, config );
7611
7612 // Properties
7613 this.fieldWidget = fieldWidget;
7614 this.buttonWidget = buttonWidget;
7615 this.$button = $( '<div>' )
7616 .addClass( 'oo-ui-actionFieldLayout-button' )
7617 .append( this.buttonWidget.$element );
7618 this.$input = $( '<div>' )
7619 .addClass( 'oo-ui-actionFieldLayout-input' )
7620 .append( this.fieldWidget.$element );
7621 this.$field
7622 .addClass( 'oo-ui-actionFieldLayout' )
7623 .append( this.$input, this.$button );
7624 };
7625
7626 /* Setup */
7627
7628 OO.inheritClass( OO.ui.ActionFieldLayout, OO.ui.FieldLayout );
7629
7630 /**
7631 * Layout made of a fieldset and optional legend.
7632 *
7633 * Just add OO.ui.FieldLayout items.
7634 *
7635 * @class
7636 * @extends OO.ui.Layout
7637 * @mixins OO.ui.IconElement
7638 * @mixins OO.ui.LabelElement
7639 * @mixins OO.ui.GroupElement
7640 *
7641 * @constructor
7642 * @param {Object} [config] Configuration options
7643 * @cfg {OO.ui.FieldLayout[]} [items] Items to add
7644 */
7645 OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
7646 // Configuration initialization
7647 config = config || {};
7648
7649 // Parent constructor
7650 OO.ui.FieldsetLayout.super.call( this, config );
7651
7652 // Mixin constructors
7653 OO.ui.IconElement.call( this, config );
7654 OO.ui.LabelElement.call( this, config );
7655 OO.ui.GroupElement.call( this, config );
7656
7657 if ( config.help ) {
7658 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
7659 classes: [ 'oo-ui-fieldsetLayout-help' ],
7660 framed: false,
7661 icon: 'info'
7662 } );
7663
7664 this.popupButtonWidget.getPopup().$body.append(
7665 $( '<div>' )
7666 .text( config.help )
7667 .addClass( 'oo-ui-fieldsetLayout-help-content' )
7668 );
7669 this.$help = this.popupButtonWidget.$element;
7670 } else {
7671 this.$help = $( [] );
7672 }
7673
7674 // Initialization
7675 this.$element
7676 .addClass( 'oo-ui-fieldsetLayout' )
7677 .prepend( this.$help, this.$icon, this.$label, this.$group );
7678 if ( Array.isArray( config.items ) ) {
7679 this.addItems( config.items );
7680 }
7681 };
7682
7683 /* Setup */
7684
7685 OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
7686 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.IconElement );
7687 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.LabelElement );
7688 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.GroupElement );
7689
7690 /**
7691 * Layout with an HTML form.
7692 *
7693 * @class
7694 * @extends OO.ui.Layout
7695 * @mixins OO.ui.GroupElement
7696 *
7697 * @constructor
7698 * @param {Object} [config] Configuration options
7699 * @cfg {string} [method] HTML form `method` attribute
7700 * @cfg {string} [action] HTML form `action` attribute
7701 * @cfg {string} [enctype] HTML form `enctype` attribute
7702 * @cfg {OO.ui.FieldsetLayout[]} [items] Items to add
7703 */
7704 OO.ui.FormLayout = function OoUiFormLayout( config ) {
7705 // Configuration initialization
7706 config = config || {};
7707
7708 // Parent constructor
7709 OO.ui.FormLayout.super.call( this, config );
7710
7711 // Mixin constructors
7712 OO.ui.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
7713
7714 // Events
7715 this.$element.on( 'submit', this.onFormSubmit.bind( this ) );
7716
7717 // Initialization
7718 this.$element
7719 .addClass( 'oo-ui-formLayout' )
7720 .attr( {
7721 method: config.method,
7722 action: config.action,
7723 enctype: config.enctype
7724 } );
7725 if ( Array.isArray( config.items ) ) {
7726 this.addItems( config.items );
7727 }
7728 };
7729
7730 /* Setup */
7731
7732 OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout );
7733 OO.mixinClass( OO.ui.FormLayout, OO.ui.GroupElement );
7734
7735 /* Events */
7736
7737 /**
7738 * @event submit
7739 */
7740
7741 /* Static Properties */
7742
7743 OO.ui.FormLayout.static.tagName = 'form';
7744
7745 /* Methods */
7746
7747 /**
7748 * Handle form submit events.
7749 *
7750 * @param {jQuery.Event} e Submit event
7751 * @fires submit
7752 */
7753 OO.ui.FormLayout.prototype.onFormSubmit = function () {
7754 this.emit( 'submit' );
7755 return false;
7756 };
7757
7758 /**
7759 * Layout with a content and menu area.
7760 *
7761 * The menu area can be positioned at the top, after, bottom or before. The content area will fill
7762 * all remaining space.
7763 *
7764 * @class
7765 * @extends OO.ui.Layout
7766 *
7767 * @constructor
7768 * @param {Object} [config] Configuration options
7769 * @cfg {number|string} [menuSize='18em'] Size of menu in pixels or any CSS unit
7770 * @cfg {boolean} [showMenu=true] Show menu
7771 * @cfg {string} [position='before'] Position of menu, either `top`, `after`, `bottom` or `before`
7772 * @cfg {boolean} [collapse] Collapse the menu out of view
7773 */
7774 OO.ui.MenuLayout = function OoUiMenuLayout( config ) {
7775 var positions = this.constructor.static.menuPositions;
7776
7777 // Configuration initialization
7778 config = config || {};
7779
7780 // Parent constructor
7781 OO.ui.MenuLayout.super.call( this, config );
7782
7783 // Properties
7784 this.showMenu = config.showMenu !== false;
7785 this.menuSize = config.menuSize || '18em';
7786 this.menuPosition = positions[ config.menuPosition ] || positions.before;
7787
7788 /**
7789 * Menu DOM node
7790 *
7791 * @property {jQuery}
7792 */
7793 this.$menu = $( '<div>' );
7794 /**
7795 * Content DOM node
7796 *
7797 * @property {jQuery}
7798 */
7799 this.$content = $( '<div>' );
7800
7801 // Initialization
7802 this.toggleMenu( this.showMenu );
7803 this.updateSizes();
7804 this.$menu
7805 .addClass( 'oo-ui-menuLayout-menu' )
7806 .css( this.menuPosition.sizeProperty, this.menuSize );
7807 this.$content.addClass( 'oo-ui-menuLayout-content' );
7808 this.$element
7809 .addClass( 'oo-ui-menuLayout ' + this.menuPosition.className )
7810 .append( this.$content, this.$menu );
7811 };
7812
7813 /* Setup */
7814
7815 OO.inheritClass( OO.ui.MenuLayout, OO.ui.Layout );
7816
7817 /* Static Properties */
7818
7819 OO.ui.MenuLayout.static.menuPositions = {
7820 top: {
7821 sizeProperty: 'height',
7822 className: 'oo-ui-menuLayout-top'
7823 },
7824 after: {
7825 sizeProperty: 'width',
7826 className: 'oo-ui-menuLayout-after'
7827 },
7828 bottom: {
7829 sizeProperty: 'height',
7830 className: 'oo-ui-menuLayout-bottom'
7831 },
7832 before: {
7833 sizeProperty: 'width',
7834 className: 'oo-ui-menuLayout-before'
7835 }
7836 };
7837
7838 /* Methods */
7839
7840 /**
7841 * Toggle menu.
7842 *
7843 * @param {boolean} showMenu Show menu, omit to toggle
7844 * @chainable
7845 */
7846 OO.ui.MenuLayout.prototype.toggleMenu = function ( showMenu ) {
7847 showMenu = showMenu === undefined ? !this.showMenu : !!showMenu;
7848
7849 if ( this.showMenu !== showMenu ) {
7850 this.showMenu = showMenu;
7851 this.updateSizes();
7852 }
7853
7854 return this;
7855 };
7856
7857 /**
7858 * Check if menu is visible
7859 *
7860 * @return {boolean} Menu is visible
7861 */
7862 OO.ui.MenuLayout.prototype.isMenuVisible = function () {
7863 return this.showMenu;
7864 };
7865
7866 /**
7867 * Set menu size.
7868 *
7869 * @param {number|string} size Size of menu in pixels or any CSS unit
7870 * @chainable
7871 */
7872 OO.ui.MenuLayout.prototype.setMenuSize = function ( size ) {
7873 this.menuSize = size;
7874 this.updateSizes();
7875
7876 return this;
7877 };
7878
7879 /**
7880 * Update menu and content CSS based on current menu size and visibility
7881 *
7882 * This method is called internally when size or position is changed.
7883 */
7884 OO.ui.MenuLayout.prototype.updateSizes = function () {
7885 if ( this.showMenu ) {
7886 this.$menu
7887 .css( this.menuPosition.sizeProperty, this.menuSize )
7888 .css( 'overflow', '' );
7889 // Set offsets on all sides. CSS resets all but one with
7890 // 'important' rules so directionality flips are supported
7891 this.$content.css( {
7892 top: this.menuSize,
7893 right: this.menuSize,
7894 bottom: this.menuSize,
7895 left: this.menuSize
7896 } );
7897 } else {
7898 this.$menu
7899 .css( this.menuPosition.sizeProperty, 0 )
7900 .css( 'overflow', 'hidden' );
7901 this.$content.css( {
7902 top: 0,
7903 right: 0,
7904 bottom: 0,
7905 left: 0
7906 } );
7907 }
7908 };
7909
7910 /**
7911 * Get menu size.
7912 *
7913 * @return {number|string} Menu size
7914 */
7915 OO.ui.MenuLayout.prototype.getMenuSize = function () {
7916 return this.menuSize;
7917 };
7918
7919 /**
7920 * Set menu position.
7921 *
7922 * @param {string} position Position of menu, either `top`, `after`, `bottom` or `before`
7923 * @throws {Error} If position value is not supported
7924 * @chainable
7925 */
7926 OO.ui.MenuLayout.prototype.setMenuPosition = function ( position ) {
7927 var positions = this.constructor.static.menuPositions;
7928
7929 if ( !positions[ position ] ) {
7930 throw new Error( 'Cannot set position; unsupported position value: ' + position );
7931 }
7932
7933 this.$menu.css( this.menuPosition.sizeProperty, '' );
7934 this.$element.removeClass( this.menuPosition.className );
7935
7936 this.menuPosition = positions[ position ];
7937
7938 this.updateSizes();
7939 this.$element.addClass( this.menuPosition.className );
7940
7941 return this;
7942 };
7943
7944 /**
7945 * Get menu position.
7946 *
7947 * @return {string} Menu position
7948 */
7949 OO.ui.MenuLayout.prototype.getMenuPosition = function () {
7950 return this.menuPosition;
7951 };
7952
7953 /**
7954 * Layout containing a series of pages.
7955 *
7956 * @class
7957 * @extends OO.ui.MenuLayout
7958 *
7959 * @constructor
7960 * @param {Object} [config] Configuration options
7961 * @cfg {boolean} [continuous=false] Show all pages, one after another
7962 * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when changing to a page
7963 * @cfg {boolean} [outlined=false] Show an outline
7964 * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
7965 */
7966 OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
7967 // Configuration initialization
7968 config = config || {};
7969
7970 // Parent constructor
7971 OO.ui.BookletLayout.super.call( this, config );
7972
7973 // Properties
7974 this.currentPageName = null;
7975 this.pages = {};
7976 this.ignoreFocus = false;
7977 this.stackLayout = new OO.ui.StackLayout( { continuous: !!config.continuous } );
7978 this.$content.append( this.stackLayout.$element );
7979 this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
7980 this.outlineVisible = false;
7981 this.outlined = !!config.outlined;
7982 if ( this.outlined ) {
7983 this.editable = !!config.editable;
7984 this.outlineControlsWidget = null;
7985 this.outlineSelectWidget = new OO.ui.OutlineSelectWidget();
7986 this.outlinePanel = new OO.ui.PanelLayout( { scrollable: true } );
7987 this.$menu.append( this.outlinePanel.$element );
7988 this.outlineVisible = true;
7989 if ( this.editable ) {
7990 this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
7991 this.outlineSelectWidget
7992 );
7993 }
7994 }
7995 this.toggleMenu( this.outlined );
7996
7997 // Events
7998 this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
7999 if ( this.outlined ) {
8000 this.outlineSelectWidget.connect( this, { select: 'onOutlineSelectWidgetSelect' } );
8001 }
8002 if ( this.autoFocus ) {
8003 // Event 'focus' does not bubble, but 'focusin' does
8004 this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
8005 }
8006
8007 // Initialization
8008 this.$element.addClass( 'oo-ui-bookletLayout' );
8009 this.stackLayout.$element.addClass( 'oo-ui-bookletLayout-stackLayout' );
8010 if ( this.outlined ) {
8011 this.outlinePanel.$element
8012 .addClass( 'oo-ui-bookletLayout-outlinePanel' )
8013 .append( this.outlineSelectWidget.$element );
8014 if ( this.editable ) {
8015 this.outlinePanel.$element
8016 .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
8017 .append( this.outlineControlsWidget.$element );
8018 }
8019 }
8020 };
8021
8022 /* Setup */
8023
8024 OO.inheritClass( OO.ui.BookletLayout, OO.ui.MenuLayout );
8025
8026 /* Events */
8027
8028 /**
8029 * @event set
8030 * @param {OO.ui.PageLayout} page Current page
8031 */
8032
8033 /**
8034 * @event add
8035 * @param {OO.ui.PageLayout[]} page Added pages
8036 * @param {number} index Index pages were added at
8037 */
8038
8039 /**
8040 * @event remove
8041 * @param {OO.ui.PageLayout[]} pages Removed pages
8042 */
8043
8044 /* Methods */
8045
8046 /**
8047 * Handle stack layout focus.
8048 *
8049 * @param {jQuery.Event} e Focusin event
8050 */
8051 OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) {
8052 var name, $target;
8053
8054 // Find the page that an element was focused within
8055 $target = $( e.target ).closest( '.oo-ui-pageLayout' );
8056 for ( name in this.pages ) {
8057 // Check for page match, exclude current page to find only page changes
8058 if ( this.pages[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentPageName ) {
8059 this.setPage( name );
8060 break;
8061 }
8062 }
8063 };
8064
8065 /**
8066 * Handle stack layout set events.
8067 *
8068 * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
8069 */
8070 OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
8071 var layout = this;
8072 if ( page ) {
8073 page.scrollElementIntoView( { complete: function () {
8074 if ( layout.autoFocus ) {
8075 layout.focus();
8076 }
8077 } } );
8078 }
8079 };
8080
8081 /**
8082 * Focus the first input in the current page.
8083 *
8084 * If no page is selected, the first selectable page will be selected.
8085 * If the focus is already in an element on the current page, nothing will happen.
8086 */
8087 OO.ui.BookletLayout.prototype.focus = function () {
8088 var $input, page = this.stackLayout.getCurrentItem();
8089 if ( !page && this.outlined ) {
8090 this.selectFirstSelectablePage();
8091 page = this.stackLayout.getCurrentItem();
8092 }
8093 if ( !page ) {
8094 return;
8095 }
8096 // Only change the focus if is not already in the current page
8097 if ( !page.$element.find( ':focus' ).length ) {
8098 $input = page.$element.find( ':input:first' );
8099 if ( $input.length ) {
8100 $input[ 0 ].focus();
8101 }
8102 }
8103 };
8104
8105 /**
8106 * Handle outline widget select events.
8107 *
8108 * @param {OO.ui.OptionWidget|null} item Selected item
8109 */
8110 OO.ui.BookletLayout.prototype.onOutlineSelectWidgetSelect = function ( item ) {
8111 if ( item ) {
8112 this.setPage( item.getData() );
8113 }
8114 };
8115
8116 /**
8117 * Check if booklet has an outline.
8118 *
8119 * @return {boolean}
8120 */
8121 OO.ui.BookletLayout.prototype.isOutlined = function () {
8122 return this.outlined;
8123 };
8124
8125 /**
8126 * Check if booklet has editing controls.
8127 *
8128 * @return {boolean}
8129 */
8130 OO.ui.BookletLayout.prototype.isEditable = function () {
8131 return this.editable;
8132 };
8133
8134 /**
8135 * Check if booklet has a visible outline.
8136 *
8137 * @return {boolean}
8138 */
8139 OO.ui.BookletLayout.prototype.isOutlineVisible = function () {
8140 return this.outlined && this.outlineVisible;
8141 };
8142
8143 /**
8144 * Hide or show the outline.
8145 *
8146 * @param {boolean} [show] Show outline, omit to invert current state
8147 * @chainable
8148 */
8149 OO.ui.BookletLayout.prototype.toggleOutline = function ( show ) {
8150 if ( this.outlined ) {
8151 show = show === undefined ? !this.outlineVisible : !!show;
8152 this.outlineVisible = show;
8153 this.toggleMenu( show );
8154 }
8155
8156 return this;
8157 };
8158
8159 /**
8160 * Get the outline widget.
8161 *
8162 * @param {OO.ui.PageLayout} page Page to be selected
8163 * @return {OO.ui.PageLayout|null} Closest page to another
8164 */
8165 OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) {
8166 var next, prev, level,
8167 pages = this.stackLayout.getItems(),
8168 index = $.inArray( page, pages );
8169
8170 if ( index !== -1 ) {
8171 next = pages[ index + 1 ];
8172 prev = pages[ index - 1 ];
8173 // Prefer adjacent pages at the same level
8174 if ( this.outlined ) {
8175 level = this.outlineSelectWidget.getItemFromData( page.getName() ).getLevel();
8176 if (
8177 prev &&
8178 level === this.outlineSelectWidget.getItemFromData( prev.getName() ).getLevel()
8179 ) {
8180 return prev;
8181 }
8182 if (
8183 next &&
8184 level === this.outlineSelectWidget.getItemFromData( next.getName() ).getLevel()
8185 ) {
8186 return next;
8187 }
8188 }
8189 }
8190 return prev || next || null;
8191 };
8192
8193 /**
8194 * Get the outline widget.
8195 *
8196 * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if booklet has no outline
8197 */
8198 OO.ui.BookletLayout.prototype.getOutline = function () {
8199 return this.outlineSelectWidget;
8200 };
8201
8202 /**
8203 * Get the outline controls widget. If the outline is not editable, null is returned.
8204 *
8205 * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
8206 */
8207 OO.ui.BookletLayout.prototype.getOutlineControls = function () {
8208 return this.outlineControlsWidget;
8209 };
8210
8211 /**
8212 * Get a page by name.
8213 *
8214 * @param {string} name Symbolic name of page
8215 * @return {OO.ui.PageLayout|undefined} Page, if found
8216 */
8217 OO.ui.BookletLayout.prototype.getPage = function ( name ) {
8218 return this.pages[ name ];
8219 };
8220
8221 /**
8222 * Get the current page
8223 *
8224 * @return {OO.ui.PageLayout|undefined} Current page, if found
8225 */
8226 OO.ui.BookletLayout.prototype.getCurrentPage = function () {
8227 var name = this.getCurrentPageName();
8228 return name ? this.getPage( name ) : undefined;
8229 };
8230
8231 /**
8232 * Get the current page name.
8233 *
8234 * @return {string|null} Current page name
8235 */
8236 OO.ui.BookletLayout.prototype.getCurrentPageName = function () {
8237 return this.currentPageName;
8238 };
8239
8240 /**
8241 * Add a page to the layout.
8242 *
8243 * When pages are added with the same names as existing pages, the existing pages will be
8244 * automatically removed before the new pages are added.
8245 *
8246 * @param {OO.ui.PageLayout[]} pages Pages to add
8247 * @param {number} index Index to insert pages after
8248 * @fires add
8249 * @chainable
8250 */
8251 OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) {
8252 var i, len, name, page, item, currentIndex,
8253 stackLayoutPages = this.stackLayout.getItems(),
8254 remove = [],
8255 items = [];
8256
8257 // Remove pages with same names
8258 for ( i = 0, len = pages.length; i < len; i++ ) {
8259 page = pages[ i ];
8260 name = page.getName();
8261
8262 if ( Object.prototype.hasOwnProperty.call( this.pages, name ) ) {
8263 // Correct the insertion index
8264 currentIndex = $.inArray( this.pages[ name ], stackLayoutPages );
8265 if ( currentIndex !== -1 && currentIndex + 1 < index ) {
8266 index--;
8267 }
8268 remove.push( this.pages[ name ] );
8269 }
8270 }
8271 if ( remove.length ) {
8272 this.removePages( remove );
8273 }
8274
8275 // Add new pages
8276 for ( i = 0, len = pages.length; i < len; i++ ) {
8277 page = pages[ i ];
8278 name = page.getName();
8279 this.pages[ page.getName() ] = page;
8280 if ( this.outlined ) {
8281 item = new OO.ui.OutlineOptionWidget( { data: name } );
8282 page.setOutlineItem( item );
8283 items.push( item );
8284 }
8285 }
8286
8287 if ( this.outlined && items.length ) {
8288 this.outlineSelectWidget.addItems( items, index );
8289 this.selectFirstSelectablePage();
8290 }
8291 this.stackLayout.addItems( pages, index );
8292 this.emit( 'add', pages, index );
8293
8294 return this;
8295 };
8296
8297 /**
8298 * Remove a page from the layout.
8299 *
8300 * @fires remove
8301 * @chainable
8302 */
8303 OO.ui.BookletLayout.prototype.removePages = function ( pages ) {
8304 var i, len, name, page,
8305 items = [];
8306
8307 for ( i = 0, len = pages.length; i < len; i++ ) {
8308 page = pages[ i ];
8309 name = page.getName();
8310 delete this.pages[ name ];
8311 if ( this.outlined ) {
8312 items.push( this.outlineSelectWidget.getItemFromData( name ) );
8313 page.setOutlineItem( null );
8314 }
8315 }
8316 if ( this.outlined && items.length ) {
8317 this.outlineSelectWidget.removeItems( items );
8318 this.selectFirstSelectablePage();
8319 }
8320 this.stackLayout.removeItems( pages );
8321 this.emit( 'remove', pages );
8322
8323 return this;
8324 };
8325
8326 /**
8327 * Clear all pages from the layout.
8328 *
8329 * @fires remove
8330 * @chainable
8331 */
8332 OO.ui.BookletLayout.prototype.clearPages = function () {
8333 var i, len,
8334 pages = this.stackLayout.getItems();
8335
8336 this.pages = {};
8337 this.currentPageName = null;
8338 if ( this.outlined ) {
8339 this.outlineSelectWidget.clearItems();
8340 for ( i = 0, len = pages.length; i < len; i++ ) {
8341 pages[ i ].setOutlineItem( null );
8342 }
8343 }
8344 this.stackLayout.clearItems();
8345
8346 this.emit( 'remove', pages );
8347
8348 return this;
8349 };
8350
8351 /**
8352 * Set the current page by name.
8353 *
8354 * @fires set
8355 * @param {string} name Symbolic name of page
8356 */
8357 OO.ui.BookletLayout.prototype.setPage = function ( name ) {
8358 var selectedItem,
8359 $focused,
8360 page = this.pages[ name ];
8361
8362 if ( name !== this.currentPageName ) {
8363 if ( this.outlined ) {
8364 selectedItem = this.outlineSelectWidget.getSelectedItem();
8365 if ( selectedItem && selectedItem.getData() !== name ) {
8366 this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getItemFromData( name ) );
8367 }
8368 }
8369 if ( page ) {
8370 if ( this.currentPageName && this.pages[ this.currentPageName ] ) {
8371 this.pages[ this.currentPageName ].setActive( false );
8372 // Blur anything focused if the next page doesn't have anything focusable - this
8373 // is not needed if the next page has something focusable because once it is focused
8374 // this blur happens automatically
8375 if ( this.autoFocus && !page.$element.find( ':input' ).length ) {
8376 $focused = this.pages[ this.currentPageName ].$element.find( ':focus' );
8377 if ( $focused.length ) {
8378 $focused[ 0 ].blur();
8379 }
8380 }
8381 }
8382 this.currentPageName = name;
8383 this.stackLayout.setItem( page );
8384 page.setActive( true );
8385 this.emit( 'set', page );
8386 }
8387 }
8388 };
8389
8390 /**
8391 * Select the first selectable page.
8392 *
8393 * @chainable
8394 */
8395 OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () {
8396 if ( !this.outlineSelectWidget.getSelectedItem() ) {
8397 this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getFirstSelectableItem() );
8398 }
8399
8400 return this;
8401 };
8402
8403 /**
8404 * Layout that expands to cover the entire area of its parent, with optional scrolling and padding.
8405 *
8406 * @class
8407 * @extends OO.ui.Layout
8408 *
8409 * @constructor
8410 * @param {Object} [config] Configuration options
8411 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
8412 * @cfg {boolean} [padded=false] Pad the content from the edges
8413 * @cfg {boolean} [expanded=true] Expand size to fill the entire parent element
8414 */
8415 OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
8416 // Configuration initialization
8417 config = $.extend( {
8418 scrollable: false,
8419 padded: false,
8420 expanded: true
8421 }, config );
8422
8423 // Parent constructor
8424 OO.ui.PanelLayout.super.call( this, config );
8425
8426 // Initialization
8427 this.$element.addClass( 'oo-ui-panelLayout' );
8428 if ( config.scrollable ) {
8429 this.$element.addClass( 'oo-ui-panelLayout-scrollable' );
8430 }
8431 if ( config.padded ) {
8432 this.$element.addClass( 'oo-ui-panelLayout-padded' );
8433 }
8434 if ( config.expanded ) {
8435 this.$element.addClass( 'oo-ui-panelLayout-expanded' );
8436 }
8437 };
8438
8439 /* Setup */
8440
8441 OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
8442
8443 /**
8444 * Page within an booklet layout.
8445 *
8446 * @class
8447 * @extends OO.ui.PanelLayout
8448 *
8449 * @constructor
8450 * @param {string} name Unique symbolic name of page
8451 * @param {Object} [config] Configuration options
8452 */
8453 OO.ui.PageLayout = function OoUiPageLayout( name, config ) {
8454 // Allow passing positional parameters inside the config object
8455 if ( OO.isPlainObject( name ) && config === undefined ) {
8456 config = name;
8457 name = config.name;
8458 }
8459
8460 // Configuration initialization
8461 config = $.extend( { scrollable: true }, config );
8462
8463 // Parent constructor
8464 OO.ui.PageLayout.super.call( this, config );
8465
8466 // Properties
8467 this.name = name;
8468 this.outlineItem = null;
8469 this.active = false;
8470
8471 // Initialization
8472 this.$element.addClass( 'oo-ui-pageLayout' );
8473 };
8474
8475 /* Setup */
8476
8477 OO.inheritClass( OO.ui.PageLayout, OO.ui.PanelLayout );
8478
8479 /* Events */
8480
8481 /**
8482 * @event active
8483 * @param {boolean} active Page is active
8484 */
8485
8486 /* Methods */
8487
8488 /**
8489 * Get page name.
8490 *
8491 * @return {string} Symbolic name of page
8492 */
8493 OO.ui.PageLayout.prototype.getName = function () {
8494 return this.name;
8495 };
8496
8497 /**
8498 * Check if page is active.
8499 *
8500 * @return {boolean} Page is active
8501 */
8502 OO.ui.PageLayout.prototype.isActive = function () {
8503 return this.active;
8504 };
8505
8506 /**
8507 * Get outline item.
8508 *
8509 * @return {OO.ui.OutlineOptionWidget|null} Outline item widget
8510 */
8511 OO.ui.PageLayout.prototype.getOutlineItem = function () {
8512 return this.outlineItem;
8513 };
8514
8515 /**
8516 * Set outline item.
8517 *
8518 * @localdoc Subclasses should override #setupOutlineItem instead of this method to adjust the
8519 * outline item as desired; this method is called for setting (with an object) and unsetting
8520 * (with null) and overriding methods would have to check the value of `outlineItem` to avoid
8521 * operating on null instead of an OO.ui.OutlineOptionWidget object.
8522 *
8523 * @param {OO.ui.OutlineOptionWidget|null} outlineItem Outline item widget, null to clear
8524 * @chainable
8525 */
8526 OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) {
8527 this.outlineItem = outlineItem || null;
8528 if ( outlineItem ) {
8529 this.setupOutlineItem();
8530 }
8531 return this;
8532 };
8533
8534 /**
8535 * Setup outline item.
8536 *
8537 * @localdoc Subclasses should override this method to adjust the outline item as desired.
8538 *
8539 * @param {OO.ui.OutlineOptionWidget} outlineItem Outline item widget to setup
8540 * @chainable
8541 */
8542 OO.ui.PageLayout.prototype.setupOutlineItem = function () {
8543 return this;
8544 };
8545
8546 /**
8547 * Set page active state.
8548 *
8549 * @param {boolean} Page is active
8550 * @fires active
8551 */
8552 OO.ui.PageLayout.prototype.setActive = function ( active ) {
8553 active = !!active;
8554
8555 if ( active !== this.active ) {
8556 this.active = active;
8557 this.$element.toggleClass( 'oo-ui-pageLayout-active', active );
8558 this.emit( 'active', this.active );
8559 }
8560 };
8561
8562 /**
8563 * Layout containing a series of mutually exclusive pages.
8564 *
8565 * @class
8566 * @extends OO.ui.PanelLayout
8567 * @mixins OO.ui.GroupElement
8568 *
8569 * @constructor
8570 * @param {Object} [config] Configuration options
8571 * @cfg {boolean} [continuous=false] Show all pages, one after another
8572 * @cfg {OO.ui.Layout[]} [items] Layouts to add
8573 */
8574 OO.ui.StackLayout = function OoUiStackLayout( config ) {
8575 // Configuration initialization
8576 config = $.extend( { scrollable: true }, config );
8577
8578 // Parent constructor
8579 OO.ui.StackLayout.super.call( this, config );
8580
8581 // Mixin constructors
8582 OO.ui.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
8583
8584 // Properties
8585 this.currentItem = null;
8586 this.continuous = !!config.continuous;
8587
8588 // Initialization
8589 this.$element.addClass( 'oo-ui-stackLayout' );
8590 if ( this.continuous ) {
8591 this.$element.addClass( 'oo-ui-stackLayout-continuous' );
8592 }
8593 if ( Array.isArray( config.items ) ) {
8594 this.addItems( config.items );
8595 }
8596 };
8597
8598 /* Setup */
8599
8600 OO.inheritClass( OO.ui.StackLayout, OO.ui.PanelLayout );
8601 OO.mixinClass( OO.ui.StackLayout, OO.ui.GroupElement );
8602
8603 /* Events */
8604
8605 /**
8606 * @event set
8607 * @param {OO.ui.Layout|null} item Current item or null if there is no longer a layout shown
8608 */
8609
8610 /* Methods */
8611
8612 /**
8613 * Get the current item.
8614 *
8615 * @return {OO.ui.Layout|null}
8616 */
8617 OO.ui.StackLayout.prototype.getCurrentItem = function () {
8618 return this.currentItem;
8619 };
8620
8621 /**
8622 * Unset the current item.
8623 *
8624 * @private
8625 * @param {OO.ui.StackLayout} layout
8626 * @fires set
8627 */
8628 OO.ui.StackLayout.prototype.unsetCurrentItem = function () {
8629 var prevItem = this.currentItem;
8630 if ( prevItem === null ) {
8631 return;
8632 }
8633
8634 this.currentItem = null;
8635 this.emit( 'set', null );
8636 };
8637
8638 /**
8639 * Add items.
8640 *
8641 * Adding an existing item (by value) will move it.
8642 *
8643 * @param {OO.ui.Layout[]} items Items to add
8644 * @param {number} [index] Index to insert items after
8645 * @chainable
8646 */
8647 OO.ui.StackLayout.prototype.addItems = function ( items, index ) {
8648 // Update the visibility
8649 this.updateHiddenState( items, this.currentItem );
8650
8651 // Mixin method
8652 OO.ui.GroupElement.prototype.addItems.call( this, items, index );
8653
8654 if ( !this.currentItem && items.length ) {
8655 this.setItem( items[ 0 ] );
8656 }
8657
8658 return this;
8659 };
8660
8661 /**
8662 * Remove items.
8663 *
8664 * Items will be detached, not removed, so they can be used later.
8665 *
8666 * @param {OO.ui.Layout[]} items Items to remove
8667 * @chainable
8668 * @fires set
8669 */
8670 OO.ui.StackLayout.prototype.removeItems = function ( items ) {
8671 // Mixin method
8672 OO.ui.GroupElement.prototype.removeItems.call( this, items );
8673
8674 if ( $.inArray( this.currentItem, items ) !== -1 ) {
8675 if ( this.items.length ) {
8676 this.setItem( this.items[ 0 ] );
8677 } else {
8678 this.unsetCurrentItem();
8679 }
8680 }
8681
8682 return this;
8683 };
8684
8685 /**
8686 * Clear all items.
8687 *
8688 * Items will be detached, not removed, so they can be used later.
8689 *
8690 * @chainable
8691 * @fires set
8692 */
8693 OO.ui.StackLayout.prototype.clearItems = function () {
8694 this.unsetCurrentItem();
8695 OO.ui.GroupElement.prototype.clearItems.call( this );
8696
8697 return this;
8698 };
8699
8700 /**
8701 * Show item.
8702 *
8703 * Any currently shown item will be hidden.
8704 *
8705 * FIXME: If the passed item to show has not been added in the items list, then
8706 * this method drops it and unsets the current item.
8707 *
8708 * @param {OO.ui.Layout} item Item to show
8709 * @chainable
8710 * @fires set
8711 */
8712 OO.ui.StackLayout.prototype.setItem = function ( item ) {
8713 if ( item !== this.currentItem ) {
8714 this.updateHiddenState( this.items, item );
8715
8716 if ( $.inArray( item, this.items ) !== -1 ) {
8717 this.currentItem = item;
8718 this.emit( 'set', item );
8719 } else {
8720 this.unsetCurrentItem();
8721 }
8722 }
8723
8724 return this;
8725 };
8726
8727 /**
8728 * Update the visibility of all items in case of non-continuous view.
8729 *
8730 * Ensure all items are hidden except for the selected one.
8731 * This method does nothing when the stack is continuous.
8732 *
8733 * @param {OO.ui.Layout[]} items Item list iterate over
8734 * @param {OO.ui.Layout} [selectedItem] Selected item to show
8735 */
8736 OO.ui.StackLayout.prototype.updateHiddenState = function ( items, selectedItem ) {
8737 var i, len;
8738
8739 if ( !this.continuous ) {
8740 for ( i = 0, len = items.length; i < len; i++ ) {
8741 if ( !selectedItem || selectedItem !== items[ i ] ) {
8742 items[ i ].$element.addClass( 'oo-ui-element-hidden' );
8743 }
8744 }
8745 if ( selectedItem ) {
8746 selectedItem.$element.removeClass( 'oo-ui-element-hidden' );
8747 }
8748 }
8749 };
8750
8751 /**
8752 * Horizontal bar layout of tools as icon buttons.
8753 *
8754 * @class
8755 * @extends OO.ui.ToolGroup
8756 *
8757 * @constructor
8758 * @param {OO.ui.Toolbar} toolbar
8759 * @param {Object} [config] Configuration options
8760 */
8761 OO.ui.BarToolGroup = function OoUiBarToolGroup( toolbar, config ) {
8762 // Allow passing positional parameters inside the config object
8763 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
8764 config = toolbar;
8765 toolbar = config.toolbar;
8766 }
8767
8768 // Parent constructor
8769 OO.ui.BarToolGroup.super.call( this, toolbar, config );
8770
8771 // Initialization
8772 this.$element.addClass( 'oo-ui-barToolGroup' );
8773 };
8774
8775 /* Setup */
8776
8777 OO.inheritClass( OO.ui.BarToolGroup, OO.ui.ToolGroup );
8778
8779 /* Static Properties */
8780
8781 OO.ui.BarToolGroup.static.titleTooltips = true;
8782
8783 OO.ui.BarToolGroup.static.accelTooltips = true;
8784
8785 OO.ui.BarToolGroup.static.name = 'bar';
8786
8787 /**
8788 * Popup list of tools with an icon and optional label.
8789 *
8790 * @abstract
8791 * @class
8792 * @extends OO.ui.ToolGroup
8793 * @mixins OO.ui.IconElement
8794 * @mixins OO.ui.IndicatorElement
8795 * @mixins OO.ui.LabelElement
8796 * @mixins OO.ui.TitledElement
8797 * @mixins OO.ui.ClippableElement
8798 *
8799 * @constructor
8800 * @param {OO.ui.Toolbar} toolbar
8801 * @param {Object} [config] Configuration options
8802 * @cfg {string} [header] Text to display at the top of the pop-up
8803 */
8804 OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
8805 // Allow passing positional parameters inside the config object
8806 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
8807 config = toolbar;
8808 toolbar = config.toolbar;
8809 }
8810
8811 // Configuration initialization
8812 config = config || {};
8813
8814 // Parent constructor
8815 OO.ui.PopupToolGroup.super.call( this, toolbar, config );
8816
8817 // Mixin constructors
8818 OO.ui.IconElement.call( this, config );
8819 OO.ui.IndicatorElement.call( this, config );
8820 OO.ui.LabelElement.call( this, config );
8821 OO.ui.TitledElement.call( this, config );
8822 OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
8823
8824 // Properties
8825 this.active = false;
8826 this.dragging = false;
8827 this.onBlurHandler = this.onBlur.bind( this );
8828 this.$handle = $( '<span>' );
8829
8830 // Events
8831 this.$handle.on( {
8832 'mousedown touchstart': this.onHandlePointerDown.bind( this ),
8833 'mouseup touchend': this.onHandlePointerUp.bind( this )
8834 } );
8835
8836 // Initialization
8837 this.$handle
8838 .addClass( 'oo-ui-popupToolGroup-handle' )
8839 .append( this.$icon, this.$label, this.$indicator );
8840 // If the pop-up should have a header, add it to the top of the toolGroup.
8841 // Note: If this feature is useful for other widgets, we could abstract it into an
8842 // OO.ui.HeaderedElement mixin constructor.
8843 if ( config.header !== undefined ) {
8844 this.$group
8845 .prepend( $( '<span>' )
8846 .addClass( 'oo-ui-popupToolGroup-header' )
8847 .text( config.header )
8848 );
8849 }
8850 this.$element
8851 .addClass( 'oo-ui-popupToolGroup' )
8852 .prepend( this.$handle );
8853 };
8854
8855 /* Setup */
8856
8857 OO.inheritClass( OO.ui.PopupToolGroup, OO.ui.ToolGroup );
8858 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IconElement );
8859 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IndicatorElement );
8860 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.LabelElement );
8861 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.TitledElement );
8862 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.ClippableElement );
8863
8864 /* Static Properties */
8865
8866 /* Methods */
8867
8868 /**
8869 * @inheritdoc
8870 */
8871 OO.ui.PopupToolGroup.prototype.setDisabled = function () {
8872 // Parent method
8873 OO.ui.PopupToolGroup.super.prototype.setDisabled.apply( this, arguments );
8874
8875 if ( this.isDisabled() && this.isElementAttached() ) {
8876 this.setActive( false );
8877 }
8878 };
8879
8880 /**
8881 * Handle focus being lost.
8882 *
8883 * The event is actually generated from a mouseup, so it is not a normal blur event object.
8884 *
8885 * @param {jQuery.Event} e Mouse up event
8886 */
8887 OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) {
8888 // Only deactivate when clicking outside the dropdown element
8889 if ( $( e.target ).closest( '.oo-ui-popupToolGroup' )[ 0 ] !== this.$element[ 0 ] ) {
8890 this.setActive( false );
8891 }
8892 };
8893
8894 /**
8895 * @inheritdoc
8896 */
8897 OO.ui.PopupToolGroup.prototype.onPointerUp = function ( e ) {
8898 // e.which is 0 for touch events, 1 for left mouse button
8899 // Only close toolgroup when a tool was actually selected
8900 // FIXME: this duplicates logic from the parent class
8901 if ( !this.isDisabled() && e.which <= 1 && this.pressed && this.pressed === this.getTargetTool( e ) ) {
8902 this.setActive( false );
8903 }
8904 return OO.ui.PopupToolGroup.super.prototype.onPointerUp.call( this, e );
8905 };
8906
8907 /**
8908 * Handle mouse up events.
8909 *
8910 * @param {jQuery.Event} e Mouse up event
8911 */
8912 OO.ui.PopupToolGroup.prototype.onHandlePointerUp = function () {
8913 return false;
8914 };
8915
8916 /**
8917 * Handle mouse down events.
8918 *
8919 * @param {jQuery.Event} e Mouse down event
8920 */
8921 OO.ui.PopupToolGroup.prototype.onHandlePointerDown = function ( e ) {
8922 // e.which is 0 for touch events, 1 for left mouse button
8923 if ( !this.isDisabled() && e.which <= 1 ) {
8924 this.setActive( !this.active );
8925 }
8926 return false;
8927 };
8928
8929 /**
8930 * Switch into active mode.
8931 *
8932 * When active, mouseup events anywhere in the document will trigger deactivation.
8933 */
8934 OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
8935 value = !!value;
8936 if ( this.active !== value ) {
8937 this.active = value;
8938 if ( value ) {
8939 this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler, true );
8940
8941 // Try anchoring the popup to the left first
8942 this.$element.addClass( 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left' );
8943 this.toggleClipping( true );
8944 if ( this.isClippedHorizontally() ) {
8945 // Anchoring to the left caused the popup to clip, so anchor it to the right instead
8946 this.toggleClipping( false );
8947 this.$element
8948 .removeClass( 'oo-ui-popupToolGroup-left' )
8949 .addClass( 'oo-ui-popupToolGroup-right' );
8950 this.toggleClipping( true );
8951 }
8952 } else {
8953 this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler, true );
8954 this.$element.removeClass(
8955 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left oo-ui-popupToolGroup-right'
8956 );
8957 this.toggleClipping( false );
8958 }
8959 }
8960 };
8961
8962 /**
8963 * Drop down list layout of tools as labeled icon buttons.
8964 *
8965 * This layout allows some tools to be collapsible, controlled by a "More" / "Fewer" option at the
8966 * bottom of the main list. These are not automatically positioned at the bottom of the list; you
8967 * may want to use the 'promote' and 'demote' configuration options to achieve this.
8968 *
8969 * @class
8970 * @extends OO.ui.PopupToolGroup
8971 *
8972 * @constructor
8973 * @param {OO.ui.Toolbar} toolbar
8974 * @param {Object} [config] Configuration options
8975 * @cfg {Array} [allowCollapse] List of tools that can be collapsed. Remaining tools will be always
8976 * shown.
8977 * @cfg {Array} [forceExpand] List of tools that *may not* be collapsed. All remaining tools will be
8978 * allowed to be collapsed.
8979 * @cfg {boolean} [expanded=false] Whether the collapsible tools are expanded by default
8980 */
8981 OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
8982 // Allow passing positional parameters inside the config object
8983 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
8984 config = toolbar;
8985 toolbar = config.toolbar;
8986 }
8987
8988 // Configuration initialization
8989 config = config || {};
8990
8991 // Properties (must be set before parent constructor, which calls #populate)
8992 this.allowCollapse = config.allowCollapse;
8993 this.forceExpand = config.forceExpand;
8994 this.expanded = config.expanded !== undefined ? config.expanded : false;
8995 this.collapsibleTools = [];
8996
8997 // Parent constructor
8998 OO.ui.ListToolGroup.super.call( this, toolbar, config );
8999
9000 // Initialization
9001 this.$element.addClass( 'oo-ui-listToolGroup' );
9002 };
9003
9004 /* Setup */
9005
9006 OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup );
9007
9008 /* Static Properties */
9009
9010 OO.ui.ListToolGroup.static.accelTooltips = true;
9011
9012 OO.ui.ListToolGroup.static.name = 'list';
9013
9014 /* Methods */
9015
9016 /**
9017 * @inheritdoc
9018 */
9019 OO.ui.ListToolGroup.prototype.populate = function () {
9020 var i, len, allowCollapse = [];
9021
9022 OO.ui.ListToolGroup.super.prototype.populate.call( this );
9023
9024 // Update the list of collapsible tools
9025 if ( this.allowCollapse !== undefined ) {
9026 allowCollapse = this.allowCollapse;
9027 } else if ( this.forceExpand !== undefined ) {
9028 allowCollapse = OO.simpleArrayDifference( Object.keys( this.tools ), this.forceExpand );
9029 }
9030
9031 this.collapsibleTools = [];
9032 for ( i = 0, len = allowCollapse.length; i < len; i++ ) {
9033 if ( this.tools[ allowCollapse[ i ] ] !== undefined ) {
9034 this.collapsibleTools.push( this.tools[ allowCollapse[ i ] ] );
9035 }
9036 }
9037
9038 // Keep at the end, even when tools are added
9039 this.$group.append( this.getExpandCollapseTool().$element );
9040
9041 this.getExpandCollapseTool().toggle( this.collapsibleTools.length !== 0 );
9042 this.updateCollapsibleState();
9043 };
9044
9045 OO.ui.ListToolGroup.prototype.getExpandCollapseTool = function () {
9046 if ( this.expandCollapseTool === undefined ) {
9047 var ExpandCollapseTool = function () {
9048 ExpandCollapseTool.super.apply( this, arguments );
9049 };
9050
9051 OO.inheritClass( ExpandCollapseTool, OO.ui.Tool );
9052
9053 ExpandCollapseTool.prototype.onSelect = function () {
9054 this.toolGroup.expanded = !this.toolGroup.expanded;
9055 this.toolGroup.updateCollapsibleState();
9056 this.setActive( false );
9057 };
9058 ExpandCollapseTool.prototype.onUpdateState = function () {
9059 // Do nothing. Tool interface requires an implementation of this function.
9060 };
9061
9062 ExpandCollapseTool.static.name = 'more-fewer';
9063
9064 this.expandCollapseTool = new ExpandCollapseTool( this );
9065 }
9066 return this.expandCollapseTool;
9067 };
9068
9069 /**
9070 * @inheritdoc
9071 */
9072 OO.ui.ListToolGroup.prototype.onPointerUp = function ( e ) {
9073 var ret = OO.ui.ListToolGroup.super.prototype.onPointerUp.call( this, e );
9074
9075 // Do not close the popup when the user wants to show more/fewer tools
9076 if ( $( e.target ).closest( '.oo-ui-tool-name-more-fewer' ).length ) {
9077 // Prevent the popup list from being hidden
9078 this.setActive( true );
9079 }
9080
9081 return ret;
9082 };
9083
9084 OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () {
9085 var i, len;
9086
9087 this.getExpandCollapseTool()
9088 .setIcon( this.expanded ? 'collapse' : 'expand' )
9089 .setTitle( OO.ui.msg( this.expanded ? 'ooui-toolgroup-collapse' : 'ooui-toolgroup-expand' ) );
9090
9091 for ( i = 0, len = this.collapsibleTools.length; i < len; i++ ) {
9092 this.collapsibleTools[ i ].toggle( this.expanded );
9093 }
9094 };
9095
9096 /**
9097 * Drop down menu layout of tools as selectable menu items.
9098 *
9099 * @class
9100 * @extends OO.ui.PopupToolGroup
9101 *
9102 * @constructor
9103 * @param {OO.ui.Toolbar} toolbar
9104 * @param {Object} [config] Configuration options
9105 */
9106 OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
9107 // Allow passing positional parameters inside the config object
9108 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
9109 config = toolbar;
9110 toolbar = config.toolbar;
9111 }
9112
9113 // Configuration initialization
9114 config = config || {};
9115
9116 // Parent constructor
9117 OO.ui.MenuToolGroup.super.call( this, toolbar, config );
9118
9119 // Events
9120 this.toolbar.connect( this, { updateState: 'onUpdateState' } );
9121
9122 // Initialization
9123 this.$element.addClass( 'oo-ui-menuToolGroup' );
9124 };
9125
9126 /* Setup */
9127
9128 OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup );
9129
9130 /* Static Properties */
9131
9132 OO.ui.MenuToolGroup.static.accelTooltips = true;
9133
9134 OO.ui.MenuToolGroup.static.name = 'menu';
9135
9136 /* Methods */
9137
9138 /**
9139 * Handle the toolbar state being updated.
9140 *
9141 * When the state changes, the title of each active item in the menu will be joined together and
9142 * used as a label for the group. The label will be empty if none of the items are active.
9143 */
9144 OO.ui.MenuToolGroup.prototype.onUpdateState = function () {
9145 var name,
9146 labelTexts = [];
9147
9148 for ( name in this.tools ) {
9149 if ( this.tools[ name ].isActive() ) {
9150 labelTexts.push( this.tools[ name ].getTitle() );
9151 }
9152 }
9153
9154 this.setLabel( labelTexts.join( ', ' ) || ' ' );
9155 };
9156
9157 /**
9158 * Tool that shows a popup when selected.
9159 *
9160 * @abstract
9161 * @class
9162 * @extends OO.ui.Tool
9163 * @mixins OO.ui.PopupElement
9164 *
9165 * @constructor
9166 * @param {OO.ui.ToolGroup} toolGroup
9167 * @param {Object} [config] Configuration options
9168 */
9169 OO.ui.PopupTool = function OoUiPopupTool( toolGroup, config ) {
9170 // Allow passing positional parameters inside the config object
9171 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
9172 config = toolGroup;
9173 toolGroup = config.toolGroup;
9174 }
9175
9176 // Parent constructor
9177 OO.ui.PopupTool.super.call( this, toolGroup, config );
9178
9179 // Mixin constructors
9180 OO.ui.PopupElement.call( this, config );
9181
9182 // Initialization
9183 this.$element
9184 .addClass( 'oo-ui-popupTool' )
9185 .append( this.popup.$element );
9186 };
9187
9188 /* Setup */
9189
9190 OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool );
9191 OO.mixinClass( OO.ui.PopupTool, OO.ui.PopupElement );
9192
9193 /* Methods */
9194
9195 /**
9196 * Handle the tool being selected.
9197 *
9198 * @inheritdoc
9199 */
9200 OO.ui.PopupTool.prototype.onSelect = function () {
9201 if ( !this.isDisabled() ) {
9202 this.popup.toggle();
9203 }
9204 this.setActive( false );
9205 return false;
9206 };
9207
9208 /**
9209 * Handle the toolbar state being updated.
9210 *
9211 * @inheritdoc
9212 */
9213 OO.ui.PopupTool.prototype.onUpdateState = function () {
9214 this.setActive( false );
9215 };
9216
9217 /**
9218 * Tool that has a tool group inside. This is a bad workaround for the lack of proper hierarchical
9219 * menus in toolbars (T74159).
9220 *
9221 * @abstract
9222 * @class
9223 * @extends OO.ui.Tool
9224 *
9225 * @constructor
9226 * @param {OO.ui.ToolGroup} toolGroup
9227 * @param {Object} [config] Configuration options
9228 */
9229 OO.ui.ToolGroupTool = function OoUiToolGroupTool( toolGroup, config ) {
9230 // Allow passing positional parameters inside the config object
9231 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
9232 config = toolGroup;
9233 toolGroup = config.toolGroup;
9234 }
9235
9236 // Parent constructor
9237 OO.ui.ToolGroupTool.super.call( this, toolGroup, config );
9238
9239 // Properties
9240 this.innerToolGroup = this.createGroup( this.constructor.static.groupConfig );
9241
9242 // Initialization
9243 this.$link.remove();
9244 this.$element
9245 .addClass( 'oo-ui-toolGroupTool' )
9246 .append( this.innerToolGroup.$element );
9247 };
9248
9249 /* Setup */
9250
9251 OO.inheritClass( OO.ui.ToolGroupTool, OO.ui.Tool );
9252
9253 /* Static Properties */
9254
9255 /**
9256 * Tool group configuration. See OO.ui.Toolbar#setup for the accepted values.
9257 *
9258 * @property {Object.<string,Array>}
9259 */
9260 OO.ui.ToolGroupTool.static.groupConfig = {};
9261
9262 /* Methods */
9263
9264 /**
9265 * Handle the tool being selected.
9266 *
9267 * @inheritdoc
9268 */
9269 OO.ui.ToolGroupTool.prototype.onSelect = function () {
9270 this.innerToolGroup.setActive( !this.innerToolGroup.active );
9271 return false;
9272 };
9273
9274 /**
9275 * Handle the toolbar state being updated.
9276 *
9277 * @inheritdoc
9278 */
9279 OO.ui.ToolGroupTool.prototype.onUpdateState = function () {
9280 this.setActive( false );
9281 };
9282
9283 /**
9284 * Build a OO.ui.ToolGroup from the configuration.
9285 *
9286 * @param {Object.<string,Array>} group Tool group configuration. See OO.ui.Toolbar#setup for the
9287 * accepted values.
9288 * @return {OO.ui.ListToolGroup}
9289 */
9290 OO.ui.ToolGroupTool.prototype.createGroup = function ( group ) {
9291 if ( group.include === '*' ) {
9292 // Apply defaults to catch-all groups
9293 if ( group.label === undefined ) {
9294 group.label = OO.ui.msg( 'ooui-toolbar-more' );
9295 }
9296 }
9297
9298 return this.toolbar.getToolGroupFactory().create( 'list', this.toolbar, group );
9299 };
9300
9301 /**
9302 * Mixin for OO.ui.Widget subclasses to provide OO.ui.GroupElement.
9303 *
9304 * Use together with OO.ui.ItemWidget to make disabled state inheritable.
9305 *
9306 * @private
9307 * @abstract
9308 * @class
9309 * @extends OO.ui.GroupElement
9310 *
9311 * @constructor
9312 * @param {Object} [config] Configuration options
9313 */
9314 OO.ui.GroupWidget = function OoUiGroupWidget( config ) {
9315 // Parent constructor
9316 OO.ui.GroupWidget.super.call( this, config );
9317 };
9318
9319 /* Setup */
9320
9321 OO.inheritClass( OO.ui.GroupWidget, OO.ui.GroupElement );
9322
9323 /* Methods */
9324
9325 /**
9326 * Set the disabled state of the widget.
9327 *
9328 * This will also update the disabled state of child widgets.
9329 *
9330 * @param {boolean} disabled Disable widget
9331 * @chainable
9332 */
9333 OO.ui.GroupWidget.prototype.setDisabled = function ( disabled ) {
9334 var i, len;
9335
9336 // Parent method
9337 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
9338 OO.ui.Widget.prototype.setDisabled.call( this, disabled );
9339
9340 // During construction, #setDisabled is called before the OO.ui.GroupElement constructor
9341 if ( this.items ) {
9342 for ( i = 0, len = this.items.length; i < len; i++ ) {
9343 this.items[ i ].updateDisabled();
9344 }
9345 }
9346
9347 return this;
9348 };
9349
9350 /**
9351 * Mixin for widgets used as items in widgets that inherit OO.ui.GroupWidget.
9352 *
9353 * Item widgets have a reference to a OO.ui.GroupWidget while they are attached to the group. This
9354 * allows bidirectional communication.
9355 *
9356 * Use together with OO.ui.GroupWidget to make disabled state inheritable.
9357 *
9358 * @private
9359 * @abstract
9360 * @class
9361 *
9362 * @constructor
9363 */
9364 OO.ui.ItemWidget = function OoUiItemWidget() {
9365 //
9366 };
9367
9368 /* Methods */
9369
9370 /**
9371 * Check if widget is disabled.
9372 *
9373 * Checks parent if present, making disabled state inheritable.
9374 *
9375 * @return {boolean} Widget is disabled
9376 */
9377 OO.ui.ItemWidget.prototype.isDisabled = function () {
9378 return this.disabled ||
9379 ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() );
9380 };
9381
9382 /**
9383 * Set group element is in.
9384 *
9385 * @param {OO.ui.GroupElement|null} group Group element, null if none
9386 * @chainable
9387 */
9388 OO.ui.ItemWidget.prototype.setElementGroup = function ( group ) {
9389 // Parent method
9390 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
9391 OO.ui.Element.prototype.setElementGroup.call( this, group );
9392
9393 // Initialize item disabled states
9394 this.updateDisabled();
9395
9396 return this;
9397 };
9398
9399 /**
9400 * Set of controls for an OO.ui.OutlineSelectWidget.
9401 *
9402 * Controls include moving items up and down, removing items, and adding different kinds of items.
9403 *
9404 * @class
9405 * @extends OO.ui.Widget
9406 * @mixins OO.ui.GroupElement
9407 * @mixins OO.ui.IconElement
9408 *
9409 * @constructor
9410 * @param {OO.ui.OutlineSelectWidget} outline Outline to control
9411 * @param {Object} [config] Configuration options
9412 */
9413 OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
9414 // Allow passing positional parameters inside the config object
9415 if ( OO.isPlainObject( outline ) && config === undefined ) {
9416 config = outline;
9417 outline = config.outline;
9418 }
9419
9420 // Configuration initialization
9421 config = $.extend( { icon: 'add' }, config );
9422
9423 // Parent constructor
9424 OO.ui.OutlineControlsWidget.super.call( this, config );
9425
9426 // Mixin constructors
9427 OO.ui.GroupElement.call( this, config );
9428 OO.ui.IconElement.call( this, config );
9429
9430 // Properties
9431 this.outline = outline;
9432 this.$movers = $( '<div>' );
9433 this.upButton = new OO.ui.ButtonWidget( {
9434 framed: false,
9435 icon: 'collapse',
9436 title: OO.ui.msg( 'ooui-outline-control-move-up' )
9437 } );
9438 this.downButton = new OO.ui.ButtonWidget( {
9439 framed: false,
9440 icon: 'expand',
9441 title: OO.ui.msg( 'ooui-outline-control-move-down' )
9442 } );
9443 this.removeButton = new OO.ui.ButtonWidget( {
9444 framed: false,
9445 icon: 'remove',
9446 title: OO.ui.msg( 'ooui-outline-control-remove' )
9447 } );
9448
9449 // Events
9450 outline.connect( this, {
9451 select: 'onOutlineChange',
9452 add: 'onOutlineChange',
9453 remove: 'onOutlineChange'
9454 } );
9455 this.upButton.connect( this, { click: [ 'emit', 'move', -1 ] } );
9456 this.downButton.connect( this, { click: [ 'emit', 'move', 1 ] } );
9457 this.removeButton.connect( this, { click: [ 'emit', 'remove' ] } );
9458
9459 // Initialization
9460 this.$element.addClass( 'oo-ui-outlineControlsWidget' );
9461 this.$group.addClass( 'oo-ui-outlineControlsWidget-items' );
9462 this.$movers
9463 .addClass( 'oo-ui-outlineControlsWidget-movers' )
9464 .append( this.removeButton.$element, this.upButton.$element, this.downButton.$element );
9465 this.$element.append( this.$icon, this.$group, this.$movers );
9466 };
9467
9468 /* Setup */
9469
9470 OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget );
9471 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.GroupElement );
9472 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.IconElement );
9473
9474 /* Events */
9475
9476 /**
9477 * @event move
9478 * @param {number} places Number of places to move
9479 */
9480
9481 /**
9482 * @event remove
9483 */
9484
9485 /* Methods */
9486
9487 /**
9488 * Handle outline change events.
9489 */
9490 OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () {
9491 var i, len, firstMovable, lastMovable,
9492 items = this.outline.getItems(),
9493 selectedItem = this.outline.getSelectedItem(),
9494 movable = selectedItem && selectedItem.isMovable(),
9495 removable = selectedItem && selectedItem.isRemovable();
9496
9497 if ( movable ) {
9498 i = -1;
9499 len = items.length;
9500 while ( ++i < len ) {
9501 if ( items[ i ].isMovable() ) {
9502 firstMovable = items[ i ];
9503 break;
9504 }
9505 }
9506 i = len;
9507 while ( i-- ) {
9508 if ( items[ i ].isMovable() ) {
9509 lastMovable = items[ i ];
9510 break;
9511 }
9512 }
9513 }
9514 this.upButton.setDisabled( !movable || selectedItem === firstMovable );
9515 this.downButton.setDisabled( !movable || selectedItem === lastMovable );
9516 this.removeButton.setDisabled( !removable );
9517 };
9518
9519 /**
9520 * ToggleWidget is mixed into other classes to create widgets with an on/off state.
9521 * Please see OO.ui.ToggleButtonWidget and OO.ui.ToggleSwitchWidget for examples.
9522 *
9523 * @abstract
9524 * @class
9525 *
9526 * @constructor
9527 * @param {Object} [config] Configuration options
9528 * @cfg {boolean} [value=false] The toggle’s initial on/off state.
9529 * By default, the toggle is in the 'off' state.
9530 */
9531 OO.ui.ToggleWidget = function OoUiToggleWidget( config ) {
9532 // Configuration initialization
9533 config = config || {};
9534
9535 // Properties
9536 this.value = null;
9537
9538 // Initialization
9539 this.$element.addClass( 'oo-ui-toggleWidget' );
9540 this.setValue( !!config.value );
9541 };
9542
9543 /* Events */
9544
9545 /**
9546 * @event change
9547 *
9548 * A change event is emitted when the on/off state of the toggle changes.
9549 *
9550 * @param {boolean} value Value representing the new state of the toggle
9551 */
9552
9553 /* Methods */
9554
9555 /**
9556 * Get the value representing the toggle’s state.
9557 *
9558 * @return {boolean} The on/off state of the toggle
9559 */
9560 OO.ui.ToggleWidget.prototype.getValue = function () {
9561 return this.value;
9562 };
9563
9564 /**
9565 * Set the state of the toggle: `true` for 'on', `false' for 'off'.
9566 *
9567 * @param {boolean} value The state of the toggle
9568 * @fires change
9569 * @chainable
9570 */
9571 OO.ui.ToggleWidget.prototype.setValue = function ( value ) {
9572 value = !!value;
9573 if ( this.value !== value ) {
9574 this.value = value;
9575 this.emit( 'change', value );
9576 this.$element.toggleClass( 'oo-ui-toggleWidget-on', value );
9577 this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value );
9578 this.$element.attr( 'aria-checked', value.toString() );
9579 }
9580 return this;
9581 };
9582
9583 /**
9584 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
9585 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
9586 * removed, and cleared from the group.
9587 *
9588 * @example
9589 * // Example: A ButtonGroupWidget with two buttons
9590 * var button1 = new OO.ui.PopupButtonWidget( {
9591 * label : 'Select a category',
9592 * icon : 'menu',
9593 * popup : {
9594 * $content: $( '<p>List of categories...</p>' ),
9595 * padded: true,
9596 * align: 'left'
9597 * }
9598 * } );
9599 * var button2 = new OO.ui.ButtonWidget( {
9600 * label : 'Add item'
9601 * });
9602 * var buttonGroup = new OO.ui.ButtonGroupWidget( {
9603 * items: [button1, button2]
9604 * } );
9605 * $('body').append(buttonGroup.$element);
9606 *
9607 * @class
9608 * @extends OO.ui.Widget
9609 * @mixins OO.ui.GroupElement
9610 *
9611 * @constructor
9612 * @param {Object} [config] Configuration options
9613 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
9614 */
9615 OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
9616 // Configuration initialization
9617 config = config || {};
9618
9619 // Parent constructor
9620 OO.ui.ButtonGroupWidget.super.call( this, config );
9621
9622 // Mixin constructors
9623 OO.ui.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
9624
9625 // Initialization
9626 this.$element.addClass( 'oo-ui-buttonGroupWidget' );
9627 if ( Array.isArray( config.items ) ) {
9628 this.addItems( config.items );
9629 }
9630 };
9631
9632 /* Setup */
9633
9634 OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget );
9635 OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.GroupElement );
9636
9637 /**
9638 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
9639 * feels, and functionality can be customized via the class’s configuration options
9640 * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
9641 * and examples.
9642 *
9643 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
9644 *
9645 * @example
9646 * // A button widget
9647 * var button = new OO.ui.ButtonWidget( {
9648 * label : 'Button with Icon',
9649 * icon : 'remove',
9650 * iconTitle : 'Remove'
9651 * } );
9652 * $( 'body' ).append( button.$element );
9653 *
9654 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
9655 *
9656 * @class
9657 * @extends OO.ui.Widget
9658 * @mixins OO.ui.ButtonElement
9659 * @mixins OO.ui.IconElement
9660 * @mixins OO.ui.IndicatorElement
9661 * @mixins OO.ui.LabelElement
9662 * @mixins OO.ui.TitledElement
9663 * @mixins OO.ui.FlaggedElement
9664 * @mixins OO.ui.TabIndexedElement
9665 *
9666 * @constructor
9667 * @param {Object} [config] Configuration options
9668 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
9669 * @cfg {string} [target] The frame or window in which to open the hyperlink.
9670 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
9671 */
9672 OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
9673 // Configuration initialization
9674 // FIXME: The `nofollow` alias is deprecated and will be removed (T89767)
9675 config = $.extend( { noFollow: config && config.nofollow }, config );
9676
9677 // Parent constructor
9678 OO.ui.ButtonWidget.super.call( this, config );
9679
9680 // Mixin constructors
9681 OO.ui.ButtonElement.call( this, config );
9682 OO.ui.IconElement.call( this, config );
9683 OO.ui.IndicatorElement.call( this, config );
9684 OO.ui.LabelElement.call( this, config );
9685 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
9686 OO.ui.FlaggedElement.call( this, config );
9687 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
9688
9689 // Properties
9690 this.href = null;
9691 this.target = null;
9692 this.noFollow = false;
9693 this.isHyperlink = false;
9694
9695 // Initialization
9696 this.$button.append( this.$icon, this.$label, this.$indicator );
9697 this.$element
9698 .addClass( 'oo-ui-buttonWidget' )
9699 .append( this.$button );
9700 this.setHref( config.href );
9701 this.setTarget( config.target );
9702 this.setNoFollow( config.noFollow );
9703 };
9704
9705 /* Setup */
9706
9707 OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget );
9708 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.ButtonElement );
9709 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IconElement );
9710 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IndicatorElement );
9711 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.LabelElement );
9712 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TitledElement );
9713 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.FlaggedElement );
9714 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TabIndexedElement );
9715
9716 /* Methods */
9717
9718 /**
9719 * @inheritdoc
9720 */
9721 OO.ui.ButtonWidget.prototype.onMouseDown = function ( e ) {
9722 if ( !this.isDisabled() ) {
9723 // Remove the tab-index while the button is down to prevent the button from stealing focus
9724 this.$button.removeAttr( 'tabindex' );
9725 }
9726
9727 return OO.ui.ButtonElement.prototype.onMouseDown.call( this, e );
9728 };
9729
9730 /**
9731 * @inheritdoc
9732 */
9733 OO.ui.ButtonWidget.prototype.onMouseUp = function ( e ) {
9734 if ( !this.isDisabled() ) {
9735 // Restore the tab-index after the button is up to restore the button's accessibility
9736 this.$button.attr( 'tabindex', this.tabIndex );
9737 }
9738
9739 return OO.ui.ButtonElement.prototype.onMouseUp.call( this, e );
9740 };
9741
9742 /**
9743 * @inheritdoc
9744 */
9745 OO.ui.ButtonWidget.prototype.onClick = function ( e ) {
9746 var ret = OO.ui.ButtonElement.prototype.onClick.call( this, e );
9747 if ( this.isHyperlink ) {
9748 return true;
9749 }
9750 return ret;
9751 };
9752
9753 /**
9754 * @inheritdoc
9755 */
9756 OO.ui.ButtonWidget.prototype.onKeyPress = function ( e ) {
9757 var ret = OO.ui.ButtonElement.prototype.onKeyPress.call( this, e );
9758 if ( this.isHyperlink ) {
9759 return true;
9760 }
9761 return ret;
9762 };
9763
9764 /**
9765 * Get hyperlink location.
9766 *
9767 * @return {string} Hyperlink location
9768 */
9769 OO.ui.ButtonWidget.prototype.getHref = function () {
9770 return this.href;
9771 };
9772
9773 /**
9774 * Get hyperlink target.
9775 *
9776 * @return {string} Hyperlink target
9777 */
9778 OO.ui.ButtonWidget.prototype.getTarget = function () {
9779 return this.target;
9780 };
9781
9782 /**
9783 * Get search engine traversal hint.
9784 *
9785 * @return {boolean} Whether search engines should avoid traversing this hyperlink
9786 */
9787 OO.ui.ButtonWidget.prototype.getNoFollow = function () {
9788 return this.noFollow;
9789 };
9790
9791 /**
9792 * Set hyperlink location.
9793 *
9794 * @param {string|null} href Hyperlink location, null to remove
9795 */
9796 OO.ui.ButtonWidget.prototype.setHref = function ( href ) {
9797 href = typeof href === 'string' ? href : null;
9798
9799 if ( href !== this.href ) {
9800 this.href = href;
9801 if ( href !== null ) {
9802 this.$button.attr( 'href', href );
9803 this.isHyperlink = true;
9804 } else {
9805 this.$button.removeAttr( 'href' );
9806 this.isHyperlink = false;
9807 }
9808 }
9809
9810 return this;
9811 };
9812
9813 /**
9814 * Set hyperlink target.
9815 *
9816 * @param {string|null} target Hyperlink target, null to remove
9817 */
9818 OO.ui.ButtonWidget.prototype.setTarget = function ( target ) {
9819 target = typeof target === 'string' ? target : null;
9820
9821 if ( target !== this.target ) {
9822 this.target = target;
9823 if ( target !== null ) {
9824 this.$button.attr( 'target', target );
9825 } else {
9826 this.$button.removeAttr( 'target' );
9827 }
9828 }
9829
9830 return this;
9831 };
9832
9833 /**
9834 * Set search engine traversal hint.
9835 *
9836 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
9837 */
9838 OO.ui.ButtonWidget.prototype.setNoFollow = function ( noFollow ) {
9839 noFollow = typeof noFollow === 'boolean' ? noFollow : true;
9840
9841 if ( noFollow !== this.noFollow ) {
9842 this.noFollow = noFollow;
9843 if ( noFollow ) {
9844 this.$button.attr( 'rel', 'nofollow' );
9845 } else {
9846 this.$button.removeAttr( 'rel' );
9847 }
9848 }
9849
9850 return this;
9851 };
9852
9853 /**
9854 * An ActionWidget is a {@link OO.ui.ButtonWidget button widget} that executes an action.
9855 * Action widgets are used with OO.ui.ActionSet, which manages the behavior and availability
9856 * of the actions. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
9857 * and examples.
9858 *
9859 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
9860 *
9861 * @class
9862 * @extends OO.ui.ButtonWidget
9863 * @mixins OO.ui.PendingElement
9864 *
9865 * @constructor
9866 * @param {Object} [config] Configuration options
9867 * @cfg {string} [action] Symbolic action name
9868 * @cfg {string[]} [modes] Symbolic mode names
9869 * @cfg {boolean} [framed=false] Render button with a frame
9870 */
9871 OO.ui.ActionWidget = function OoUiActionWidget( config ) {
9872 // Configuration initialization
9873 config = $.extend( { framed: false }, config );
9874
9875 // Parent constructor
9876 OO.ui.ActionWidget.super.call( this, config );
9877
9878 // Mixin constructors
9879 OO.ui.PendingElement.call( this, config );
9880
9881 // Properties
9882 this.action = config.action || '';
9883 this.modes = config.modes || [];
9884 this.width = 0;
9885 this.height = 0;
9886
9887 // Initialization
9888 this.$element.addClass( 'oo-ui-actionWidget' );
9889 };
9890
9891 /* Setup */
9892
9893 OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget );
9894 OO.mixinClass( OO.ui.ActionWidget, OO.ui.PendingElement );
9895
9896 /* Events */
9897
9898 /**
9899 * @event resize
9900 */
9901
9902 /* Methods */
9903
9904 /**
9905 * Check if action is available in a certain mode.
9906 *
9907 * @param {string} mode Name of mode
9908 * @return {boolean} Has mode
9909 */
9910 OO.ui.ActionWidget.prototype.hasMode = function ( mode ) {
9911 return this.modes.indexOf( mode ) !== -1;
9912 };
9913
9914 /**
9915 * Get symbolic action name.
9916 *
9917 * @return {string}
9918 */
9919 OO.ui.ActionWidget.prototype.getAction = function () {
9920 return this.action;
9921 };
9922
9923 /**
9924 * Get symbolic action name.
9925 *
9926 * @return {string}
9927 */
9928 OO.ui.ActionWidget.prototype.getModes = function () {
9929 return this.modes.slice();
9930 };
9931
9932 /**
9933 * Emit a resize event if the size has changed.
9934 *
9935 * @chainable
9936 */
9937 OO.ui.ActionWidget.prototype.propagateResize = function () {
9938 var width, height;
9939
9940 if ( this.isElementAttached() ) {
9941 width = this.$element.width();
9942 height = this.$element.height();
9943
9944 if ( width !== this.width || height !== this.height ) {
9945 this.width = width;
9946 this.height = height;
9947 this.emit( 'resize' );
9948 }
9949 }
9950
9951 return this;
9952 };
9953
9954 /**
9955 * @inheritdoc
9956 */
9957 OO.ui.ActionWidget.prototype.setIcon = function () {
9958 // Mixin method
9959 OO.ui.IconElement.prototype.setIcon.apply( this, arguments );
9960 this.propagateResize();
9961
9962 return this;
9963 };
9964
9965 /**
9966 * @inheritdoc
9967 */
9968 OO.ui.ActionWidget.prototype.setLabel = function () {
9969 // Mixin method
9970 OO.ui.LabelElement.prototype.setLabel.apply( this, arguments );
9971 this.propagateResize();
9972
9973 return this;
9974 };
9975
9976 /**
9977 * @inheritdoc
9978 */
9979 OO.ui.ActionWidget.prototype.setFlags = function () {
9980 // Mixin method
9981 OO.ui.FlaggedElement.prototype.setFlags.apply( this, arguments );
9982 this.propagateResize();
9983
9984 return this;
9985 };
9986
9987 /**
9988 * @inheritdoc
9989 */
9990 OO.ui.ActionWidget.prototype.clearFlags = function () {
9991 // Mixin method
9992 OO.ui.FlaggedElement.prototype.clearFlags.apply( this, arguments );
9993 this.propagateResize();
9994
9995 return this;
9996 };
9997
9998 /**
9999 * Toggle visibility of button.
10000 *
10001 * @param {boolean} [show] Show button, omit to toggle visibility
10002 * @chainable
10003 */
10004 OO.ui.ActionWidget.prototype.toggle = function () {
10005 // Parent method
10006 OO.ui.ActionWidget.super.prototype.toggle.apply( this, arguments );
10007 this.propagateResize();
10008
10009 return this;
10010 };
10011
10012 /**
10013 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
10014 * which is used to display additional information or options.
10015 *
10016 * @example
10017 * // Example of a popup button.
10018 * var popupButton = new OO.ui.PopupButtonWidget( {
10019 * label: 'Popup button with options',
10020 * icon: 'menu',
10021 * popup: {
10022 * $content: $( '<p>Additional options here.</p>' ),
10023 * padded: true,
10024 * align: 'left'
10025 * }
10026 * } );
10027 * // Append the button to the DOM.
10028 * $( 'body' ).append( popupButton.$element );
10029 *
10030 * @class
10031 * @extends OO.ui.ButtonWidget
10032 * @mixins OO.ui.PopupElement
10033 *
10034 * @constructor
10035 * @param {Object} [config] Configuration options
10036 */
10037 OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
10038 // Parent constructor
10039 OO.ui.PopupButtonWidget.super.call( this, config );
10040
10041 // Mixin constructors
10042 OO.ui.PopupElement.call( this, config );
10043
10044 // Events
10045 this.connect( this, { click: 'onAction' } );
10046
10047 // Initialization
10048 this.$element
10049 .addClass( 'oo-ui-popupButtonWidget' )
10050 .attr( 'aria-haspopup', 'true' )
10051 .append( this.popup.$element );
10052 };
10053
10054 /* Setup */
10055
10056 OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
10057 OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.PopupElement );
10058
10059 /* Methods */
10060
10061 /**
10062 * Handle the button action being triggered.
10063 *
10064 * @private
10065 */
10066 OO.ui.PopupButtonWidget.prototype.onAction = function () {
10067 this.popup.toggle();
10068 };
10069
10070 /**
10071 * ToggleButtons are buttons that have a state (‘on’ or ‘off’) that is represented by a
10072 * Boolean value. Like other {@link OO.ui.ButtonWidget buttons}, toggle buttons can be
10073 * configured with {@link OO.ui.IconElement icons}, {@link OO.ui.IndicatorElement indicators},
10074 * {@link OO.ui.TitledElement titles}, {@link OO.ui.FlaggedElement styling flags},
10075 * and {@link OO.ui.LabelElement labels}. Please see
10076 * the [OOjs UI documentation][1] on MediaWiki for more information.
10077 *
10078 * @example
10079 * // Toggle buttons in the 'off' and 'on' state.
10080 * var toggleButton1 = new OO.ui.ToggleButtonWidget( {
10081 * label: 'Toggle Button off'
10082 * } );
10083 * var toggleButton2 = new OO.ui.ToggleButtonWidget( {
10084 * label: 'Toggle Button on',
10085 * value: true
10086 * } );
10087 * // Append the buttons to the DOM.
10088 * $( 'body' ).append( toggleButton1.$element, toggleButton2.$element );
10089 *
10090 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Toggle_buttons
10091 *
10092 * @class
10093 * @extends OO.ui.ButtonWidget
10094 * @mixins OO.ui.ToggleWidget
10095 *
10096 * @constructor
10097 * @param {Object} [config] Configuration options
10098 * @cfg {boolean} [value=false] The toggle button’s initial on/off
10099 * state. By default, the button is in the 'off' state.
10100 */
10101 OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) {
10102 // Configuration initialization
10103 config = config || {};
10104
10105 // Parent constructor
10106 OO.ui.ToggleButtonWidget.super.call( this, config );
10107
10108 // Mixin constructors
10109 OO.ui.ToggleWidget.call( this, config );
10110
10111 // Events
10112 this.connect( this, { click: 'onAction' } );
10113
10114 // Initialization
10115 this.$element.addClass( 'oo-ui-toggleButtonWidget' );
10116 };
10117
10118 /* Setup */
10119
10120 OO.inheritClass( OO.ui.ToggleButtonWidget, OO.ui.ButtonWidget );
10121 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget );
10122
10123 /* Methods */
10124
10125 /**
10126 *
10127 * @private
10128 * Handle the button action being triggered.
10129 */
10130 OO.ui.ToggleButtonWidget.prototype.onAction = function () {
10131 this.setValue( !this.value );
10132 };
10133
10134 /**
10135 * @inheritdoc
10136 */
10137 OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
10138 value = !!value;
10139 if ( value !== this.value ) {
10140 this.$button.attr( 'aria-pressed', value.toString() );
10141 this.setActive( value );
10142 }
10143
10144 // Parent method (from mixin)
10145 OO.ui.ToggleWidget.prototype.setValue.call( this, value );
10146
10147 return this;
10148 };
10149
10150 /**
10151 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
10152 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
10153 * users can interact with it.
10154 *
10155 * @example
10156 * // Example: A DropdownWidget with a menu that contains three options
10157 * var dropDown=new OO.ui.DropdownWidget( {
10158 * label: 'Dropdown menu: Select a menu option',
10159 * menu: {
10160 * items: [
10161 * new OO.ui.MenuOptionWidget( {
10162 * data: 'a',
10163 * label: 'First'
10164 * } ),
10165 * new OO.ui.MenuOptionWidget( {
10166 * data: 'b',
10167 * label: 'Second'
10168 * } ),
10169 * new OO.ui.MenuOptionWidget( {
10170 * data: 'c',
10171 * label: 'Third'
10172 * } )
10173 * ]
10174 * }
10175 * } );
10176 *
10177 * $('body').append(dropDown.$element);
10178 *
10179 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
10180 *
10181 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
10182 *
10183 * @class
10184 * @extends OO.ui.Widget
10185 * @mixins OO.ui.IconElement
10186 * @mixins OO.ui.IndicatorElement
10187 * @mixins OO.ui.LabelElement
10188 * @mixins OO.ui.TitledElement
10189 * @mixins OO.ui.TabIndexedElement
10190 *
10191 * @constructor
10192 * @param {Object} [config] Configuration options
10193 * @cfg {Object} [menu] Configuration options to pass to menu widget
10194 */
10195 OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
10196 // Configuration initialization
10197 config = $.extend( { indicator: 'down' }, config );
10198
10199 // Parent constructor
10200 OO.ui.DropdownWidget.super.call( this, config );
10201
10202 // Properties (must be set before TabIndexedElement constructor call)
10203 this.$handle = this.$( '<span>' );
10204
10205 // Mixin constructors
10206 OO.ui.IconElement.call( this, config );
10207 OO.ui.IndicatorElement.call( this, config );
10208 OO.ui.LabelElement.call( this, config );
10209 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
10210 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
10211
10212 // Properties
10213 this.menu = new OO.ui.MenuSelectWidget( $.extend( { widget: this }, config.menu ) );
10214
10215 // Events
10216 this.$handle.on( {
10217 click: this.onClick.bind( this ),
10218 keypress: this.onKeyPress.bind( this )
10219 } );
10220 this.menu.connect( this, { select: 'onMenuSelect' } );
10221
10222 // Initialization
10223 this.$handle
10224 .addClass( 'oo-ui-dropdownWidget-handle' )
10225 .append( this.$icon, this.$label, this.$indicator );
10226 this.$element
10227 .addClass( 'oo-ui-dropdownWidget' )
10228 .append( this.$handle, this.menu.$element );
10229 };
10230
10231 /* Setup */
10232
10233 OO.inheritClass( OO.ui.DropdownWidget, OO.ui.Widget );
10234 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.IconElement );
10235 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.IndicatorElement );
10236 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.LabelElement );
10237 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.TitledElement );
10238 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.TabIndexedElement );
10239
10240 /* Methods */
10241
10242 /**
10243 * Get the menu.
10244 *
10245 * @return {OO.ui.MenuSelectWidget} Menu of widget
10246 */
10247 OO.ui.DropdownWidget.prototype.getMenu = function () {
10248 return this.menu;
10249 };
10250
10251 /**
10252 * Handles menu select events.
10253 *
10254 * @private
10255 * @param {OO.ui.MenuOptionWidget} item Selected menu item
10256 */
10257 OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
10258 var selectedLabel;
10259
10260 if ( !item ) {
10261 return;
10262 }
10263
10264 selectedLabel = item.getLabel();
10265
10266 // If the label is a DOM element, clone it, because setLabel will append() it
10267 if ( selectedLabel instanceof jQuery ) {
10268 selectedLabel = selectedLabel.clone();
10269 }
10270
10271 this.setLabel( selectedLabel );
10272 };
10273
10274 /**
10275 * Handle mouse click events.
10276 *
10277 * @private
10278 * @param {jQuery.Event} e Mouse click event
10279 */
10280 OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
10281 if ( !this.isDisabled() && e.which === 1 ) {
10282 this.menu.toggle();
10283 }
10284 return false;
10285 };
10286
10287 /**
10288 * Handle key press events.
10289 *
10290 * @private
10291 * @param {jQuery.Event} e Key press event
10292 */
10293 OO.ui.DropdownWidget.prototype.onKeyPress = function ( e ) {
10294 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
10295 this.menu.toggle();
10296 }
10297 return false;
10298 };
10299
10300 /**
10301 * IconWidget is a generic widget for {@link OO.ui.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
10302 * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
10303 * for a list of icons included in the library.
10304 *
10305 * @example
10306 * // An icon widget with a label
10307 * var myIcon = new OO.ui.IconWidget({
10308 * icon: 'help',
10309 * iconTitle: 'Help'
10310 * });
10311 * // Create a label.
10312 * var iconLabel = new OO.ui.LabelWidget({
10313 * label: 'Help'
10314 * });
10315 * $('body').append(myIcon.$element, iconLabel.$element);
10316 *
10317 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
10318 *
10319 * @class
10320 * @extends OO.ui.Widget
10321 * @mixins OO.ui.IconElement
10322 * @mixins OO.ui.TitledElement
10323 *
10324 * @constructor
10325 * @param {Object} [config] Configuration options
10326 */
10327 OO.ui.IconWidget = function OoUiIconWidget( config ) {
10328 // Configuration initialization
10329 config = config || {};
10330
10331 // Parent constructor
10332 OO.ui.IconWidget.super.call( this, config );
10333
10334 // Mixin constructors
10335 OO.ui.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) );
10336 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
10337
10338 // Initialization
10339 this.$element.addClass( 'oo-ui-iconWidget' );
10340 };
10341
10342 /* Setup */
10343
10344 OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
10345 OO.mixinClass( OO.ui.IconWidget, OO.ui.IconElement );
10346 OO.mixinClass( OO.ui.IconWidget, OO.ui.TitledElement );
10347
10348 /* Static Properties */
10349
10350 OO.ui.IconWidget.static.tagName = 'span';
10351
10352 /**
10353 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
10354 * attention to the status of an item or to clarify the function of a control. For a list of
10355 * indicators included in the library, please see the [OOjs UI documentation on MediaWiki][1].
10356 *
10357 * @example
10358 * // Example of an indicator widget
10359 * var indicator1 = new OO.ui.IndicatorWidget( {
10360 * indicator: 'alert'
10361 * });
10362 *
10363 * // Create a fieldset layout to add a label
10364 * var fieldset = new OO.ui.FieldsetLayout( );
10365 * fieldset.addItems( [
10366 * new OO.ui.FieldLayout( indicator1, {label: 'An alert indicator:'} )
10367 * ] );
10368 * $( 'body' ).append( fieldset.$element );
10369 *
10370 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
10371 *
10372 * @class
10373 * @extends OO.ui.Widget
10374 * @mixins OO.ui.IndicatorElement
10375 * @mixins OO.ui.TitledElement
10376 *
10377 * @constructor
10378 * @param {Object} [config] Configuration options
10379 */
10380 OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
10381 // Configuration initialization
10382 config = config || {};
10383
10384 // Parent constructor
10385 OO.ui.IndicatorWidget.super.call( this, config );
10386
10387 // Mixin constructors
10388 OO.ui.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) );
10389 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
10390
10391 // Initialization
10392 this.$element.addClass( 'oo-ui-indicatorWidget' );
10393 };
10394
10395 /* Setup */
10396
10397 OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
10398 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.IndicatorElement );
10399 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.TitledElement );
10400
10401 /* Static Properties */
10402
10403 OO.ui.IndicatorWidget.static.tagName = 'span';
10404
10405 /**
10406 * InputWidget is the base class for all input widgets, which
10407 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
10408 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
10409 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
10410 *
10411 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
10412 *
10413 * @abstract
10414 * @class
10415 * @extends OO.ui.Widget
10416 * @mixins OO.ui.FlaggedElement
10417 * @mixins OO.ui.TabIndexedElement
10418 *
10419 * @constructor
10420 * @param {Object} [config] Configuration options
10421 * @cfg {string} [name=''] HTML input name
10422 * @cfg {string} [value=''] Input value
10423 * @cfg {Function} [inputFilter] Filter function to apply to the input. Takes a string argument and returns a string.
10424 */
10425 OO.ui.InputWidget = function OoUiInputWidget( config ) {
10426 // Configuration initialization
10427 config = config || {};
10428
10429 // Parent constructor
10430 OO.ui.InputWidget.super.call( this, config );
10431
10432 // Properties
10433 this.$input = this.getInputElement( config );
10434 this.value = '';
10435 this.inputFilter = config.inputFilter;
10436
10437 // Mixin constructors
10438 OO.ui.FlaggedElement.call( this, config );
10439 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$input } ) );
10440
10441 // Events
10442 this.$input.on( 'keydown mouseup cut paste change input select', this.onEdit.bind( this ) );
10443
10444 // Initialization
10445 this.$input
10446 .attr( 'name', config.name )
10447 .prop( 'disabled', this.isDisabled() );
10448 this.$element.addClass( 'oo-ui-inputWidget' ).append( this.$input, $( '<span>' ) );
10449 this.setValue( config.value );
10450 };
10451
10452 /* Setup */
10453
10454 OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
10455 OO.mixinClass( OO.ui.InputWidget, OO.ui.FlaggedElement );
10456 OO.mixinClass( OO.ui.InputWidget, OO.ui.TabIndexedElement );
10457
10458 /* Events */
10459
10460 /**
10461 * @event change
10462 * @param {string} value
10463 */
10464
10465 /* Methods */
10466
10467 /**
10468 * Get input element.
10469 *
10470 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
10471 * different circumstances. The element must have a `value` property (like form elements).
10472 *
10473 * @private
10474 * @param {Object} config Configuration options
10475 * @return {jQuery} Input element
10476 */
10477 OO.ui.InputWidget.prototype.getInputElement = function () {
10478 return $( '<input>' );
10479 };
10480
10481 /**
10482 * Handle potentially value-changing events.
10483 *
10484 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
10485 */
10486 OO.ui.InputWidget.prototype.onEdit = function () {
10487 var widget = this;
10488 if ( !this.isDisabled() ) {
10489 // Allow the stack to clear so the value will be updated
10490 setTimeout( function () {
10491 widget.setValue( widget.$input.val() );
10492 } );
10493 }
10494 };
10495
10496 /**
10497 * Get the value of the input.
10498 *
10499 * @return {string} Input value
10500 */
10501 OO.ui.InputWidget.prototype.getValue = function () {
10502 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
10503 // it, and we won't know unless they're kind enough to trigger a 'change' event.
10504 var value = this.$input.val();
10505 if ( this.value !== value ) {
10506 this.setValue( value );
10507 }
10508 return this.value;
10509 };
10510
10511 /**
10512 * Sets the direction of the current input, either RTL or LTR
10513 *
10514 * @param {boolean} isRTL
10515 */
10516 OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
10517 this.$input.prop( 'dir', isRTL ? 'rtl' : 'ltr' );
10518 };
10519
10520 /**
10521 * Set the value of the input.
10522 *
10523 * @param {string} value New value
10524 * @fires change
10525 * @chainable
10526 */
10527 OO.ui.InputWidget.prototype.setValue = function ( value ) {
10528 value = this.cleanUpValue( value );
10529 // Update the DOM if it has changed. Note that with cleanUpValue, it
10530 // is possible for the DOM value to change without this.value changing.
10531 if ( this.$input.val() !== value ) {
10532 this.$input.val( value );
10533 }
10534 if ( this.value !== value ) {
10535 this.value = value;
10536 this.emit( 'change', this.value );
10537 }
10538 return this;
10539 };
10540
10541 /**
10542 * Clean up incoming value.
10543 *
10544 * Ensures value is a string, and converts undefined and null to empty string.
10545 *
10546 * @private
10547 * @param {string} value Original value
10548 * @return {string} Cleaned up value
10549 */
10550 OO.ui.InputWidget.prototype.cleanUpValue = function ( value ) {
10551 if ( value === undefined || value === null ) {
10552 return '';
10553 } else if ( this.inputFilter ) {
10554 return this.inputFilter( String( value ) );
10555 } else {
10556 return String( value );
10557 }
10558 };
10559
10560 /**
10561 * Simulate the behavior of clicking on a label bound to this input.
10562 */
10563 OO.ui.InputWidget.prototype.simulateLabelClick = function () {
10564 if ( !this.isDisabled() ) {
10565 if ( this.$input.is( ':checkbox, :radio' ) ) {
10566 this.$input.click();
10567 }
10568 if ( this.$input.is( ':input' ) ) {
10569 this.$input[ 0 ].focus();
10570 }
10571 }
10572 };
10573
10574 /**
10575 * @inheritdoc
10576 */
10577 OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
10578 OO.ui.InputWidget.super.prototype.setDisabled.call( this, state );
10579 if ( this.$input ) {
10580 this.$input.prop( 'disabled', this.isDisabled() );
10581 }
10582 return this;
10583 };
10584
10585 /**
10586 * Focus the input.
10587 *
10588 * @chainable
10589 */
10590 OO.ui.InputWidget.prototype.focus = function () {
10591 this.$input[ 0 ].focus();
10592 return this;
10593 };
10594
10595 /**
10596 * Blur the input.
10597 *
10598 * @chainable
10599 */
10600 OO.ui.InputWidget.prototype.blur = function () {
10601 this.$input[ 0 ].blur();
10602 return this;
10603 };
10604
10605 /**
10606 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
10607 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
10608 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
10609 * HTML `<button/>` (the default) or an HTML `<input/>` tags. See the
10610 * [OOjs UI documentation on MediaWiki] [1] for more information.
10611 *
10612 * @example
10613 * // A ButtonInputWidget rendered as an HTML button, the default.
10614 * var button = new OO.ui.ButtonInputWidget( {
10615 * label: 'Input button',
10616 * icon: 'check',
10617 * value: 'check'
10618 * } );
10619 * $( 'body' ).append( button.$element );
10620 *
10621 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
10622 *
10623 * @class
10624 * @extends OO.ui.InputWidget
10625 * @mixins OO.ui.ButtonElement
10626 * @mixins OO.ui.IconElement
10627 * @mixins OO.ui.IndicatorElement
10628 * @mixins OO.ui.LabelElement
10629 * @mixins OO.ui.TitledElement
10630 * @mixins OO.ui.FlaggedElement
10631 *
10632 * @constructor
10633 * @param {Object} [config] Configuration options
10634 * @cfg {string} [type='button'] HTML tag `type` attribute, may be 'button', 'submit' or 'reset'
10635 * @cfg {boolean} [useInputTag=false] Whether to use `<input/>` rather than `<button/>`. Only useful
10636 * if you need IE 6 support in a form with multiple buttons. If you use this option, icons and
10637 * indicators will not be displayed, it won't be possible to have a non-plaintext label, and it
10638 * won't be possible to set a value (which will internally become identical to the label).
10639 */
10640 OO.ui.ButtonInputWidget = function OoUiButtonInputWidget( config ) {
10641 // Configuration initialization
10642 config = $.extend( { type: 'button', useInputTag: false }, config );
10643
10644 // Properties (must be set before parent constructor, which calls #setValue)
10645 this.useInputTag = config.useInputTag;
10646
10647 // Parent constructor
10648 OO.ui.ButtonInputWidget.super.call( this, config );
10649
10650 // Mixin constructors
10651 OO.ui.ButtonElement.call( this, $.extend( {}, config, { $button: this.$input } ) );
10652 OO.ui.IconElement.call( this, config );
10653 OO.ui.IndicatorElement.call( this, config );
10654 OO.ui.LabelElement.call( this, config );
10655 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
10656 OO.ui.FlaggedElement.call( this, config );
10657
10658 // Initialization
10659 if ( !config.useInputTag ) {
10660 this.$input.append( this.$icon, this.$label, this.$indicator );
10661 }
10662 this.$element.addClass( 'oo-ui-buttonInputWidget' );
10663 };
10664
10665 /* Setup */
10666
10667 OO.inheritClass( OO.ui.ButtonInputWidget, OO.ui.InputWidget );
10668 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.ButtonElement );
10669 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.IconElement );
10670 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.IndicatorElement );
10671 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.LabelElement );
10672 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.TitledElement );
10673 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.FlaggedElement );
10674
10675 /* Methods */
10676
10677 /**
10678 * @inheritdoc
10679 * @private
10680 */
10681 OO.ui.ButtonInputWidget.prototype.getInputElement = function ( config ) {
10682 var html = '<' + ( config.useInputTag ? 'input' : 'button' ) + ' type="' + config.type + '">';
10683 return $( html );
10684 };
10685
10686 /**
10687 * Set label value.
10688 *
10689 * Overridden to support setting the 'value' of `<input/>` elements.
10690 *
10691 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
10692 * text; or null for no label
10693 * @chainable
10694 */
10695 OO.ui.ButtonInputWidget.prototype.setLabel = function ( label ) {
10696 OO.ui.LabelElement.prototype.setLabel.call( this, label );
10697
10698 if ( this.useInputTag ) {
10699 if ( typeof label === 'function' ) {
10700 label = OO.ui.resolveMsg( label );
10701 }
10702 if ( label instanceof jQuery ) {
10703 label = label.text();
10704 }
10705 if ( !label ) {
10706 label = '';
10707 }
10708 this.$input.val( label );
10709 }
10710
10711 return this;
10712 };
10713
10714 /**
10715 * Set the value of the input.
10716 *
10717 * Overridden to disable for `<input/>` elements, which have value identical to the label.
10718 *
10719 * @param {string} value New value
10720 * @chainable
10721 */
10722 OO.ui.ButtonInputWidget.prototype.setValue = function ( value ) {
10723 if ( !this.useInputTag ) {
10724 OO.ui.ButtonInputWidget.super.prototype.setValue.call( this, value );
10725 }
10726 return this;
10727 };
10728
10729 /**
10730 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
10731 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
10732 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
10733 * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
10734 *
10735 * @example
10736 * // An example of selected, unselected, and disabled checkbox inputs
10737 * var checkbox1=new OO.ui.CheckboxInputWidget({
10738 * value: 'a',
10739 * selected: true
10740 * });
10741 * var checkbox2=new OO.ui.CheckboxInputWidget({
10742 * value: 'b'
10743 * });
10744 * var checkbox3=new OO.ui.CheckboxInputWidget( {
10745 * value:'c',
10746 * disabled: true
10747 * } );
10748 * // Create a fieldset layout with fields for each checkbox.
10749 * var fieldset = new OO.ui.FieldsetLayout( {
10750 * label: 'Checkboxes'
10751 * } );
10752 * fieldset.addItems( [
10753 * new OO.ui.FieldLayout( checkbox1, {label : 'Selected checkbox', align : 'inline'}),
10754 * new OO.ui.FieldLayout( checkbox2, {label : 'Unselected checkbox', align : 'inline'}),
10755 * new OO.ui.FieldLayout( checkbox3, {label : 'Disabled checkbox', align : 'inline'}),
10756 * ] );
10757 * $( 'body' ).append( fieldset.$element );
10758 *
10759 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
10760 *
10761 * @class
10762 * @extends OO.ui.InputWidget
10763 *
10764 * @constructor
10765 * @param {Object} [config] Configuration options
10766 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
10767 */
10768 OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
10769 // Configuration initialization
10770 config = config || {};
10771
10772 // Parent constructor
10773 OO.ui.CheckboxInputWidget.super.call( this, config );
10774
10775 // Initialization
10776 this.$element.addClass( 'oo-ui-checkboxInputWidget' );
10777 this.setSelected( config.selected !== undefined ? config.selected : false );
10778 };
10779
10780 /* Setup */
10781
10782 OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
10783
10784 /* Methods */
10785
10786 /**
10787 * @inheritdoc
10788 * @private
10789 */
10790 OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
10791 return $( '<input type="checkbox" />' );
10792 };
10793
10794 /**
10795 * @inheritdoc
10796 */
10797 OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
10798 var widget = this;
10799 if ( !this.isDisabled() ) {
10800 // Allow the stack to clear so the value will be updated
10801 setTimeout( function () {
10802 widget.setSelected( widget.$input.prop( 'checked' ) );
10803 } );
10804 }
10805 };
10806
10807 /**
10808 * Set selection state of this checkbox.
10809 *
10810 * @param {boolean} state `true` for selected
10811 * @chainable
10812 */
10813 OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state ) {
10814 state = !!state;
10815 if ( this.selected !== state ) {
10816 this.selected = state;
10817 this.$input.prop( 'checked', this.selected );
10818 this.emit( 'change', this.selected );
10819 }
10820 return this;
10821 };
10822
10823 /**
10824 * Check if this checkbox is selected.
10825 *
10826 * @return {boolean} Checkbox is selected
10827 */
10828 OO.ui.CheckboxInputWidget.prototype.isSelected = function () {
10829 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
10830 // it, and we won't know unless they're kind enough to trigger a 'change' event.
10831 var selected = this.$input.prop( 'checked' );
10832 if ( this.selected !== selected ) {
10833 this.setSelected( selected );
10834 }
10835 return this.selected;
10836 };
10837
10838 /**
10839 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
10840 * within a {@link OO.ui.FormLayout form}. The selected value is synchronized with the value
10841 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
10842 * more information about input widgets.
10843 *
10844 * @example
10845 * // Example: A DropdownInputWidget with three options
10846 * var dropDown=new OO.ui.DropdownInputWidget( {
10847 * label: 'Dropdown menu: Select a menu option',
10848 * options: [
10849 * { data: 'a', label: 'First' } ,
10850 * { data: 'b', label: 'Second'} ,
10851 * { data: 'c', label: 'Third' }
10852 * ]
10853 * } );
10854 * $('body').append(dropDown.$element);
10855 *
10856 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
10857 *
10858 * @class
10859 * @extends OO.ui.InputWidget
10860 *
10861 * @constructor
10862 * @param {Object} [config] Configuration options
10863 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
10864 */
10865 OO.ui.DropdownInputWidget = function OoUiDropdownInputWidget( config ) {
10866 // Configuration initialization
10867 config = config || {};
10868
10869 // Properties (must be done before parent constructor which calls #setDisabled)
10870 this.dropdownWidget = new OO.ui.DropdownWidget();
10871
10872 // Parent constructor
10873 OO.ui.DropdownInputWidget.super.call( this, config );
10874
10875 // Events
10876 this.dropdownWidget.getMenu().connect( this, { select: 'onMenuSelect' } );
10877
10878 // Initialization
10879 this.setOptions( config.options || [] );
10880 this.$element
10881 .addClass( 'oo-ui-dropdownInputWidget' )
10882 .append( this.dropdownWidget.$element );
10883 };
10884
10885 /* Setup */
10886
10887 OO.inheritClass( OO.ui.DropdownInputWidget, OO.ui.InputWidget );
10888
10889 /* Methods */
10890
10891 /**
10892 * @inheritdoc
10893 * @private
10894 */
10895 OO.ui.DropdownInputWidget.prototype.getInputElement = function () {
10896 return $( '<input type="hidden">' );
10897 };
10898
10899 /**
10900 * Handles menu select events.
10901 *
10902 * @private
10903 * @param {OO.ui.MenuOptionWidget} item Selected menu item
10904 */
10905 OO.ui.DropdownInputWidget.prototype.onMenuSelect = function ( item ) {
10906 this.setValue( item.getData() );
10907 };
10908
10909 /**
10910 * @inheritdoc
10911 */
10912 OO.ui.DropdownInputWidget.prototype.setValue = function ( value ) {
10913 var item = this.dropdownWidget.getMenu().getItemFromData( value );
10914 if ( item ) {
10915 this.dropdownWidget.getMenu().selectItem( item );
10916 }
10917 OO.ui.DropdownInputWidget.super.prototype.setValue.call( this, value );
10918 return this;
10919 };
10920
10921 /**
10922 * @inheritdoc
10923 */
10924 OO.ui.DropdownInputWidget.prototype.setDisabled = function ( state ) {
10925 this.dropdownWidget.setDisabled( state );
10926 OO.ui.DropdownInputWidget.super.prototype.setDisabled.call( this, state );
10927 return this;
10928 };
10929
10930 /**
10931 * Set the options available for this input.
10932 *
10933 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10934 * @chainable
10935 */
10936 OO.ui.DropdownInputWidget.prototype.setOptions = function ( options ) {
10937 var value = this.getValue();
10938
10939 // Rebuild the dropdown menu
10940 this.dropdownWidget.getMenu()
10941 .clearItems()
10942 .addItems( options.map( function ( opt ) {
10943 return new OO.ui.MenuOptionWidget( {
10944 data: opt.data,
10945 label: opt.label !== undefined ? opt.label : opt.data
10946 } );
10947 } ) );
10948
10949 // Restore the previous value, or reset to something sensible
10950 if ( this.dropdownWidget.getMenu().getItemFromData( value ) ) {
10951 // Previous value is still available, ensure consistency with the dropdown
10952 this.setValue( value );
10953 } else {
10954 // No longer valid, reset
10955 if ( options.length ) {
10956 this.setValue( options[ 0 ].data );
10957 }
10958 }
10959
10960 return this;
10961 };
10962
10963 /**
10964 * @inheritdoc
10965 */
10966 OO.ui.DropdownInputWidget.prototype.focus = function () {
10967 this.dropdownWidget.getMenu().toggle( true );
10968 return this;
10969 };
10970
10971 /**
10972 * @inheritdoc
10973 */
10974 OO.ui.DropdownInputWidget.prototype.blur = function () {
10975 this.dropdownWidget.getMenu().toggle( false );
10976 return this;
10977 };
10978
10979 /**
10980 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
10981 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
10982 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
10983 * please see the [OOjs UI documentation on MediaWiki][1].
10984 *
10985 * @example
10986 * // An example of selected, unselected, and disabled radio inputs
10987 * var radio1=new OO.ui.RadioInputWidget({
10988 * value: 'a',
10989 * selected: true
10990 * });
10991 * var radio2=new OO.ui.RadioInputWidget({
10992 * value: 'b'
10993 * });
10994 * var radio3=new OO.ui.RadioInputWidget( {
10995 * value:'c',
10996 * disabled: true
10997 * } );
10998 * // Create a fieldset layout with fields for each radio button.
10999 * var fieldset = new OO.ui.FieldsetLayout( {
11000 * label: 'Radio inputs'
11001 * } );
11002 * fieldset.addItems( [
11003 * new OO.ui.FieldLayout( radio1, {label : 'Selected', align : 'inline'}),
11004 * new OO.ui.FieldLayout( radio2, {label : 'Unselected', align : 'inline'}),
11005 * new OO.ui.FieldLayout( radio3, {label : 'Disabled', align : 'inline'}),
11006 * ] );
11007 * $( 'body' ).append( fieldset.$element );
11008 *
11009 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
11010 *
11011 * @class
11012 * @extends OO.ui.InputWidget
11013 *
11014 * @constructor
11015 * @param {Object} [config] Configuration options
11016 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
11017 */
11018 OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) {
11019 // Configuration initialization
11020 config = config || {};
11021
11022 // Parent constructor
11023 OO.ui.RadioInputWidget.super.call( this, config );
11024
11025 // Initialization
11026 this.$element.addClass( 'oo-ui-radioInputWidget' );
11027 this.setSelected( config.selected !== undefined ? config.selected : false );
11028 };
11029
11030 /* Setup */
11031
11032 OO.inheritClass( OO.ui.RadioInputWidget, OO.ui.InputWidget );
11033
11034 /* Methods */
11035
11036 /**
11037 * @inheritdoc
11038 * @private
11039 */
11040 OO.ui.RadioInputWidget.prototype.getInputElement = function () {
11041 return $( '<input type="radio" />' );
11042 };
11043
11044 /**
11045 * @inheritdoc
11046 */
11047 OO.ui.RadioInputWidget.prototype.onEdit = function () {
11048 // RadioInputWidget doesn't track its state.
11049 };
11050
11051 /**
11052 * Set selection state of this radio button.
11053 *
11054 * @param {boolean} state `true` for selected
11055 * @chainable
11056 */
11057 OO.ui.RadioInputWidget.prototype.setSelected = function ( state ) {
11058 // RadioInputWidget doesn't track its state.
11059 this.$input.prop( 'checked', state );
11060 return this;
11061 };
11062
11063 /**
11064 * Check if this radio button is selected.
11065 *
11066 * @return {boolean} Radio is selected
11067 */
11068 OO.ui.RadioInputWidget.prototype.isSelected = function () {
11069 return this.$input.prop( 'checked' );
11070 };
11071
11072 /**
11073 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
11074 * size of the field as well as its presentation. In addition, these widgets can be configured
11075 * with {@link OO.ui.IconElement icons}, {@link OO.ui.IndicatorElement indicators}, an optional
11076 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
11077 * which modifies incoming values rather than validating them.
11078 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
11079 *
11080 * @example
11081 * // Example of a text input widget
11082 * var textInput=new OO.ui.TextInputWidget( {
11083 * value: 'Text input'
11084 * } )
11085 * $('body').append(textInput.$element);
11086 *
11087 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
11088 *
11089 * @class
11090 * @extends OO.ui.InputWidget
11091 * @mixins OO.ui.IconElement
11092 * @mixins OO.ui.IndicatorElement
11093 * @mixins OO.ui.PendingElement
11094 * @mixins OO.ui.LabelElement
11095 *
11096 * @constructor
11097 * @param {Object} [config] Configuration options
11098 * @cfg {string} [type='text'] The value of the HTML `type` attribute
11099 * @cfg {string} [placeholder] Placeholder text
11100 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
11101 * instruct the browser to focus this widget.
11102 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
11103 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
11104 * @cfg {boolean} [multiline=false] Allow multiple lines of text
11105 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
11106 * Use the #maxRows config to specify a maximum number of displayed rows.
11107 * @cfg {boolean} [maxRows=10] Maximum number of rows to display when #autosize is set to true.
11108 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
11109 * the value or placeholder text: `'before'` or `'after'`
11110 * @cfg {boolean} [required=false] Mark the field as required
11111 * @cfg {RegExp|string} [validate] Validation pattern, either a regular expression or the
11112 * symbolic name of a pattern defined by the class: 'non-empty' (the value cannot be an empty string)
11113 * or 'integer' (the value must contain only numbers).
11114 */
11115 OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
11116 // Configuration initialization
11117 config = $.extend( {
11118 type: 'text',
11119 labelPosition: 'after',
11120 maxRows: 10
11121 }, config );
11122
11123 // Parent constructor
11124 OO.ui.TextInputWidget.super.call( this, config );
11125
11126 // Mixin constructors
11127 OO.ui.IconElement.call( this, config );
11128 OO.ui.IndicatorElement.call( this, config );
11129 OO.ui.PendingElement.call( this, config );
11130 OO.ui.LabelElement.call( this, config );
11131
11132 // Properties
11133 this.readOnly = false;
11134 this.multiline = !!config.multiline;
11135 this.autosize = !!config.autosize;
11136 this.maxRows = config.maxRows;
11137 this.validate = null;
11138
11139 // Clone for resizing
11140 if ( this.autosize ) {
11141 this.$clone = this.$input
11142 .clone()
11143 .insertAfter( this.$input )
11144 .attr( 'aria-hidden', 'true' )
11145 .addClass( 'oo-ui-element-hidden' );
11146 }
11147
11148 this.setValidation( config.validate );
11149 this.setLabelPosition( config.labelPosition );
11150
11151 // Events
11152 this.$input.on( {
11153 keypress: this.onKeyPress.bind( this ),
11154 blur: this.setValidityFlag.bind( this )
11155 } );
11156 this.$element.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach.bind( this ) );
11157 this.$icon.on( 'mousedown', this.onIconMouseDown.bind( this ) );
11158 this.$indicator.on( 'mousedown', this.onIndicatorMouseDown.bind( this ) );
11159 this.on( 'labelChange', this.updatePosition.bind( this ) );
11160
11161 // Initialization
11162 this.$element
11163 .addClass( 'oo-ui-textInputWidget' )
11164 .append( this.$icon, this.$indicator );
11165 this.setReadOnly( !!config.readOnly );
11166 if ( config.placeholder ) {
11167 this.$input.attr( 'placeholder', config.placeholder );
11168 }
11169 if ( config.maxLength !== undefined ) {
11170 this.$input.attr( 'maxlength', config.maxLength );
11171 }
11172 if ( config.autofocus ) {
11173 this.$input.attr( 'autofocus', 'autofocus' );
11174 }
11175 if ( config.required ) {
11176 this.$input.attr( 'required', 'true' );
11177 }
11178 };
11179
11180 /* Setup */
11181
11182 OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
11183 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IconElement );
11184 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IndicatorElement );
11185 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.PendingElement );
11186 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.LabelElement );
11187
11188 /* Static properties */
11189
11190 OO.ui.TextInputWidget.static.validationPatterns = {
11191 'non-empty': /.+/,
11192 integer: /^\d+$/
11193 };
11194
11195 /* Events */
11196
11197 /**
11198 * An `enter` event is emitted when the user presses 'enter' inside the text box.
11199 *
11200 * Not emitted if the input is multiline.
11201 *
11202 * @event enter
11203 */
11204
11205 /* Methods */
11206
11207 /**
11208 * Handle icon mouse down events.
11209 *
11210 * @private
11211 * @param {jQuery.Event} e Mouse down event
11212 * @fires icon
11213 */
11214 OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
11215 if ( e.which === 1 ) {
11216 this.$input[ 0 ].focus();
11217 return false;
11218 }
11219 };
11220
11221 /**
11222 * Handle indicator mouse down events.
11223 *
11224 * @private
11225 * @param {jQuery.Event} e Mouse down event
11226 * @fires indicator
11227 */
11228 OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
11229 if ( e.which === 1 ) {
11230 this.$input[ 0 ].focus();
11231 return false;
11232 }
11233 };
11234
11235 /**
11236 * Handle key press events.
11237 *
11238 * @private
11239 * @param {jQuery.Event} e Key press event
11240 * @fires enter If enter key is pressed and input is not multiline
11241 */
11242 OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
11243 if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) {
11244 this.emit( 'enter', e );
11245 }
11246 };
11247
11248 /**
11249 * Handle element attach events.
11250 *
11251 * @private
11252 * @param {jQuery.Event} e Element attach event
11253 */
11254 OO.ui.TextInputWidget.prototype.onElementAttach = function () {
11255 // Any previously calculated size is now probably invalid if we reattached elsewhere
11256 this.valCache = null;
11257 this.adjustSize();
11258 this.positionLabel();
11259 };
11260
11261 /**
11262 * @inheritdoc
11263 */
11264 OO.ui.TextInputWidget.prototype.onEdit = function () {
11265 this.adjustSize();
11266
11267 // Parent method
11268 return OO.ui.TextInputWidget.super.prototype.onEdit.call( this );
11269 };
11270
11271 /**
11272 * @inheritdoc
11273 */
11274 OO.ui.TextInputWidget.prototype.setValue = function ( value ) {
11275 // Parent method
11276 OO.ui.TextInputWidget.super.prototype.setValue.call( this, value );
11277
11278 this.setValidityFlag();
11279 this.adjustSize();
11280 return this;
11281 };
11282
11283 /**
11284 * Check if the input is {@link #readOnly read-only}.
11285 *
11286 * @return {boolean}
11287 */
11288 OO.ui.TextInputWidget.prototype.isReadOnly = function () {
11289 return this.readOnly;
11290 };
11291
11292 /**
11293 * Set the {@link #readOnly read-only} state of the input.
11294 *
11295 * @param {boolean} state Make input read-only
11296 * @chainable
11297 */
11298 OO.ui.TextInputWidget.prototype.setReadOnly = function ( state ) {
11299 this.readOnly = !!state;
11300 this.$input.prop( 'readOnly', this.readOnly );
11301 return this;
11302 };
11303
11304 /**
11305 * Automatically adjust the size of the text input.
11306 *
11307 * This only affects #multiline inputs that are {@link #autosize autosized}.
11308 *
11309 * @chainable
11310 */
11311 OO.ui.TextInputWidget.prototype.adjustSize = function () {
11312 var scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError, idealHeight;
11313
11314 if ( this.multiline && this.autosize && this.$input.val() !== this.valCache ) {
11315 this.$clone
11316 .val( this.$input.val() )
11317 .attr( 'rows', '' )
11318 // Set inline height property to 0 to measure scroll height
11319 .css( 'height', 0 );
11320
11321 this.$clone.removeClass( 'oo-ui-element-hidden' );
11322
11323 this.valCache = this.$input.val();
11324
11325 scrollHeight = this.$clone[ 0 ].scrollHeight;
11326
11327 // Remove inline height property to measure natural heights
11328 this.$clone.css( 'height', '' );
11329 innerHeight = this.$clone.innerHeight();
11330 outerHeight = this.$clone.outerHeight();
11331
11332 // Measure max rows height
11333 this.$clone
11334 .attr( 'rows', this.maxRows )
11335 .css( 'height', 'auto' )
11336 .val( '' );
11337 maxInnerHeight = this.$clone.innerHeight();
11338
11339 // Difference between reported innerHeight and scrollHeight with no scrollbars present
11340 // Equals 1 on Blink-based browsers and 0 everywhere else
11341 measurementError = maxInnerHeight - this.$clone[ 0 ].scrollHeight;
11342 idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
11343
11344 this.$clone.addClass( 'oo-ui-element-hidden' );
11345
11346 // Only apply inline height when expansion beyond natural height is needed
11347 if ( idealHeight > innerHeight ) {
11348 // Use the difference between the inner and outer height as a buffer
11349 this.$input.css( 'height', idealHeight + ( outerHeight - innerHeight ) );
11350 } else {
11351 this.$input.css( 'height', '' );
11352 }
11353 }
11354 return this;
11355 };
11356
11357 /**
11358 * @inheritdoc
11359 * @private
11360 */
11361 OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
11362 return config.multiline ? $( '<textarea>' ) : $( '<input type="' + config.type + '" />' );
11363 };
11364
11365 /**
11366 * Check if the input supports multiple lines.
11367 *
11368 * @return {boolean}
11369 */
11370 OO.ui.TextInputWidget.prototype.isMultiline = function () {
11371 return !!this.multiline;
11372 };
11373
11374 /**
11375 * Check if the input automatically adjusts its size.
11376 *
11377 * @return {boolean}
11378 */
11379 OO.ui.TextInputWidget.prototype.isAutosizing = function () {
11380 return !!this.autosize;
11381 };
11382
11383 /**
11384 * Select the entire text of the input.
11385 *
11386 * @chainable
11387 */
11388 OO.ui.TextInputWidget.prototype.select = function () {
11389 this.$input.select();
11390 return this;
11391 };
11392
11393 /**
11394 * Set the validation pattern.
11395 *
11396 * The validation pattern is either a regular expression or the symbolic name of a pattern
11397 * defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
11398 * value must contain only numbers).
11399 *
11400 * @param {RegExp|string|null} validate Regular expression or the symbolic name of a
11401 * pattern (either ‘integer’ or ‘non-empty’) defined by the class.
11402 */
11403 OO.ui.TextInputWidget.prototype.setValidation = function ( validate ) {
11404 if ( validate instanceof RegExp ) {
11405 this.validate = validate;
11406 } else {
11407 this.validate = this.constructor.static.validationPatterns[ validate ] || /.*/;
11408 }
11409 };
11410
11411 /**
11412 * Sets the 'invalid' flag appropriately.
11413 */
11414 OO.ui.TextInputWidget.prototype.setValidityFlag = function () {
11415 var widget = this;
11416 this.isValid().done( function ( valid ) {
11417 widget.setFlags( { invalid: !valid } );
11418 } );
11419 };
11420
11421 /**
11422 * Check if a value is valid.
11423 *
11424 * This method returns a promise that resolves with a boolean `true` if the current value is
11425 * considered valid according to the supplied {@link #validate validation pattern}.
11426 *
11427 * @return {jQuery.Deferred} A promise that resolves to a boolean `true` if the value is valid.
11428 */
11429 OO.ui.TextInputWidget.prototype.isValid = function () {
11430 return $.Deferred().resolve( !!this.getValue().match( this.validate ) ).promise();
11431 };
11432
11433 /**
11434 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
11435 *
11436 * @param {string} labelPosition Label position, 'before' or 'after'
11437 * @chainable
11438 */
11439 OO.ui.TextInputWidget.prototype.setLabelPosition = function ( labelPosition ) {
11440 this.labelPosition = labelPosition;
11441 this.updatePosition();
11442 return this;
11443 };
11444
11445 /**
11446 * Deprecated alias of #setLabelPosition
11447 *
11448 * @deprecated Use setLabelPosition instead.
11449 */
11450 OO.ui.TextInputWidget.prototype.setPosition =
11451 OO.ui.TextInputWidget.prototype.setLabelPosition;
11452
11453 /**
11454 * Update the position of the inline label.
11455 *
11456 * This method is called by #setLabelPosition, and can also be called on its own if
11457 * something causes the label to be mispositioned.
11458 *
11459 *
11460 * @chainable
11461 */
11462 OO.ui.TextInputWidget.prototype.updatePosition = function () {
11463 var after = this.labelPosition === 'after';
11464
11465 this.$element
11466 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label && after )
11467 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label && !after );
11468
11469 if ( this.label ) {
11470 this.positionLabel();
11471 }
11472
11473 return this;
11474 };
11475
11476 /**
11477 * Position the label by setting the correct padding on the input.
11478 *
11479 * @private
11480 * @chainable
11481 */
11482 OO.ui.TextInputWidget.prototype.positionLabel = function () {
11483 // Clear old values
11484 this.$input
11485 // Clear old values if present
11486 .css( {
11487 'padding-right': '',
11488 'padding-left': ''
11489 } );
11490
11491 if ( this.label ) {
11492 this.$element.append( this.$label );
11493 } else {
11494 this.$label.detach();
11495 return;
11496 }
11497
11498 var after = this.labelPosition === 'after',
11499 rtl = this.$element.css( 'direction' ) === 'rtl',
11500 property = after === rtl ? 'padding-left' : 'padding-right';
11501
11502 this.$input.css( property, this.$label.outerWidth( true ) );
11503
11504 return this;
11505 };
11506
11507 /**
11508 * ComboBoxWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
11509 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
11510 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
11511 *
11512 * - by typing a value in the text input field. If the value exactly matches the value of a menu
11513 * option, that option will appear to be selected.
11514 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
11515 * input field.
11516 *
11517 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
11518 *
11519 * @example
11520 * // Example: A ComboBoxWidget.
11521 * var comboBox=new OO.ui.ComboBoxWidget( {
11522 * label: 'ComboBoxWidget',
11523 * input: { value: 'Option One' },
11524 * menu: {
11525 * items: [
11526 * new OO.ui.MenuOptionWidget( {
11527 * data: 'Option 1',
11528 * label: 'Option One' } ),
11529 * new OO.ui.MenuOptionWidget( {
11530 * data: 'Option 2',
11531 * label: 'Option Two' } ),
11532 * new OO.ui.MenuOptionWidget( {
11533 * data: 'Option 3',
11534 * label: 'Option Three'} ),
11535 * new OO.ui.MenuOptionWidget( {
11536 * data: 'Option 4',
11537 * label: 'Option Four' } ),
11538 * new OO.ui.MenuOptionWidget( {
11539 * data: 'Option 5',
11540 * label: 'Option Five' } )
11541 * ]
11542 * }
11543 * } );
11544 * $('body').append(comboBox.$element);
11545 *
11546 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
11547 *
11548 * @class
11549 * @extends OO.ui.Widget
11550 * @mixins OO.ui.TabIndexedElement
11551 *
11552 * @constructor
11553 * @param {Object} [config] Configuration options
11554 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}.
11555 * @cfg {Object} [input] Configuration options to pass to the {@link OO.ui.TextInputWidget text input widget}.
11556 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
11557 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
11558 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
11559 */
11560 OO.ui.ComboBoxWidget = function OoUiComboBoxWidget( config ) {
11561 // Configuration initialization
11562 config = config || {};
11563
11564 // Parent constructor
11565 OO.ui.ComboBoxWidget.super.call( this, config );
11566
11567 // Properties (must be set before TabIndexedElement constructor call)
11568 this.$indicator = this.$( '<span>' );
11569
11570 // Mixin constructors
11571 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$indicator } ) );
11572
11573 // Properties
11574 this.$overlay = config.$overlay || this.$element;
11575 this.input = new OO.ui.TextInputWidget( $.extend(
11576 {
11577 indicator: 'down',
11578 $indicator: this.$indicator,
11579 disabled: this.isDisabled()
11580 },
11581 config.input
11582 ) );
11583 this.input.$input.eq( 0 ).attr( {
11584 role: 'combobox',
11585 'aria-autocomplete': 'list'
11586 } );
11587 this.menu = new OO.ui.TextInputMenuSelectWidget( this.input, $.extend(
11588 {
11589 widget: this,
11590 input: this.input,
11591 disabled: this.isDisabled()
11592 },
11593 config.menu
11594 ) );
11595
11596 // Events
11597 this.$indicator.on( {
11598 click: this.onClick.bind( this ),
11599 keypress: this.onKeyPress.bind( this )
11600 } );
11601 this.input.connect( this, {
11602 change: 'onInputChange',
11603 enter: 'onInputEnter'
11604 } );
11605 this.menu.connect( this, {
11606 choose: 'onMenuChoose',
11607 add: 'onMenuItemsChange',
11608 remove: 'onMenuItemsChange'
11609 } );
11610
11611 // Initialization
11612 this.$element.addClass( 'oo-ui-comboBoxWidget' ).append( this.input.$element );
11613 this.$overlay.append( this.menu.$element );
11614 this.onMenuItemsChange();
11615 };
11616
11617 /* Setup */
11618
11619 OO.inheritClass( OO.ui.ComboBoxWidget, OO.ui.Widget );
11620 OO.mixinClass( OO.ui.ComboBoxWidget, OO.ui.TabIndexedElement );
11621
11622 /* Methods */
11623
11624 /**
11625 * Get the combobox's menu.
11626 * @return {OO.ui.TextInputMenuSelectWidget} Menu widget
11627 */
11628 OO.ui.ComboBoxWidget.prototype.getMenu = function () {
11629 return this.menu;
11630 };
11631
11632 /**
11633 * Handle input change events.
11634 *
11635 * @private
11636 * @param {string} value New value
11637 */
11638 OO.ui.ComboBoxWidget.prototype.onInputChange = function ( value ) {
11639 var match = this.menu.getItemFromData( value );
11640
11641 this.menu.selectItem( match );
11642 if ( this.menu.getHighlightedItem() ) {
11643 this.menu.highlightItem( match );
11644 }
11645
11646 if ( !this.isDisabled() ) {
11647 this.menu.toggle( true );
11648 }
11649 };
11650
11651 /**
11652 * Handle mouse click events.
11653 *
11654 *
11655 * @private
11656 * @param {jQuery.Event} e Mouse click event
11657 */
11658 OO.ui.ComboBoxWidget.prototype.onClick = function ( e ) {
11659 if ( !this.isDisabled() && e.which === 1 ) {
11660 this.menu.toggle();
11661 this.input.$input[ 0 ].focus();
11662 }
11663 return false;
11664 };
11665
11666 /**
11667 * Handle key press events.
11668 *
11669 *
11670 * @private
11671 * @param {jQuery.Event} e Key press event
11672 */
11673 OO.ui.ComboBoxWidget.prototype.onKeyPress = function ( e ) {
11674 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
11675 this.menu.toggle();
11676 this.input.$input[ 0 ].focus();
11677 }
11678 return false;
11679 };
11680
11681 /**
11682 * Handle input enter events.
11683 *
11684 * @private
11685 */
11686 OO.ui.ComboBoxWidget.prototype.onInputEnter = function () {
11687 if ( !this.isDisabled() ) {
11688 this.menu.toggle( false );
11689 }
11690 };
11691
11692 /**
11693 * Handle menu choose events.
11694 *
11695 * @private
11696 * @param {OO.ui.OptionWidget} item Chosen item
11697 */
11698 OO.ui.ComboBoxWidget.prototype.onMenuChoose = function ( item ) {
11699 if ( item ) {
11700 this.input.setValue( item.getData() );
11701 }
11702 };
11703
11704 /**
11705 * Handle menu item change events.
11706 *
11707 * @private
11708 */
11709 OO.ui.ComboBoxWidget.prototype.onMenuItemsChange = function () {
11710 var match = this.menu.getItemFromData( this.input.getValue() );
11711 this.menu.selectItem( match );
11712 if ( this.menu.getHighlightedItem() ) {
11713 this.menu.highlightItem( match );
11714 }
11715 this.$element.toggleClass( 'oo-ui-comboBoxWidget-empty', this.menu.isEmpty() );
11716 };
11717
11718 /**
11719 * @inheritdoc
11720 */
11721 OO.ui.ComboBoxWidget.prototype.setDisabled = function ( disabled ) {
11722 // Parent method
11723 OO.ui.ComboBoxWidget.super.prototype.setDisabled.call( this, disabled );
11724
11725 if ( this.input ) {
11726 this.input.setDisabled( this.isDisabled() );
11727 }
11728 if ( this.menu ) {
11729 this.menu.setDisabled( this.isDisabled() );
11730 }
11731
11732 return this;
11733 };
11734
11735 /**
11736 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
11737 * be configured with a `label` option that is set to a string, a label node, or a function:
11738 *
11739 * - String: a plaintext string
11740 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
11741 * label that includes a link or special styling, such as a gray color or additional graphical elements.
11742 * - Function: a function that will produce a string in the future. Functions are used
11743 * in cases where the value of the label is not currently defined.
11744 *
11745 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
11746 * will come into focus when the label is clicked.
11747 *
11748 * @example
11749 * // Examples of LabelWidgets
11750 * var label1 = new OO.ui.LabelWidget({
11751 * label: 'plaintext label'
11752 * });
11753 * var label2 = new OO.ui.LabelWidget({
11754 * label: $( '<a href="default.html">jQuery label</a>' )
11755 * });
11756 * // Create a fieldset layout with fields for each example
11757 * var fieldset = new OO.ui.FieldsetLayout( );
11758 * fieldset.addItems( [
11759 * new OO.ui.FieldLayout( label1 ),
11760 * new OO.ui.FieldLayout( label2 )
11761 * ] );
11762 * $( 'body' ).append( fieldset.$element );
11763 *
11764 *
11765 * @class
11766 * @extends OO.ui.Widget
11767 * @mixins OO.ui.LabelElement
11768 *
11769 * @constructor
11770 * @param {Object} [config] Configuration options
11771 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
11772 * Clicking the label will focus the specified input field.
11773 */
11774 OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
11775 // Configuration initialization
11776 config = config || {};
11777
11778 // Parent constructor
11779 OO.ui.LabelWidget.super.call( this, config );
11780
11781 // Mixin constructors
11782 OO.ui.LabelElement.call( this, $.extend( {}, config, { $label: this.$element } ) );
11783 OO.ui.TitledElement.call( this, config );
11784
11785 // Properties
11786 this.input = config.input;
11787
11788 // Events
11789 if ( this.input instanceof OO.ui.InputWidget ) {
11790 this.$element.on( 'click', this.onClick.bind( this ) );
11791 }
11792
11793 // Initialization
11794 this.$element.addClass( 'oo-ui-labelWidget' );
11795 };
11796
11797 /* Setup */
11798
11799 OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
11800 OO.mixinClass( OO.ui.LabelWidget, OO.ui.LabelElement );
11801 OO.mixinClass( OO.ui.LabelWidget, OO.ui.TitledElement );
11802
11803 /* Static Properties */
11804
11805 OO.ui.LabelWidget.static.tagName = 'span';
11806
11807 /* Methods */
11808
11809 /**
11810 * Handles label mouse click events.
11811 *
11812 * @private
11813 * @param {jQuery.Event} e Mouse click event
11814 */
11815 OO.ui.LabelWidget.prototype.onClick = function () {
11816 this.input.simulateLabelClick();
11817 return false;
11818 };
11819
11820 /**
11821 * OptionWidgets are special elements that can be selected and configured with data. The
11822 * data is often unique for each option, but it does not have to be. OptionWidgets are used
11823 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
11824 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
11825 *
11826 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
11827 *
11828 * @class
11829 * @extends OO.ui.Widget
11830 * @mixins OO.ui.LabelElement
11831 * @mixins OO.ui.FlaggedElement
11832 *
11833 * @constructor
11834 * @param {Object} [config] Configuration options
11835 */
11836 OO.ui.OptionWidget = function OoUiOptionWidget( config ) {
11837 // Configuration initialization
11838 config = config || {};
11839
11840 // Parent constructor
11841 OO.ui.OptionWidget.super.call( this, config );
11842
11843 // Mixin constructors
11844 OO.ui.ItemWidget.call( this );
11845 OO.ui.LabelElement.call( this, config );
11846 OO.ui.FlaggedElement.call( this, config );
11847
11848 // Properties
11849 this.selected = false;
11850 this.highlighted = false;
11851 this.pressed = false;
11852
11853 // Initialization
11854 this.$element
11855 .data( 'oo-ui-optionWidget', this )
11856 .attr( 'role', 'option' )
11857 .addClass( 'oo-ui-optionWidget' )
11858 .append( this.$label );
11859 };
11860
11861 /* Setup */
11862
11863 OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
11864 OO.mixinClass( OO.ui.OptionWidget, OO.ui.ItemWidget );
11865 OO.mixinClass( OO.ui.OptionWidget, OO.ui.LabelElement );
11866 OO.mixinClass( OO.ui.OptionWidget, OO.ui.FlaggedElement );
11867
11868 /* Static Properties */
11869
11870 OO.ui.OptionWidget.static.selectable = true;
11871
11872 OO.ui.OptionWidget.static.highlightable = true;
11873
11874 OO.ui.OptionWidget.static.pressable = true;
11875
11876 OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
11877
11878 /* Methods */
11879
11880 /**
11881 * Check if the option can be selected.
11882 *
11883 * @return {boolean} Item is selectable
11884 */
11885 OO.ui.OptionWidget.prototype.isSelectable = function () {
11886 return this.constructor.static.selectable && !this.isDisabled();
11887 };
11888
11889 /**
11890 * Check if the option can be highlighted. A highlight indicates that the option
11891 * may be selected when a user presses enter or clicks. Disabled items cannot
11892 * be highlighted.
11893 *
11894 * @return {boolean} Item is highlightable
11895 */
11896 OO.ui.OptionWidget.prototype.isHighlightable = function () {
11897 return this.constructor.static.highlightable && !this.isDisabled();
11898 };
11899
11900 /**
11901 * Check if the option can be pressed. The pressed state occurs when a user mouses
11902 * down on an item, but has not yet let go of the mouse.
11903 *
11904 * @return {boolean} Item is pressable
11905 */
11906 OO.ui.OptionWidget.prototype.isPressable = function () {
11907 return this.constructor.static.pressable && !this.isDisabled();
11908 };
11909
11910 /**
11911 * Check if the option is selected.
11912 *
11913 * @return {boolean} Item is selected
11914 */
11915 OO.ui.OptionWidget.prototype.isSelected = function () {
11916 return this.selected;
11917 };
11918
11919 /**
11920 * Check if the option is highlighted. A highlight indicates that the
11921 * item may be selected when a user presses enter or clicks.
11922 *
11923 * @return {boolean} Item is highlighted
11924 */
11925 OO.ui.OptionWidget.prototype.isHighlighted = function () {
11926 return this.highlighted;
11927 };
11928
11929 /**
11930 * Check if the option is pressed. The pressed state occurs when a user mouses
11931 * down on an item, but has not yet let go of the mouse. The item may appear
11932 * selected, but it will not be selected until the user releases the mouse.
11933 *
11934 * @return {boolean} Item is pressed
11935 */
11936 OO.ui.OptionWidget.prototype.isPressed = function () {
11937 return this.pressed;
11938 };
11939
11940 /**
11941 * Set the option’s selected state. In general, all modifications to the selection
11942 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
11943 * method instead of this method.
11944 *
11945 * @param {boolean} [state=false] Select option
11946 * @chainable
11947 */
11948 OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
11949 if ( this.constructor.static.selectable ) {
11950 this.selected = !!state;
11951 this.$element
11952 .toggleClass( 'oo-ui-optionWidget-selected', state )
11953 .attr( 'aria-selected', state.toString() );
11954 if ( state && this.constructor.static.scrollIntoViewOnSelect ) {
11955 this.scrollElementIntoView();
11956 }
11957 this.updateThemeClasses();
11958 }
11959 return this;
11960 };
11961
11962 /**
11963 * Set the option’s highlighted state. In general, all programmatic
11964 * modifications to the highlight should be handled by the
11965 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
11966 * method instead of this method.
11967 *
11968 * @param {boolean} [state=false] Highlight option
11969 * @chainable
11970 */
11971 OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
11972 if ( this.constructor.static.highlightable ) {
11973 this.highlighted = !!state;
11974 this.$element.toggleClass( 'oo-ui-optionWidget-highlighted', state );
11975 this.updateThemeClasses();
11976 }
11977 return this;
11978 };
11979
11980 /**
11981 * Set the option’s pressed state. In general, all
11982 * programmatic modifications to the pressed state should be handled by the
11983 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
11984 * method instead of this method.
11985 *
11986 * @param {boolean} [state=false] Press option
11987 * @chainable
11988 */
11989 OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
11990 if ( this.constructor.static.pressable ) {
11991 this.pressed = !!state;
11992 this.$element.toggleClass( 'oo-ui-optionWidget-pressed', state );
11993 this.updateThemeClasses();
11994 }
11995 return this;
11996 };
11997
11998 /**
11999 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
12000 * with an {@link OO.ui.IconElement icon} and/or {@link OO.ui.IndicatorElement indicator}.
12001 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
12002 * options. For more information about options and selects, please see the
12003 * [OOjs UI documentation on MediaWiki][1].
12004 *
12005 * @example
12006 * // Decorated options in a select widget
12007 * var select=new OO.ui.SelectWidget( {
12008 * items: [
12009 * new OO.ui.DecoratedOptionWidget( {
12010 * data: 'a',
12011 * label: 'Option with icon',
12012 * icon: 'help'
12013 * } ),
12014 * new OO.ui.DecoratedOptionWidget( {
12015 * data: 'b',
12016 * label: 'Option with indicator',
12017 * indicator: 'next'
12018 * } )
12019 * ]
12020 * } );
12021 * $('body').append(select.$element);
12022 *
12023 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
12024 *
12025 * @class
12026 * @extends OO.ui.OptionWidget
12027 * @mixins OO.ui.IconElement
12028 * @mixins OO.ui.IndicatorElement
12029 *
12030 * @constructor
12031 * @param {Object} [config] Configuration options
12032 */
12033 OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( config ) {
12034 // Parent constructor
12035 OO.ui.DecoratedOptionWidget.super.call( this, config );
12036
12037 // Mixin constructors
12038 OO.ui.IconElement.call( this, config );
12039 OO.ui.IndicatorElement.call( this, config );
12040
12041 // Initialization
12042 this.$element
12043 .addClass( 'oo-ui-decoratedOptionWidget' )
12044 .prepend( this.$icon )
12045 .append( this.$indicator );
12046 };
12047
12048 /* Setup */
12049
12050 OO.inheritClass( OO.ui.DecoratedOptionWidget, OO.ui.OptionWidget );
12051 OO.mixinClass( OO.ui.OptionWidget, OO.ui.IconElement );
12052 OO.mixinClass( OO.ui.OptionWidget, OO.ui.IndicatorElement );
12053
12054 /**
12055 * ButtonOptionWidget is a special type of {@link OO.ui.ButtonElement button element} that
12056 * can be selected and configured with data. The class is
12057 * used with OO.ui.ButtonSelectWidget to create a selection of button options. Please see the
12058 * [OOjs UI documentation on MediaWiki] [1] for more information.
12059 *
12060 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_options
12061 *
12062 * @class
12063 * @extends OO.ui.DecoratedOptionWidget
12064 * @mixins OO.ui.ButtonElement
12065 * @mixins OO.ui.TabIndexedElement
12066 *
12067 * @constructor
12068 * @param {Object} [config] Configuration options
12069 */
12070 OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( config ) {
12071 // Configuration initialization
12072 config = $.extend( { tabIndex: -1 }, config );
12073
12074 // Parent constructor
12075 OO.ui.ButtonOptionWidget.super.call( this, config );
12076
12077 // Mixin constructors
12078 OO.ui.ButtonElement.call( this, config );
12079 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
12080
12081 // Initialization
12082 this.$element.addClass( 'oo-ui-buttonOptionWidget' );
12083 this.$button.append( this.$element.contents() );
12084 this.$element.append( this.$button );
12085 };
12086
12087 /* Setup */
12088
12089 OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.DecoratedOptionWidget );
12090 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.ButtonElement );
12091 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.TabIndexedElement );
12092
12093 /* Static Properties */
12094
12095 // Allow button mouse down events to pass through so they can be handled by the parent select widget
12096 OO.ui.ButtonOptionWidget.static.cancelButtonMouseDownEvents = false;
12097
12098 OO.ui.ButtonOptionWidget.static.highlightable = false;
12099
12100 /* Methods */
12101
12102 /**
12103 * @inheritdoc
12104 */
12105 OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) {
12106 OO.ui.ButtonOptionWidget.super.prototype.setSelected.call( this, state );
12107
12108 if ( this.constructor.static.selectable ) {
12109 this.setActive( state );
12110 }
12111
12112 return this;
12113 };
12114
12115 /**
12116 * RadioOptionWidget is an option widget that looks like a radio button.
12117 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
12118 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
12119 *
12120 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
12121 *
12122 * @class
12123 * @extends OO.ui.OptionWidget
12124 *
12125 * @constructor
12126 * @param {Object} [config] Configuration options
12127 */
12128 OO.ui.RadioOptionWidget = function OoUiRadioOptionWidget( config ) {
12129 // Configuration initialization
12130 config = config || {};
12131
12132 // Properties (must be done before parent constructor which calls #setDisabled)
12133 this.radio = new OO.ui.RadioInputWidget( { value: config.data, tabIndex: -1 } );
12134
12135 // Parent constructor
12136 OO.ui.RadioOptionWidget.super.call( this, config );
12137
12138 // Initialization
12139 this.$element
12140 .addClass( 'oo-ui-radioOptionWidget' )
12141 .prepend( this.radio.$element );
12142 };
12143
12144 /* Setup */
12145
12146 OO.inheritClass( OO.ui.RadioOptionWidget, OO.ui.OptionWidget );
12147
12148 /* Static Properties */
12149
12150 OO.ui.RadioOptionWidget.static.highlightable = false;
12151
12152 OO.ui.RadioOptionWidget.static.scrollIntoViewOnSelect = true;
12153
12154 OO.ui.RadioOptionWidget.static.pressable = false;
12155
12156 OO.ui.RadioOptionWidget.static.tagName = 'label';
12157
12158 /* Methods */
12159
12160 /**
12161 * @inheritdoc
12162 */
12163 OO.ui.RadioOptionWidget.prototype.setSelected = function ( state ) {
12164 OO.ui.RadioOptionWidget.super.prototype.setSelected.call( this, state );
12165
12166 this.radio.setSelected( state );
12167
12168 return this;
12169 };
12170
12171 /**
12172 * @inheritdoc
12173 */
12174 OO.ui.RadioOptionWidget.prototype.setDisabled = function ( disabled ) {
12175 OO.ui.RadioOptionWidget.super.prototype.setDisabled.call( this, disabled );
12176
12177 this.radio.setDisabled( this.isDisabled() );
12178
12179 return this;
12180 };
12181
12182 /**
12183 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
12184 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
12185 * the [OOjs UI documentation on MediaWiki] [1] for more information.
12186 *
12187 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
12188 *
12189 * @class
12190 * @extends OO.ui.DecoratedOptionWidget
12191 *
12192 * @constructor
12193 * @param {Object} [config] Configuration options
12194 */
12195 OO.ui.MenuOptionWidget = function OoUiMenuOptionWidget( config ) {
12196 // Configuration initialization
12197 config = $.extend( { icon: 'check' }, config );
12198
12199 // Parent constructor
12200 OO.ui.MenuOptionWidget.super.call( this, config );
12201
12202 // Initialization
12203 this.$element
12204 .attr( 'role', 'menuitem' )
12205 .addClass( 'oo-ui-menuOptionWidget' );
12206 };
12207
12208 /* Setup */
12209
12210 OO.inheritClass( OO.ui.MenuOptionWidget, OO.ui.DecoratedOptionWidget );
12211
12212 /* Static Properties */
12213
12214 OO.ui.MenuOptionWidget.static.scrollIntoViewOnSelect = true;
12215
12216 /**
12217 * Section to group one or more items in a OO.ui.MenuSelectWidget.
12218 *
12219 * @class
12220 * @extends OO.ui.DecoratedOptionWidget
12221 *
12222 * @constructor
12223 * @param {Object} [config] Configuration options
12224 */
12225 OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( config ) {
12226 // Parent constructor
12227 OO.ui.MenuSectionOptionWidget.super.call( this, config );
12228
12229 // Initialization
12230 this.$element.addClass( 'oo-ui-menuSectionOptionWidget' );
12231 };
12232
12233 /* Setup */
12234
12235 OO.inheritClass( OO.ui.MenuSectionOptionWidget, OO.ui.DecoratedOptionWidget );
12236
12237 /* Static Properties */
12238
12239 OO.ui.MenuSectionOptionWidget.static.selectable = false;
12240
12241 OO.ui.MenuSectionOptionWidget.static.highlightable = false;
12242
12243 /**
12244 * Items for an OO.ui.OutlineSelectWidget.
12245 *
12246 * @class
12247 * @extends OO.ui.DecoratedOptionWidget
12248 *
12249 * @constructor
12250 * @param {Object} [config] Configuration options
12251 * @cfg {number} [level] Indentation level
12252 * @cfg {boolean} [movable] Allow modification from outline controls
12253 */
12254 OO.ui.OutlineOptionWidget = function OoUiOutlineOptionWidget( config ) {
12255 // Configuration initialization
12256 config = config || {};
12257
12258 // Parent constructor
12259 OO.ui.OutlineOptionWidget.super.call( this, config );
12260
12261 // Properties
12262 this.level = 0;
12263 this.movable = !!config.movable;
12264 this.removable = !!config.removable;
12265
12266 // Initialization
12267 this.$element.addClass( 'oo-ui-outlineOptionWidget' );
12268 this.setLevel( config.level );
12269 };
12270
12271 /* Setup */
12272
12273 OO.inheritClass( OO.ui.OutlineOptionWidget, OO.ui.DecoratedOptionWidget );
12274
12275 /* Static Properties */
12276
12277 OO.ui.OutlineOptionWidget.static.highlightable = false;
12278
12279 OO.ui.OutlineOptionWidget.static.scrollIntoViewOnSelect = true;
12280
12281 OO.ui.OutlineOptionWidget.static.levelClass = 'oo-ui-outlineOptionWidget-level-';
12282
12283 OO.ui.OutlineOptionWidget.static.levels = 3;
12284
12285 /* Methods */
12286
12287 /**
12288 * Check if item is movable.
12289 *
12290 * Movability is used by outline controls.
12291 *
12292 * @return {boolean} Item is movable
12293 */
12294 OO.ui.OutlineOptionWidget.prototype.isMovable = function () {
12295 return this.movable;
12296 };
12297
12298 /**
12299 * Check if item is removable.
12300 *
12301 * Removability is used by outline controls.
12302 *
12303 * @return {boolean} Item is removable
12304 */
12305 OO.ui.OutlineOptionWidget.prototype.isRemovable = function () {
12306 return this.removable;
12307 };
12308
12309 /**
12310 * Get indentation level.
12311 *
12312 * @return {number} Indentation level
12313 */
12314 OO.ui.OutlineOptionWidget.prototype.getLevel = function () {
12315 return this.level;
12316 };
12317
12318 /**
12319 * Set movability.
12320 *
12321 * Movability is used by outline controls.
12322 *
12323 * @param {boolean} movable Item is movable
12324 * @chainable
12325 */
12326 OO.ui.OutlineOptionWidget.prototype.setMovable = function ( movable ) {
12327 this.movable = !!movable;
12328 this.updateThemeClasses();
12329 return this;
12330 };
12331
12332 /**
12333 * Set removability.
12334 *
12335 * Removability is used by outline controls.
12336 *
12337 * @param {boolean} movable Item is removable
12338 * @chainable
12339 */
12340 OO.ui.OutlineOptionWidget.prototype.setRemovable = function ( removable ) {
12341 this.removable = !!removable;
12342 this.updateThemeClasses();
12343 return this;
12344 };
12345
12346 /**
12347 * Set indentation level.
12348 *
12349 * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
12350 * @chainable
12351 */
12352 OO.ui.OutlineOptionWidget.prototype.setLevel = function ( level ) {
12353 var levels = this.constructor.static.levels,
12354 levelClass = this.constructor.static.levelClass,
12355 i = levels;
12356
12357 this.level = level ? Math.max( 0, Math.min( levels - 1, level ) ) : 0;
12358 while ( i-- ) {
12359 if ( this.level === i ) {
12360 this.$element.addClass( levelClass + i );
12361 } else {
12362 this.$element.removeClass( levelClass + i );
12363 }
12364 }
12365 this.updateThemeClasses();
12366
12367 return this;
12368 };
12369
12370 /**
12371 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
12372 * By default, each popup has an anchor that points toward its origin.
12373 * Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples.
12374 *
12375 * @example
12376 * // A popup widget.
12377 * var popup=new OO.ui.PopupWidget({
12378 * $content: $( '<p>Hi there!</p>' ),
12379 * padded: true,
12380 * width: 300
12381 * } );
12382 *
12383 * $('body').append(popup.$element);
12384 * // To display the popup, toggle the visibility to 'true'.
12385 * popup.toggle(true);
12386 *
12387 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups
12388 *
12389 * @class
12390 * @extends OO.ui.Widget
12391 * @mixins OO.ui.LabelElement
12392 *
12393 * @constructor
12394 * @param {Object} [config] Configuration options
12395 * @cfg {number} [width=320] Width of popup in pixels
12396 * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
12397 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
12398 * @cfg {string} [align='center'] Alignment of the popup: `center`, `left`, or `right`.
12399 * If the popup is right-aligned, the right edge of the popup is aligned to the anchor.
12400 * For left-aligned popups, the left edge is aligned to the anchor.
12401 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
12402 * See the [OOjs UI docs on MediaWiki][3] for an example.
12403 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
12404 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
12405 * @cfg {jQuery} [$content] Content to append to the popup's body
12406 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
12407 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
12408 * This config option is only relevant if #autoClose is set to `true`. See the [OOjs UI docs on MediaWiki][2]
12409 * for an example.
12410 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#autocloseExample
12411 * @cfg {boolean} [head] Show a popup header that contains a #label (if specified) and close
12412 * button.
12413 * @cfg {boolean} [padded] Add padding to the popup's body
12414 */
12415 OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
12416 // Configuration initialization
12417 config = config || {};
12418
12419 // Parent constructor
12420 OO.ui.PopupWidget.super.call( this, config );
12421
12422 // Properties (must be set before ClippableElement constructor call)
12423 this.$body = $( '<div>' );
12424
12425 // Mixin constructors
12426 OO.ui.LabelElement.call( this, config );
12427 OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$body } ) );
12428
12429 // Properties
12430 this.$popup = $( '<div>' );
12431 this.$head = $( '<div>' );
12432 this.$anchor = $( '<div>' );
12433 // If undefined, will be computed lazily in updateDimensions()
12434 this.$container = config.$container;
12435 this.containerPadding = config.containerPadding !== undefined ? config.containerPadding : 10;
12436 this.autoClose = !!config.autoClose;
12437 this.$autoCloseIgnore = config.$autoCloseIgnore;
12438 this.transitionTimeout = null;
12439 this.anchor = null;
12440 this.width = config.width !== undefined ? config.width : 320;
12441 this.height = config.height !== undefined ? config.height : null;
12442 this.align = config.align || 'center';
12443 this.closeButton = new OO.ui.ButtonWidget( { framed: false, icon: 'close' } );
12444 this.onMouseDownHandler = this.onMouseDown.bind( this );
12445 this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
12446
12447 // Events
12448 this.closeButton.connect( this, { click: 'onCloseButtonClick' } );
12449
12450 // Initialization
12451 this.toggleAnchor( config.anchor === undefined || config.anchor );
12452 this.$body.addClass( 'oo-ui-popupWidget-body' );
12453 this.$anchor.addClass( 'oo-ui-popupWidget-anchor' );
12454 this.$head
12455 .addClass( 'oo-ui-popupWidget-head' )
12456 .append( this.$label, this.closeButton.$element );
12457 if ( !config.head ) {
12458 this.$head.addClass( 'oo-ui-element-hidden' );
12459 }
12460 this.$popup
12461 .addClass( 'oo-ui-popupWidget-popup' )
12462 .append( this.$head, this.$body );
12463 this.$element
12464 .addClass( 'oo-ui-popupWidget' )
12465 .append( this.$popup, this.$anchor );
12466 // Move content, which was added to #$element by OO.ui.Widget, to the body
12467 if ( config.$content instanceof jQuery ) {
12468 this.$body.append( config.$content );
12469 }
12470 if ( config.padded ) {
12471 this.$body.addClass( 'oo-ui-popupWidget-body-padded' );
12472 }
12473
12474 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
12475 // that reference properties not initialized at that time of parent class construction
12476 // TODO: Find a better way to handle post-constructor setup
12477 this.visible = false;
12478 this.$element.addClass( 'oo-ui-element-hidden' );
12479 };
12480
12481 /* Setup */
12482
12483 OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
12484 OO.mixinClass( OO.ui.PopupWidget, OO.ui.LabelElement );
12485 OO.mixinClass( OO.ui.PopupWidget, OO.ui.ClippableElement );
12486
12487 /* Methods */
12488
12489 /**
12490 * Handles mouse down events.
12491 *
12492 * @private
12493 * @param {MouseEvent} e Mouse down event
12494 */
12495 OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
12496 if (
12497 this.isVisible() &&
12498 !$.contains( this.$element[ 0 ], e.target ) &&
12499 ( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length )
12500 ) {
12501 this.toggle( false );
12502 }
12503 };
12504
12505 /**
12506 * Bind mouse down listener.
12507 *
12508 * @private
12509 */
12510 OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
12511 // Capture clicks outside popup
12512 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
12513 };
12514
12515 /**
12516 * Handles close button click events.
12517 *
12518 * @private
12519 */
12520 OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
12521 if ( this.isVisible() ) {
12522 this.toggle( false );
12523 }
12524 };
12525
12526 /**
12527 * Unbind mouse down listener.
12528 *
12529 * @private
12530 */
12531 OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
12532 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
12533 };
12534
12535 /**
12536 * Handles key down events.
12537 *
12538 * @private
12539 * @param {KeyboardEvent} e Key down event
12540 */
12541 OO.ui.PopupWidget.prototype.onDocumentKeyDown = function ( e ) {
12542 if (
12543 e.which === OO.ui.Keys.ESCAPE &&
12544 this.isVisible()
12545 ) {
12546 this.toggle( false );
12547 e.preventDefault();
12548 e.stopPropagation();
12549 }
12550 };
12551
12552 /**
12553 * Bind key down listener.
12554 *
12555 * @private
12556 */
12557 OO.ui.PopupWidget.prototype.bindKeyDownListener = function () {
12558 this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
12559 };
12560
12561 /**
12562 * Unbind key down listener.
12563 *
12564 * @private
12565 */
12566 OO.ui.PopupWidget.prototype.unbindKeyDownListener = function () {
12567 this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
12568 };
12569
12570 /**
12571 * Show, hide, or toggle the visibility of the anchor.
12572 *
12573 * @param {boolean} [show] Show anchor, omit to toggle
12574 */
12575 OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) {
12576 show = show === undefined ? !this.anchored : !!show;
12577
12578 if ( this.anchored !== show ) {
12579 if ( show ) {
12580 this.$element.addClass( 'oo-ui-popupWidget-anchored' );
12581 } else {
12582 this.$element.removeClass( 'oo-ui-popupWidget-anchored' );
12583 }
12584 this.anchored = show;
12585 }
12586 };
12587
12588 /**
12589 * Check if the anchor is visible.
12590 *
12591 * @return {boolean} Anchor is visible
12592 */
12593 OO.ui.PopupWidget.prototype.hasAnchor = function () {
12594 return this.anchor;
12595 };
12596
12597 /**
12598 * @inheritdoc
12599 */
12600 OO.ui.PopupWidget.prototype.toggle = function ( show ) {
12601 show = show === undefined ? !this.isVisible() : !!show;
12602
12603 var change = show !== this.isVisible();
12604
12605 // Parent method
12606 OO.ui.PopupWidget.super.prototype.toggle.call( this, show );
12607
12608 if ( change ) {
12609 if ( show ) {
12610 if ( this.autoClose ) {
12611 this.bindMouseDownListener();
12612 this.bindKeyDownListener();
12613 }
12614 this.updateDimensions();
12615 this.toggleClipping( true );
12616 } else {
12617 this.toggleClipping( false );
12618 if ( this.autoClose ) {
12619 this.unbindMouseDownListener();
12620 this.unbindKeyDownListener();
12621 }
12622 }
12623 }
12624
12625 return this;
12626 };
12627
12628 /**
12629 * Set the size of the popup.
12630 *
12631 * Changing the size may also change the popup's position depending on the alignment.
12632 *
12633 * @param {number} width Width in pixels
12634 * @param {number} height Height in pixels
12635 * @param {boolean} [transition=false] Use a smooth transition
12636 * @chainable
12637 */
12638 OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) {
12639 this.width = width;
12640 this.height = height !== undefined ? height : null;
12641 if ( this.isVisible() ) {
12642 this.updateDimensions( transition );
12643 }
12644 };
12645
12646 /**
12647 * Update the size and position.
12648 *
12649 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
12650 * be called automatically.
12651 *
12652 * @param {boolean} [transition=false] Use a smooth transition
12653 * @chainable
12654 */
12655 OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
12656 var popupOffset, originOffset, containerLeft, containerWidth, containerRight,
12657 popupLeft, popupRight, overlapLeft, overlapRight, anchorWidth,
12658 widget = this;
12659
12660 if ( !this.$container ) {
12661 // Lazy-initialize $container if not specified in constructor
12662 this.$container = $( this.getClosestScrollableElementContainer() );
12663 }
12664
12665 // Set height and width before measuring things, since it might cause our measurements
12666 // to change (e.g. due to scrollbars appearing or disappearing)
12667 this.$popup.css( {
12668 width: this.width,
12669 height: this.height !== null ? this.height : 'auto'
12670 } );
12671
12672 // Compute initial popupOffset based on alignment
12673 popupOffset = this.width * ( { left: 0, center: -0.5, right: -1 } )[ this.align ];
12674
12675 // Figure out if this will cause the popup to go beyond the edge of the container
12676 originOffset = this.$element.offset().left;
12677 containerLeft = this.$container.offset().left;
12678 containerWidth = this.$container.innerWidth();
12679 containerRight = containerLeft + containerWidth;
12680 popupLeft = popupOffset - this.containerPadding;
12681 popupRight = popupOffset + this.containerPadding + this.width + this.containerPadding;
12682 overlapLeft = ( originOffset + popupLeft ) - containerLeft;
12683 overlapRight = containerRight - ( originOffset + popupRight );
12684
12685 // Adjust offset to make the popup not go beyond the edge, if needed
12686 if ( overlapRight < 0 ) {
12687 popupOffset += overlapRight;
12688 } else if ( overlapLeft < 0 ) {
12689 popupOffset -= overlapLeft;
12690 }
12691
12692 // Adjust offset to avoid anchor being rendered too close to the edge
12693 // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
12694 // TODO: Find a measurement that works for CSS anchors and image anchors
12695 anchorWidth = this.$anchor[ 0 ].scrollWidth * 2;
12696 if ( popupOffset + this.width < anchorWidth ) {
12697 popupOffset = anchorWidth - this.width;
12698 } else if ( -popupOffset < anchorWidth ) {
12699 popupOffset = -anchorWidth;
12700 }
12701
12702 // Prevent transition from being interrupted
12703 clearTimeout( this.transitionTimeout );
12704 if ( transition ) {
12705 // Enable transition
12706 this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
12707 }
12708
12709 // Position body relative to anchor
12710 this.$popup.css( 'margin-left', popupOffset );
12711
12712 if ( transition ) {
12713 // Prevent transitioning after transition is complete
12714 this.transitionTimeout = setTimeout( function () {
12715 widget.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
12716 }, 200 );
12717 } else {
12718 // Prevent transitioning immediately
12719 this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
12720 }
12721
12722 // Reevaluate clipping state since we've relocated and resized the popup
12723 this.clip();
12724
12725 return this;
12726 };
12727
12728 /**
12729 * Progress bars visually display the status of an operation, such as a download,
12730 * and can be either determinate or indeterminate:
12731 *
12732 * - **determinate** process bars show the percent of an operation that is complete.
12733 *
12734 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
12735 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
12736 * not use percentages.
12737 *
12738 * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
12739 *
12740 * @example
12741 * // Examples of determinate and indeterminate progress bars.
12742 * var progressBar1=new OO.ui.ProgressBarWidget( {
12743 * progress: 33
12744 * } );
12745 *
12746 * var progressBar2=new OO.ui.ProgressBarWidget( {
12747 * progress: false
12748 * } );
12749 * // Create a FieldsetLayout to layout progress bars
12750 * var fieldset = new OO.ui.FieldsetLayout;
12751 * fieldset.addItems( [
12752 * new OO.ui.FieldLayout( progressBar1, {label : 'Determinate', align : 'top'}),
12753 * new OO.ui.FieldLayout( progressBar2, {label : 'Indeterminate', align : 'top'})
12754 * ] );
12755 * $( 'body' ).append( fieldset.$element );
12756 *
12757 * @class
12758 * @extends OO.ui.Widget
12759 *
12760 * @constructor
12761 * @param {Object} [config] Configuration options
12762 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
12763 * To create a determinate progress bar, specify a number that reflects the initial percent complete.
12764 * By default, the progress bar is indeterminate.
12765 */
12766 OO.ui.ProgressBarWidget = function OoUiProgressBarWidget( config ) {
12767 // Configuration initialization
12768 config = config || {};
12769
12770 // Parent constructor
12771 OO.ui.ProgressBarWidget.super.call( this, config );
12772
12773 // Properties
12774 this.$bar = $( '<div>' );
12775 this.progress = null;
12776
12777 // Initialization
12778 this.setProgress( config.progress !== undefined ? config.progress : false );
12779 this.$bar.addClass( 'oo-ui-progressBarWidget-bar' );
12780 this.$element
12781 .attr( {
12782 role: 'progressbar',
12783 'aria-valuemin': 0,
12784 'aria-valuemax': 100
12785 } )
12786 .addClass( 'oo-ui-progressBarWidget' )
12787 .append( this.$bar );
12788 };
12789
12790 /* Setup */
12791
12792 OO.inheritClass( OO.ui.ProgressBarWidget, OO.ui.Widget );
12793
12794 /* Static Properties */
12795
12796 OO.ui.ProgressBarWidget.static.tagName = 'div';
12797
12798 /* Methods */
12799
12800 /**
12801 * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
12802 *
12803 * @return {number|boolean} Progress percent
12804 */
12805 OO.ui.ProgressBarWidget.prototype.getProgress = function () {
12806 return this.progress;
12807 };
12808
12809 /**
12810 * Set the percent of the process completed or `false` for an indeterminate process.
12811 *
12812 * @param {number|boolean} progress Progress percent or `false` for indeterminate
12813 */
12814 OO.ui.ProgressBarWidget.prototype.setProgress = function ( progress ) {
12815 this.progress = progress;
12816
12817 if ( progress !== false ) {
12818 this.$bar.css( 'width', this.progress + '%' );
12819 this.$element.attr( 'aria-valuenow', this.progress );
12820 } else {
12821 this.$bar.css( 'width', '' );
12822 this.$element.removeAttr( 'aria-valuenow' );
12823 }
12824 this.$element.toggleClass( 'oo-ui-progressBarWidget-indeterminate', !progress );
12825 };
12826
12827 /**
12828 * Search widget.
12829 *
12830 * Search widgets combine a query input, placed above, and a results selection widget, placed below.
12831 * Results are cleared and populated each time the query is changed.
12832 *
12833 * @class
12834 * @extends OO.ui.Widget
12835 *
12836 * @constructor
12837 * @param {Object} [config] Configuration options
12838 * @cfg {string|jQuery} [placeholder] Placeholder text for query input
12839 * @cfg {string} [value] Initial query value
12840 */
12841 OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
12842 // Configuration initialization
12843 config = config || {};
12844
12845 // Parent constructor
12846 OO.ui.SearchWidget.super.call( this, config );
12847
12848 // Properties
12849 this.query = new OO.ui.TextInputWidget( {
12850 icon: 'search',
12851 placeholder: config.placeholder,
12852 value: config.value
12853 } );
12854 this.results = new OO.ui.SelectWidget();
12855 this.$query = $( '<div>' );
12856 this.$results = $( '<div>' );
12857
12858 // Events
12859 this.query.connect( this, {
12860 change: 'onQueryChange',
12861 enter: 'onQueryEnter'
12862 } );
12863 this.results.connect( this, {
12864 highlight: 'onResultsHighlight',
12865 select: 'onResultsSelect'
12866 } );
12867 this.query.$input.on( 'keydown', this.onQueryKeydown.bind( this ) );
12868
12869 // Initialization
12870 this.$query
12871 .addClass( 'oo-ui-searchWidget-query' )
12872 .append( this.query.$element );
12873 this.$results
12874 .addClass( 'oo-ui-searchWidget-results' )
12875 .append( this.results.$element );
12876 this.$element
12877 .addClass( 'oo-ui-searchWidget' )
12878 .append( this.$results, this.$query );
12879 };
12880
12881 /* Setup */
12882
12883 OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget );
12884
12885 /* Events */
12886
12887 /**
12888 * @event highlight
12889 * @param {Object|null} item Item data or null if no item is highlighted
12890 */
12891
12892 /**
12893 * @event select
12894 * @param {Object|null} item Item data or null if no item is selected
12895 */
12896
12897 /* Methods */
12898
12899 /**
12900 * Handle query key down events.
12901 *
12902 * @param {jQuery.Event} e Key down event
12903 */
12904 OO.ui.SearchWidget.prototype.onQueryKeydown = function ( e ) {
12905 var highlightedItem, nextItem,
12906 dir = e.which === OO.ui.Keys.DOWN ? 1 : ( e.which === OO.ui.Keys.UP ? -1 : 0 );
12907
12908 if ( dir ) {
12909 highlightedItem = this.results.getHighlightedItem();
12910 if ( !highlightedItem ) {
12911 highlightedItem = this.results.getSelectedItem();
12912 }
12913 nextItem = this.results.getRelativeSelectableItem( highlightedItem, dir );
12914 this.results.highlightItem( nextItem );
12915 nextItem.scrollElementIntoView();
12916 }
12917 };
12918
12919 /**
12920 * Handle select widget select events.
12921 *
12922 * Clears existing results. Subclasses should repopulate items according to new query.
12923 *
12924 * @param {string} value New value
12925 */
12926 OO.ui.SearchWidget.prototype.onQueryChange = function () {
12927 // Reset
12928 this.results.clearItems();
12929 };
12930
12931 /**
12932 * Handle select widget enter key events.
12933 *
12934 * Selects highlighted item.
12935 *
12936 * @param {string} value New value
12937 */
12938 OO.ui.SearchWidget.prototype.onQueryEnter = function () {
12939 // Reset
12940 this.results.selectItem( this.results.getHighlightedItem() );
12941 };
12942
12943 /**
12944 * Handle select widget highlight events.
12945 *
12946 * @param {OO.ui.OptionWidget} item Highlighted item
12947 * @fires highlight
12948 */
12949 OO.ui.SearchWidget.prototype.onResultsHighlight = function ( item ) {
12950 this.emit( 'highlight', item ? item.getData() : null );
12951 };
12952
12953 /**
12954 * Handle select widget select events.
12955 *
12956 * @param {OO.ui.OptionWidget} item Selected item
12957 * @fires select
12958 */
12959 OO.ui.SearchWidget.prototype.onResultsSelect = function ( item ) {
12960 this.emit( 'select', item ? item.getData() : null );
12961 };
12962
12963 /**
12964 * Get the query input.
12965 *
12966 * @return {OO.ui.TextInputWidget} Query input
12967 */
12968 OO.ui.SearchWidget.prototype.getQuery = function () {
12969 return this.query;
12970 };
12971
12972 /**
12973 * Get the results list.
12974 *
12975 * @return {OO.ui.SelectWidget} Select list
12976 */
12977 OO.ui.SearchWidget.prototype.getResults = function () {
12978 return this.results;
12979 };
12980
12981 /**
12982 * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
12983 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
12984 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
12985 * menu selects}.
12986 *
12987 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
12988 * information, please see the [OOjs UI documentation on MediaWiki][1].
12989 *
12990 * @example
12991 * // Example of a select widget with three options
12992 * var select=new OO.ui.SelectWidget( {
12993 * items: [
12994 * new OO.ui.OptionWidget( {
12995 * data: 'a',
12996 * label: 'Option One',
12997 * } ),
12998 * new OO.ui.OptionWidget( {
12999 * data: 'b',
13000 * label: 'Option Two',
13001 * } ),
13002 * new OO.ui.OptionWidget( {
13003 * data: 'c',
13004 * label: 'Option Three',
13005 * } ),
13006 * ]
13007 * } );
13008 * $('body').append(select.$element);
13009 *
13010 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
13011 *
13012 * @class
13013 * @extends OO.ui.Widget
13014 * @mixins OO.ui.GroupElement
13015 *
13016 * @constructor
13017 * @param {Object} [config] Configuration options
13018 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
13019 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
13020 * the [OOjs UI documentation on MediaWiki] [2] for examples.
13021 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
13022 */
13023 OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
13024 // Configuration initialization
13025 config = config || {};
13026
13027 // Parent constructor
13028 OO.ui.SelectWidget.super.call( this, config );
13029
13030 // Mixin constructors
13031 OO.ui.GroupWidget.call( this, $.extend( {}, config, { $group: this.$element } ) );
13032
13033 // Properties
13034 this.pressed = false;
13035 this.selecting = null;
13036 this.onMouseUpHandler = this.onMouseUp.bind( this );
13037 this.onMouseMoveHandler = this.onMouseMove.bind( this );
13038 this.onKeyDownHandler = this.onKeyDown.bind( this );
13039
13040 // Events
13041 this.$element.on( {
13042 mousedown: this.onMouseDown.bind( this ),
13043 mouseover: this.onMouseOver.bind( this ),
13044 mouseleave: this.onMouseLeave.bind( this )
13045 } );
13046
13047 // Initialization
13048 this.$element
13049 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
13050 .attr( 'role', 'listbox' );
13051 if ( Array.isArray( config.items ) ) {
13052 this.addItems( config.items );
13053 }
13054 };
13055
13056 /* Setup */
13057
13058 OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
13059
13060 // Need to mixin base class as well
13061 OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupElement );
13062 OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupWidget );
13063
13064 /* Events */
13065
13066 /**
13067 * @event highlight
13068 *
13069 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
13070 *
13071 * @param {OO.ui.OptionWidget|null} item Highlighted item
13072 */
13073
13074 /**
13075 * @event press
13076 *
13077 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
13078 * pressed state of an option.
13079 *
13080 * @param {OO.ui.OptionWidget|null} item Pressed item
13081 */
13082
13083 /**
13084 * @event select
13085 *
13086 * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
13087 *
13088 * @param {OO.ui.OptionWidget|null} item Selected item
13089 */
13090
13091 /**
13092 * @event choose
13093 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
13094 * @param {OO.ui.OptionWidget|null} item Chosen item
13095 */
13096
13097 /**
13098 * @event add
13099 *
13100 * An `add` event is emitted when options are added to the select with the #addItems method.
13101 *
13102 * @param {OO.ui.OptionWidget[]} items Added items
13103 * @param {number} index Index of insertion point
13104 */
13105
13106 /**
13107 * @event remove
13108 *
13109 * A `remove` event is emitted when options are removed from the select with the #clearItems
13110 * or #removeItems methods.
13111 *
13112 * @param {OO.ui.OptionWidget[]} items Removed items
13113 */
13114
13115 /* Methods */
13116
13117 /**
13118 * Handle mouse down events.
13119 *
13120 * @private
13121 * @param {jQuery.Event} e Mouse down event
13122 */
13123 OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
13124 var item;
13125
13126 if ( !this.isDisabled() && e.which === 1 ) {
13127 this.togglePressed( true );
13128 item = this.getTargetItem( e );
13129 if ( item && item.isSelectable() ) {
13130 this.pressItem( item );
13131 this.selecting = item;
13132 this.getElementDocument().addEventListener(
13133 'mouseup',
13134 this.onMouseUpHandler,
13135 true
13136 );
13137 this.getElementDocument().addEventListener(
13138 'mousemove',
13139 this.onMouseMoveHandler,
13140 true
13141 );
13142 }
13143 }
13144 return false;
13145 };
13146
13147 /**
13148 * Handle mouse up events.
13149 *
13150 * @private
13151 * @param {jQuery.Event} e Mouse up event
13152 */
13153 OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
13154 var item;
13155
13156 this.togglePressed( false );
13157 if ( !this.selecting ) {
13158 item = this.getTargetItem( e );
13159 if ( item && item.isSelectable() ) {
13160 this.selecting = item;
13161 }
13162 }
13163 if ( !this.isDisabled() && e.which === 1 && this.selecting ) {
13164 this.pressItem( null );
13165 this.chooseItem( this.selecting );
13166 this.selecting = null;
13167 }
13168
13169 this.getElementDocument().removeEventListener(
13170 'mouseup',
13171 this.onMouseUpHandler,
13172 true
13173 );
13174 this.getElementDocument().removeEventListener(
13175 'mousemove',
13176 this.onMouseMoveHandler,
13177 true
13178 );
13179
13180 return false;
13181 };
13182
13183 /**
13184 * Handle mouse move events.
13185 *
13186 * @private
13187 * @param {jQuery.Event} e Mouse move event
13188 */
13189 OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
13190 var item;
13191
13192 if ( !this.isDisabled() && this.pressed ) {
13193 item = this.getTargetItem( e );
13194 if ( item && item !== this.selecting && item.isSelectable() ) {
13195 this.pressItem( item );
13196 this.selecting = item;
13197 }
13198 }
13199 return false;
13200 };
13201
13202 /**
13203 * Handle mouse over events.
13204 *
13205 * @private
13206 * @param {jQuery.Event} e Mouse over event
13207 */
13208 OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
13209 var item;
13210
13211 if ( !this.isDisabled() ) {
13212 item = this.getTargetItem( e );
13213 this.highlightItem( item && item.isHighlightable() ? item : null );
13214 }
13215 return false;
13216 };
13217
13218 /**
13219 * Handle mouse leave events.
13220 *
13221 * @private
13222 * @param {jQuery.Event} e Mouse over event
13223 */
13224 OO.ui.SelectWidget.prototype.onMouseLeave = function () {
13225 if ( !this.isDisabled() ) {
13226 this.highlightItem( null );
13227 }
13228 return false;
13229 };
13230
13231 /**
13232 * Handle key down events.
13233 *
13234 * @protected
13235 * @param {jQuery.Event} e Key down event
13236 */
13237 OO.ui.SelectWidget.prototype.onKeyDown = function ( e ) {
13238 var nextItem,
13239 handled = false,
13240 currentItem = this.getHighlightedItem() || this.getSelectedItem();
13241
13242 if ( !this.isDisabled() && this.isVisible() ) {
13243 switch ( e.keyCode ) {
13244 case OO.ui.Keys.ENTER:
13245 if ( currentItem && currentItem.constructor.static.highlightable ) {
13246 // Was only highlighted, now let's select it. No-op if already selected.
13247 this.chooseItem( currentItem );
13248 handled = true;
13249 }
13250 break;
13251 case OO.ui.Keys.UP:
13252 case OO.ui.Keys.LEFT:
13253 nextItem = this.getRelativeSelectableItem( currentItem, -1 );
13254 handled = true;
13255 break;
13256 case OO.ui.Keys.DOWN:
13257 case OO.ui.Keys.RIGHT:
13258 nextItem = this.getRelativeSelectableItem( currentItem, 1 );
13259 handled = true;
13260 break;
13261 case OO.ui.Keys.ESCAPE:
13262 case OO.ui.Keys.TAB:
13263 if ( currentItem && currentItem.constructor.static.highlightable ) {
13264 currentItem.setHighlighted( false );
13265 }
13266 this.unbindKeyDownListener();
13267 // Don't prevent tabbing away / defocusing
13268 handled = false;
13269 break;
13270 }
13271
13272 if ( nextItem ) {
13273 if ( nextItem.constructor.static.highlightable ) {
13274 this.highlightItem( nextItem );
13275 } else {
13276 this.chooseItem( nextItem );
13277 }
13278 nextItem.scrollElementIntoView();
13279 }
13280
13281 if ( handled ) {
13282 // Can't just return false, because e is not always a jQuery event
13283 e.preventDefault();
13284 e.stopPropagation();
13285 }
13286 }
13287 };
13288
13289 /**
13290 * Bind key down listener.
13291 *
13292 * @protected
13293 */
13294 OO.ui.SelectWidget.prototype.bindKeyDownListener = function () {
13295 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
13296 };
13297
13298 /**
13299 * Unbind key down listener.
13300 *
13301 * @protected
13302 */
13303 OO.ui.SelectWidget.prototype.unbindKeyDownListener = function () {
13304 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
13305 };
13306
13307 /**
13308 * Get the closest item to a jQuery.Event.
13309 *
13310 * @private
13311 * @param {jQuery.Event} e
13312 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
13313 */
13314 OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) {
13315 var $item = $( e.target ).closest( '.oo-ui-optionWidget' );
13316 if ( $item.length ) {
13317 return $item.data( 'oo-ui-optionWidget' );
13318 }
13319 return null;
13320 };
13321
13322 /**
13323 * Get selected item.
13324 *
13325 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
13326 */
13327 OO.ui.SelectWidget.prototype.getSelectedItem = function () {
13328 var i, len;
13329
13330 for ( i = 0, len = this.items.length; i < len; i++ ) {
13331 if ( this.items[ i ].isSelected() ) {
13332 return this.items[ i ];
13333 }
13334 }
13335 return null;
13336 };
13337
13338 /**
13339 * Get highlighted item.
13340 *
13341 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
13342 */
13343 OO.ui.SelectWidget.prototype.getHighlightedItem = function () {
13344 var i, len;
13345
13346 for ( i = 0, len = this.items.length; i < len; i++ ) {
13347 if ( this.items[ i ].isHighlighted() ) {
13348 return this.items[ i ];
13349 }
13350 }
13351 return null;
13352 };
13353
13354 /**
13355 * Toggle pressed state.
13356 *
13357 * Press is a state that occurs when a user mouses down on an item, but
13358 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
13359 * until the user releases the mouse.
13360 *
13361 * @param {boolean} pressed An option is being pressed
13362 */
13363 OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) {
13364 if ( pressed === undefined ) {
13365 pressed = !this.pressed;
13366 }
13367 if ( pressed !== this.pressed ) {
13368 this.$element
13369 .toggleClass( 'oo-ui-selectWidget-pressed', pressed )
13370 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed );
13371 this.pressed = pressed;
13372 }
13373 };
13374
13375 /**
13376 * Highlight an option. If the `item` param is omitted, no options will be highlighted
13377 * and any existing highlight will be removed. The highlight is mutually exclusive.
13378 *
13379 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
13380 * @fires highlight
13381 * @chainable
13382 */
13383 OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
13384 var i, len, highlighted,
13385 changed = false;
13386
13387 for ( i = 0, len = this.items.length; i < len; i++ ) {
13388 highlighted = this.items[ i ] === item;
13389 if ( this.items[ i ].isHighlighted() !== highlighted ) {
13390 this.items[ i ].setHighlighted( highlighted );
13391 changed = true;
13392 }
13393 }
13394 if ( changed ) {
13395 this.emit( 'highlight', item );
13396 }
13397
13398 return this;
13399 };
13400
13401 /**
13402 * Programmatically select an option by its reference. If the `item` parameter is omitted,
13403 * all options will be deselected.
13404 *
13405 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
13406 * @fires select
13407 * @chainable
13408 */
13409 OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
13410 var i, len, selected,
13411 changed = false;
13412
13413 for ( i = 0, len = this.items.length; i < len; i++ ) {
13414 selected = this.items[ i ] === item;
13415 if ( this.items[ i ].isSelected() !== selected ) {
13416 this.items[ i ].setSelected( selected );
13417 changed = true;
13418 }
13419 }
13420 if ( changed ) {
13421 this.emit( 'select', item );
13422 }
13423
13424 return this;
13425 };
13426
13427 /**
13428 * Press an item.
13429 *
13430 * Press is a state that occurs when a user mouses down on an item, but has not
13431 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
13432 * releases the mouse.
13433 *
13434 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
13435 * @fires press
13436 * @chainable
13437 */
13438 OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
13439 var i, len, pressed,
13440 changed = false;
13441
13442 for ( i = 0, len = this.items.length; i < len; i++ ) {
13443 pressed = this.items[ i ] === item;
13444 if ( this.items[ i ].isPressed() !== pressed ) {
13445 this.items[ i ].setPressed( pressed );
13446 changed = true;
13447 }
13448 }
13449 if ( changed ) {
13450 this.emit( 'press', item );
13451 }
13452
13453 return this;
13454 };
13455
13456 /**
13457 * Choose an item.
13458 *
13459 * Note that ‘choose’ should never be modified programmatically. A user can choose
13460 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
13461 * use the #selectItem method.
13462 *
13463 * This method is identical to #selectItem, but may vary in subclasses that take additional action
13464 * when users choose an item with the keyboard or mouse.
13465 *
13466 * @param {OO.ui.OptionWidget} item Item to choose
13467 * @fires choose
13468 * @chainable
13469 */
13470 OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
13471 this.selectItem( item );
13472 this.emit( 'choose', item );
13473
13474 return this;
13475 };
13476
13477 /**
13478 * Get an option by its position relative to the specified item (or to the start of the option array,
13479 * if item is `null`). The direction in which to search through the option array is specified with a
13480 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
13481 * `null` if there are no options in the array.
13482 *
13483 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
13484 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
13485 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
13486 */
13487 OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction ) {
13488 var currentIndex, nextIndex, i,
13489 increase = direction > 0 ? 1 : -1,
13490 len = this.items.length;
13491
13492 if ( item instanceof OO.ui.OptionWidget ) {
13493 currentIndex = $.inArray( item, this.items );
13494 nextIndex = ( currentIndex + increase + len ) % len;
13495 } else {
13496 // If no item is selected and moving forward, start at the beginning.
13497 // If moving backward, start at the end.
13498 nextIndex = direction > 0 ? 0 : len - 1;
13499 }
13500
13501 for ( i = 0; i < len; i++ ) {
13502 item = this.items[ nextIndex ];
13503 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
13504 return item;
13505 }
13506 nextIndex = ( nextIndex + increase + len ) % len;
13507 }
13508 return null;
13509 };
13510
13511 /**
13512 * Get the next selectable item or `null` if there are no selectable items.
13513 * Disabled options and menu-section markers and breaks are not selectable.
13514 *
13515 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
13516 */
13517 OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
13518 var i, len, item;
13519
13520 for ( i = 0, len = this.items.length; i < len; i++ ) {
13521 item = this.items[ i ];
13522 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
13523 return item;
13524 }
13525 }
13526
13527 return null;
13528 };
13529
13530 /**
13531 * Add an array of options to the select. Optionally, an index number can be used to
13532 * specify an insertion point.
13533 *
13534 * @param {OO.ui.OptionWidget[]} items Items to add
13535 * @param {number} [index] Index to insert items after
13536 * @fires add
13537 * @chainable
13538 */
13539 OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
13540 // Mixin method
13541 OO.ui.GroupWidget.prototype.addItems.call( this, items, index );
13542
13543 // Always provide an index, even if it was omitted
13544 this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index );
13545
13546 return this;
13547 };
13548
13549 /**
13550 * Remove the specified array of options from the select. Options will be detached
13551 * from the DOM, not removed, so they can be reused later. To remove all options from
13552 * the select, you may wish to use the #clearItems method instead.
13553 *
13554 * @param {OO.ui.OptionWidget[]} items Items to remove
13555 * @fires remove
13556 * @chainable
13557 */
13558 OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
13559 var i, len, item;
13560
13561 // Deselect items being removed
13562 for ( i = 0, len = items.length; i < len; i++ ) {
13563 item = items[ i ];
13564 if ( item.isSelected() ) {
13565 this.selectItem( null );
13566 }
13567 }
13568
13569 // Mixin method
13570 OO.ui.GroupWidget.prototype.removeItems.call( this, items );
13571
13572 this.emit( 'remove', items );
13573
13574 return this;
13575 };
13576
13577 /**
13578 * Clear all options from the select. Options will be detached from the DOM, not removed,
13579 * so that they can be reused later. To remove a subset of options from the select, use
13580 * the #removeItems method.
13581 *
13582 * @fires remove
13583 * @chainable
13584 */
13585 OO.ui.SelectWidget.prototype.clearItems = function () {
13586 var items = this.items.slice();
13587
13588 // Mixin method
13589 OO.ui.GroupWidget.prototype.clearItems.call( this );
13590
13591 // Clear selection
13592 this.selectItem( null );
13593
13594 this.emit( 'remove', items );
13595
13596 return this;
13597 };
13598
13599 /**
13600 * ButtonSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains
13601 * button options and is used together with
13602 * OO.ui.ButtonOptionWidget. The ButtonSelectWidget provides an interface for
13603 * highlighting, choosing, and selecting mutually exclusive options. Please see
13604 * the [OOjs UI documentation on MediaWiki] [1] for more information.
13605 *
13606 * @example
13607 * // Example: A ButtonSelectWidget that contains three ButtonOptionWidgets
13608 * var option1 = new OO.ui.ButtonOptionWidget( {
13609 * data: 1,
13610 * label: 'Option 1',
13611 * title:'Button option 1'
13612 * } );
13613 *
13614 * var option2 = new OO.ui.ButtonOptionWidget( {
13615 * data: 2,
13616 * label: 'Option 2',
13617 * title:'Button option 2'
13618 * } );
13619 *
13620 * var option3 = new OO.ui.ButtonOptionWidget( {
13621 * data: 3,
13622 * label: 'Option 3',
13623 * title:'Button option 3'
13624 * } );
13625 *
13626 * var buttonSelect=new OO.ui.ButtonSelectWidget( {
13627 * items: [option1, option2, option3]
13628 * } );
13629 * $('body').append(buttonSelect.$element);
13630 *
13631 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
13632 *
13633 * @class
13634 * @extends OO.ui.SelectWidget
13635 * @mixins OO.ui.TabIndexedElement
13636 *
13637 * @constructor
13638 * @param {Object} [config] Configuration options
13639 */
13640 OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
13641 // Parent constructor
13642 OO.ui.ButtonSelectWidget.super.call( this, config );
13643
13644 // Mixin constructors
13645 OO.ui.TabIndexedElement.call( this, config );
13646
13647 // Events
13648 this.$element.on( {
13649 focus: this.bindKeyDownListener.bind( this ),
13650 blur: this.unbindKeyDownListener.bind( this )
13651 } );
13652
13653 // Initialization
13654 this.$element.addClass( 'oo-ui-buttonSelectWidget' );
13655 };
13656
13657 /* Setup */
13658
13659 OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
13660 OO.mixinClass( OO.ui.ButtonSelectWidget, OO.ui.TabIndexedElement );
13661
13662 /**
13663 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
13664 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
13665 * an interface for adding, removing and selecting options.
13666 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
13667 *
13668 * @example
13669 * // A RadioSelectWidget with RadioOptions.
13670 * var option1 = new OO.ui.RadioOptionWidget( {
13671 * data: 'a',
13672 * label: 'Selected radio option'
13673 * } );
13674 *
13675 * var option2 = new OO.ui.RadioOptionWidget( {
13676 * data: 'b',
13677 * label: 'Unselected radio option'
13678 * } );
13679 *
13680 * var radioSelect=new OO.ui.RadioSelectWidget( {
13681 * items: [option1, option2]
13682 * } );
13683 *
13684 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
13685 * radioSelect.selectItem( option1 );
13686 *
13687 * $('body').append(radioSelect.$element);
13688 *
13689 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
13690
13691 *
13692 * @class
13693 * @extends OO.ui.SelectWidget
13694 * @mixins OO.ui.TabIndexedElement
13695 *
13696 * @constructor
13697 * @param {Object} [config] Configuration options
13698 */
13699 OO.ui.RadioSelectWidget = function OoUiRadioSelectWidget( config ) {
13700 // Parent constructor
13701 OO.ui.RadioSelectWidget.super.call( this, config );
13702
13703 // Mixin constructors
13704 OO.ui.TabIndexedElement.call( this, config );
13705
13706 // Events
13707 this.$element.on( {
13708 focus: this.bindKeyDownListener.bind( this ),
13709 blur: this.unbindKeyDownListener.bind( this )
13710 } );
13711
13712 // Initialization
13713 this.$element.addClass( 'oo-ui-radioSelectWidget' );
13714 };
13715
13716 /* Setup */
13717
13718 OO.inheritClass( OO.ui.RadioSelectWidget, OO.ui.SelectWidget );
13719 OO.mixinClass( OO.ui.RadioSelectWidget, OO.ui.TabIndexedElement );
13720
13721 /**
13722 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
13723 * is used together with OO.ui.MenuOptionWidget. See {@link OO.ui.DropdownWidget DropdownWidget} and
13724 * {@link OO.ui.ComboBoxWidget ComboBoxWidget} for examples of interfaces that contain menus.
13725 * MenuSelectWidgets themselves are not designed to be instantiated directly, rather subclassed
13726 * and customized to be opened, closed, and displayed as needed.
13727 *
13728 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
13729 * mouse outside the menu.
13730 *
13731 * Menus also have support for keyboard interaction:
13732 *
13733 * - Enter/Return key: choose and select a menu option
13734 * - Up-arrow key: highlight the previous menu option
13735 * - Down-arrow key: highlight the next menu option
13736 * - Esc key: hide the menu
13737 *
13738 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
13739 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
13740 *
13741 * @class
13742 * @extends OO.ui.SelectWidget
13743 * @mixins OO.ui.ClippableElement
13744 *
13745 * @constructor
13746 * @param {Object} [config] Configuration options
13747 * @cfg {OO.ui.TextInputWidget} [input] Input to bind keyboard handlers to
13748 * @cfg {OO.ui.Widget} [widget] Widget to bind mouse handlers to
13749 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu
13750 */
13751 OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) {
13752 // Configuration initialization
13753 config = config || {};
13754
13755 // Parent constructor
13756 OO.ui.MenuSelectWidget.super.call( this, config );
13757
13758 // Mixin constructors
13759 OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
13760
13761 // Properties
13762 this.newItems = null;
13763 this.autoHide = config.autoHide === undefined || !!config.autoHide;
13764 this.$input = config.input ? config.input.$input : null;
13765 this.$widget = config.widget ? config.widget.$element : null;
13766 this.onDocumentMouseDownHandler = this.onDocumentMouseDown.bind( this );
13767
13768 // Initialization
13769 this.$element
13770 .addClass( 'oo-ui-menuSelectWidget' )
13771 .attr( 'role', 'menu' );
13772
13773 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
13774 // that reference properties not initialized at that time of parent class construction
13775 // TODO: Find a better way to handle post-constructor setup
13776 this.visible = false;
13777 this.$element.addClass( 'oo-ui-element-hidden' );
13778 };
13779
13780 /* Setup */
13781
13782 OO.inheritClass( OO.ui.MenuSelectWidget, OO.ui.SelectWidget );
13783 OO.mixinClass( OO.ui.MenuSelectWidget, OO.ui.ClippableElement );
13784
13785 /* Methods */
13786
13787 /**
13788 * Handles document mouse down events.
13789 *
13790 * @protected
13791 * @param {jQuery.Event} e Key down event
13792 */
13793 OO.ui.MenuSelectWidget.prototype.onDocumentMouseDown = function ( e ) {
13794 if (
13795 !OO.ui.contains( this.$element[ 0 ], e.target, true ) &&
13796 ( !this.$widget || !OO.ui.contains( this.$widget[ 0 ], e.target, true ) )
13797 ) {
13798 this.toggle( false );
13799 }
13800 };
13801
13802 /**
13803 * @inheritdoc
13804 */
13805 OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) {
13806 var currentItem = this.getHighlightedItem() || this.getSelectedItem();
13807
13808 if ( !this.isDisabled() && this.isVisible() ) {
13809 switch ( e.keyCode ) {
13810 case OO.ui.Keys.LEFT:
13811 case OO.ui.Keys.RIGHT:
13812 // Do nothing if a text field is associated, arrow keys will be handled natively
13813 if ( !this.$input ) {
13814 OO.ui.MenuSelectWidget.super.prototype.onKeyDown.call( this, e );
13815 }
13816 break;
13817 case OO.ui.Keys.ESCAPE:
13818 case OO.ui.Keys.TAB:
13819 if ( currentItem ) {
13820 currentItem.setHighlighted( false );
13821 }
13822 this.toggle( false );
13823 // Don't prevent tabbing away, prevent defocusing
13824 if ( e.keyCode === OO.ui.Keys.ESCAPE ) {
13825 e.preventDefault();
13826 e.stopPropagation();
13827 }
13828 break;
13829 default:
13830 OO.ui.MenuSelectWidget.super.prototype.onKeyDown.call( this, e );
13831 return;
13832 }
13833 }
13834 };
13835
13836 /**
13837 * @inheritdoc
13838 */
13839 OO.ui.MenuSelectWidget.prototype.bindKeyDownListener = function () {
13840 if ( this.$input ) {
13841 this.$input.on( 'keydown', this.onKeyDownHandler );
13842 } else {
13843 OO.ui.MenuSelectWidget.super.prototype.bindKeyDownListener.call( this );
13844 }
13845 };
13846
13847 /**
13848 * @inheritdoc
13849 */
13850 OO.ui.MenuSelectWidget.prototype.unbindKeyDownListener = function () {
13851 if ( this.$input ) {
13852 this.$input.off( 'keydown', this.onKeyDownHandler );
13853 } else {
13854 OO.ui.MenuSelectWidget.super.prototype.unbindKeyDownListener.call( this );
13855 }
13856 };
13857
13858 /**
13859 * Choose an item.
13860 *
13861 * This will close the menu, unlike #selectItem which only changes selection.
13862 *
13863 * @param {OO.ui.OptionWidget} item Item to choose
13864 * @chainable
13865 */
13866 OO.ui.MenuSelectWidget.prototype.chooseItem = function ( item ) {
13867 OO.ui.MenuSelectWidget.super.prototype.chooseItem.call( this, item );
13868 this.toggle( false );
13869 return this;
13870 };
13871
13872 /**
13873 * @inheritdoc
13874 */
13875 OO.ui.MenuSelectWidget.prototype.addItems = function ( items, index ) {
13876 var i, len, item;
13877
13878 // Parent method
13879 OO.ui.MenuSelectWidget.super.prototype.addItems.call( this, items, index );
13880
13881 // Auto-initialize
13882 if ( !this.newItems ) {
13883 this.newItems = [];
13884 }
13885
13886 for ( i = 0, len = items.length; i < len; i++ ) {
13887 item = items[ i ];
13888 if ( this.isVisible() ) {
13889 // Defer fitting label until item has been attached
13890 item.fitLabel();
13891 } else {
13892 this.newItems.push( item );
13893 }
13894 }
13895
13896 // Reevaluate clipping
13897 this.clip();
13898
13899 return this;
13900 };
13901
13902 /**
13903 * @inheritdoc
13904 */
13905 OO.ui.MenuSelectWidget.prototype.removeItems = function ( items ) {
13906 // Parent method
13907 OO.ui.MenuSelectWidget.super.prototype.removeItems.call( this, items );
13908
13909 // Reevaluate clipping
13910 this.clip();
13911
13912 return this;
13913 };
13914
13915 /**
13916 * @inheritdoc
13917 */
13918 OO.ui.MenuSelectWidget.prototype.clearItems = function () {
13919 // Parent method
13920 OO.ui.MenuSelectWidget.super.prototype.clearItems.call( this );
13921
13922 // Reevaluate clipping
13923 this.clip();
13924
13925 return this;
13926 };
13927
13928 /**
13929 * @inheritdoc
13930 */
13931 OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
13932 visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
13933
13934 var i, len,
13935 change = visible !== this.isVisible();
13936
13937 // Parent method
13938 OO.ui.MenuSelectWidget.super.prototype.toggle.call( this, visible );
13939
13940 if ( change ) {
13941 if ( visible ) {
13942 this.bindKeyDownListener();
13943
13944 if ( this.newItems && this.newItems.length ) {
13945 for ( i = 0, len = this.newItems.length; i < len; i++ ) {
13946 this.newItems[ i ].fitLabel();
13947 }
13948 this.newItems = null;
13949 }
13950 this.toggleClipping( true );
13951
13952 // Auto-hide
13953 if ( this.autoHide ) {
13954 this.getElementDocument().addEventListener(
13955 'mousedown', this.onDocumentMouseDownHandler, true
13956 );
13957 }
13958 } else {
13959 this.unbindKeyDownListener();
13960 this.getElementDocument().removeEventListener(
13961 'mousedown', this.onDocumentMouseDownHandler, true
13962 );
13963 this.toggleClipping( false );
13964 }
13965 }
13966
13967 return this;
13968 };
13969
13970 /**
13971 * Menu for a text input widget.
13972 *
13973 * This menu is specially designed to be positioned beneath a text input widget. The menu's position
13974 * is automatically calculated and maintained when the menu is toggled or the window is resized.
13975 *
13976 * @class
13977 * @extends OO.ui.MenuSelectWidget
13978 *
13979 * @constructor
13980 * @param {OO.ui.TextInputWidget} inputWidget Text input widget to provide menu for
13981 * @param {Object} [config] Configuration options
13982 * @cfg {jQuery} [$container=input.$element] Element to render menu under
13983 */
13984 OO.ui.TextInputMenuSelectWidget = function OoUiTextInputMenuSelectWidget( inputWidget, config ) {
13985 // Allow passing positional parameters inside the config object
13986 if ( OO.isPlainObject( inputWidget ) && config === undefined ) {
13987 config = inputWidget;
13988 inputWidget = config.inputWidget;
13989 }
13990
13991 // Configuration initialization
13992 config = config || {};
13993
13994 // Parent constructor
13995 OO.ui.TextInputMenuSelectWidget.super.call( this, config );
13996
13997 // Properties
13998 this.inputWidget = inputWidget;
13999 this.$container = config.$container || this.inputWidget.$element;
14000 this.onWindowResizeHandler = this.onWindowResize.bind( this );
14001
14002 // Initialization
14003 this.$element.addClass( 'oo-ui-textInputMenuSelectWidget' );
14004 };
14005
14006 /* Setup */
14007
14008 OO.inheritClass( OO.ui.TextInputMenuSelectWidget, OO.ui.MenuSelectWidget );
14009
14010 /* Methods */
14011
14012 /**
14013 * Handle window resize event.
14014 *
14015 * @param {jQuery.Event} e Window resize event
14016 */
14017 OO.ui.TextInputMenuSelectWidget.prototype.onWindowResize = function () {
14018 this.position();
14019 };
14020
14021 /**
14022 * @inheritdoc
14023 */
14024 OO.ui.TextInputMenuSelectWidget.prototype.toggle = function ( visible ) {
14025 visible = visible === undefined ? !this.isVisible() : !!visible;
14026
14027 var change = visible !== this.isVisible();
14028
14029 if ( change && visible ) {
14030 // Make sure the width is set before the parent method runs.
14031 // After this we have to call this.position(); again to actually
14032 // position ourselves correctly.
14033 this.position();
14034 }
14035
14036 // Parent method
14037 OO.ui.TextInputMenuSelectWidget.super.prototype.toggle.call( this, visible );
14038
14039 if ( change ) {
14040 if ( this.isVisible() ) {
14041 this.position();
14042 $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
14043 } else {
14044 $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
14045 }
14046 }
14047
14048 return this;
14049 };
14050
14051 /**
14052 * Position the menu.
14053 *
14054 * @chainable
14055 */
14056 OO.ui.TextInputMenuSelectWidget.prototype.position = function () {
14057 var $container = this.$container,
14058 pos = OO.ui.Element.static.getRelativePosition( $container, this.$element.offsetParent() );
14059
14060 // Position under input
14061 pos.top += $container.height();
14062 this.$element.css( pos );
14063
14064 // Set width
14065 this.setIdealSize( $container.width() );
14066 // We updated the position, so re-evaluate the clipping state
14067 this.clip();
14068
14069 return this;
14070 };
14071
14072 /**
14073 * Structured list of items.
14074 *
14075 * Use with OO.ui.OutlineOptionWidget.
14076 *
14077 * @class
14078 * @extends OO.ui.SelectWidget
14079 * @mixins OO.ui.TabIndexedElement
14080 *
14081 * @constructor
14082 * @param {Object} [config] Configuration options
14083 */
14084 OO.ui.OutlineSelectWidget = function OoUiOutlineSelectWidget( config ) {
14085 // Parent constructor
14086 OO.ui.OutlineSelectWidget.super.call( this, config );
14087
14088 // Mixin constructors
14089 OO.ui.TabIndexedElement.call( this, config );
14090
14091 // Events
14092 this.$element.on( {
14093 focus: this.bindKeyDownListener.bind( this ),
14094 blur: this.unbindKeyDownListener.bind( this )
14095 } );
14096
14097 // Initialization
14098 this.$element.addClass( 'oo-ui-outlineSelectWidget' );
14099 };
14100
14101 /* Setup */
14102
14103 OO.inheritClass( OO.ui.OutlineSelectWidget, OO.ui.SelectWidget );
14104 OO.mixinClass( OO.ui.OutlineSelectWidget, OO.ui.TabIndexedElement );
14105
14106 /**
14107 * ToggleSwitches are switches that slide on and off. Their state is represented by a Boolean
14108 * value (`true` for ‘on’, and `false` otherwise, the default). The ‘off’ state is represented
14109 * visually by a slider in the leftmost position.
14110 *
14111 * @example
14112 * // Toggle switches in the 'off' and 'on' position.
14113 * var toggleSwitch1 = new OO.ui.ToggleSwitchWidget({
14114 * value: false
14115 * } );
14116 * var toggleSwitch2 = new OO.ui.ToggleSwitchWidget({
14117 * value: true
14118 * } );
14119 *
14120 * // Create a FieldsetLayout to layout and label switches
14121 * var fieldset = new OO.ui.FieldsetLayout( {
14122 * label: 'Toggle switches'
14123 * } );
14124 * fieldset.addItems( [
14125 * new OO.ui.FieldLayout( toggleSwitch1, {label : 'Off', align : 'top'}),
14126 * new OO.ui.FieldLayout( toggleSwitch2, {label : 'On', align : 'top'})
14127 * ] );
14128 * $( 'body' ).append( fieldset.$element );
14129 *
14130 * @class
14131 * @extends OO.ui.Widget
14132 * @mixins OO.ui.ToggleWidget
14133 * @mixins OO.ui.TabIndexedElement
14134 *
14135 * @constructor
14136 * @param {Object} [config] Configuration options
14137 * @cfg {boolean} [value=false] The toggle switch’s initial on/off state.
14138 * By default, the toggle switch is in the 'off' position.
14139 */
14140 OO.ui.ToggleSwitchWidget = function OoUiToggleSwitchWidget( config ) {
14141 // Parent constructor
14142 OO.ui.ToggleSwitchWidget.super.call( this, config );
14143
14144 // Mixin constructors
14145 OO.ui.ToggleWidget.call( this, config );
14146 OO.ui.TabIndexedElement.call( this, config );
14147
14148 // Properties
14149 this.dragging = false;
14150 this.dragStart = null;
14151 this.sliding = false;
14152 this.$glow = $( '<span>' );
14153 this.$grip = $( '<span>' );
14154
14155 // Events
14156 this.$element.on( {
14157 click: this.onClick.bind( this ),
14158 keypress: this.onKeyPress.bind( this )
14159 } );
14160
14161 // Initialization
14162 this.$glow.addClass( 'oo-ui-toggleSwitchWidget-glow' );
14163 this.$grip.addClass( 'oo-ui-toggleSwitchWidget-grip' );
14164 this.$element
14165 .addClass( 'oo-ui-toggleSwitchWidget' )
14166 .attr( 'role', 'checkbox' )
14167 .append( this.$glow, this.$grip );
14168 };
14169
14170 /* Setup */
14171
14172 OO.inheritClass( OO.ui.ToggleSwitchWidget, OO.ui.Widget );
14173 OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.ToggleWidget );
14174 OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.TabIndexedElement );
14175
14176 /* Methods */
14177
14178 /**
14179 * Handle mouse click events.
14180 *
14181 * @private
14182 * @param {jQuery.Event} e Mouse click event
14183 */
14184 OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
14185 if ( !this.isDisabled() && e.which === 1 ) {
14186 this.setValue( !this.value );
14187 }
14188 return false;
14189 };
14190
14191 /**
14192 * Handle key press events.
14193 *
14194 * @private
14195 * @param {jQuery.Event} e Key press event
14196 */
14197 OO.ui.ToggleSwitchWidget.prototype.onKeyPress = function ( e ) {
14198 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
14199 this.setValue( !this.value );
14200 }
14201 return false;
14202 };
14203
14204 }( OO ) );