Remove unused 'XMPGetInfo' and 'XMPGetResults' hooks
[lhc/web/wiklou.git] / resources / lib / oojs-ui / oojs-ui.js
1 /*!
2 * OOjs UI v0.11.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-04-30T01:42:23Z
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 * Check if an element is focusable.
49 * Inspired from :focusable in jQueryUI v1.11.4 - 2015-04-14
50 *
51 * @param {jQuery} element Element to test
52 * @return {Boolean} [description]
53 */
54 OO.ui.isFocusableElement = function ( $element ) {
55 var node = $element[0],
56 nodeName = node.nodeName.toLowerCase(),
57 // Check if the element have tabindex set
58 isInElementGroup = /^(input|select|textarea|button|object)$/.test( nodeName ),
59 // Check if the element is a link with href or if it has tabindex
60 isOtherElement = (
61 ( nodeName === 'a' && node.href ) ||
62 !isNaN( $element.attr( 'tabindex' ) )
63 ),
64 // Check if the element is visible
65 isVisible = (
66 // This is quicker than calling $element.is( ':visible' )
67 $.expr.filters.visible( node ) &&
68 // Check that all parents are visible
69 !$element.parents().addBack().filter( function () {
70 return $.css( this, 'visibility' ) === 'hidden';
71 } ).length
72 );
73
74 return (
75 ( isInElementGroup ? !node.disabled : isOtherElement ) &&
76 isVisible
77 );
78 };
79
80 /**
81 * Get the user's language and any fallback languages.
82 *
83 * These language codes are used to localize user interface elements in the user's language.
84 *
85 * In environments that provide a localization system, this function should be overridden to
86 * return the user's language(s). The default implementation returns English (en) only.
87 *
88 * @return {string[]} Language codes, in descending order of priority
89 */
90 OO.ui.getUserLanguages = function () {
91 return [ 'en' ];
92 };
93
94 /**
95 * Get a value in an object keyed by language code.
96 *
97 * @param {Object.<string,Mixed>} obj Object keyed by language code
98 * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
99 * @param {string} [fallback] Fallback code, used if no matching language can be found
100 * @return {Mixed} Local value
101 */
102 OO.ui.getLocalValue = function ( obj, lang, fallback ) {
103 var i, len, langs;
104
105 // Requested language
106 if ( obj[ lang ] ) {
107 return obj[ lang ];
108 }
109 // Known user language
110 langs = OO.ui.getUserLanguages();
111 for ( i = 0, len = langs.length; i < len; i++ ) {
112 lang = langs[ i ];
113 if ( obj[ lang ] ) {
114 return obj[ lang ];
115 }
116 }
117 // Fallback language
118 if ( obj[ fallback ] ) {
119 return obj[ fallback ];
120 }
121 // First existing language
122 for ( lang in obj ) {
123 return obj[ lang ];
124 }
125
126 return undefined;
127 };
128
129 /**
130 * Check if a node is contained within another node
131 *
132 * Similar to jQuery#contains except a list of containers can be supplied
133 * and a boolean argument allows you to include the container in the match list
134 *
135 * @param {HTMLElement|HTMLElement[]} containers Container node(s) to search in
136 * @param {HTMLElement} contained Node to find
137 * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match, otherwise only match descendants
138 * @return {boolean} The node is in the list of target nodes
139 */
140 OO.ui.contains = function ( containers, contained, matchContainers ) {
141 var i;
142 if ( !Array.isArray( containers ) ) {
143 containers = [ containers ];
144 }
145 for ( i = containers.length - 1; i >= 0; i-- ) {
146 if ( ( matchContainers && contained === containers[ i ] ) || $.contains( containers[ i ], contained ) ) {
147 return true;
148 }
149 }
150 return false;
151 };
152
153 /**
154 * Reconstitute a JavaScript object corresponding to a widget created by
155 * the PHP implementation.
156 *
157 * This is an alias for `OO.ui.Element.static.infuse()`.
158 *
159 * @param {string|HTMLElement|jQuery} idOrNode
160 * A DOM id (if a string) or node for the widget to infuse.
161 * @return {OO.ui.Element}
162 * The `OO.ui.Element` corresponding to this (infusable) document node.
163 */
164 OO.ui.infuse = function ( idOrNode ) {
165 return OO.ui.Element.static.infuse( idOrNode );
166 };
167
168 ( function () {
169 /**
170 * Message store for the default implementation of OO.ui.msg
171 *
172 * Environments that provide a localization system should not use this, but should override
173 * OO.ui.msg altogether.
174 *
175 * @private
176 */
177 var messages = {
178 // Tool tip for a button that moves items in a list down one place
179 'ooui-outline-control-move-down': 'Move item down',
180 // Tool tip for a button that moves items in a list up one place
181 'ooui-outline-control-move-up': 'Move item up',
182 // Tool tip for a button that removes items from a list
183 'ooui-outline-control-remove': 'Remove item',
184 // Label for the toolbar group that contains a list of all other available tools
185 'ooui-toolbar-more': 'More',
186 // Label for the fake tool that expands the full list of tools in a toolbar group
187 'ooui-toolgroup-expand': 'More',
188 // Label for the fake tool that collapses the full list of tools in a toolbar group
189 'ooui-toolgroup-collapse': 'Fewer',
190 // Default label for the accept button of a confirmation dialog
191 'ooui-dialog-message-accept': 'OK',
192 // Default label for the reject button of a confirmation dialog
193 'ooui-dialog-message-reject': 'Cancel',
194 // Title for process dialog error description
195 'ooui-dialog-process-error': 'Something went wrong',
196 // Label for process dialog dismiss error button, visible when describing errors
197 'ooui-dialog-process-dismiss': 'Dismiss',
198 // Label for process dialog retry action button, visible when describing only recoverable errors
199 'ooui-dialog-process-retry': 'Try again',
200 // Label for process dialog retry action button, visible when describing only warnings
201 'ooui-dialog-process-continue': 'Continue'
202 };
203
204 /**
205 * Get a localized message.
206 *
207 * In environments that provide a localization system, this function should be overridden to
208 * return the message translated in the user's language. The default implementation always returns
209 * English messages.
210 *
211 * After the message key, message parameters may optionally be passed. In the default implementation,
212 * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
213 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
214 * they support unnamed, ordered message parameters.
215 *
216 * @abstract
217 * @param {string} key Message key
218 * @param {Mixed...} [params] Message parameters
219 * @return {string} Translated message with parameters substituted
220 */
221 OO.ui.msg = function ( key ) {
222 var message = messages[ key ],
223 params = Array.prototype.slice.call( arguments, 1 );
224 if ( typeof message === 'string' ) {
225 // Perform $1 substitution
226 message = message.replace( /\$(\d+)/g, function ( unused, n ) {
227 var i = parseInt( n, 10 );
228 return params[ i - 1 ] !== undefined ? params[ i - 1 ] : '$' + n;
229 } );
230 } else {
231 // Return placeholder if message not found
232 message = '[' + key + ']';
233 }
234 return message;
235 };
236
237 /**
238 * Package a message and arguments for deferred resolution.
239 *
240 * Use this when you are statically specifying a message and the message may not yet be present.
241 *
242 * @param {string} key Message key
243 * @param {Mixed...} [params] Message parameters
244 * @return {Function} Function that returns the resolved message when executed
245 */
246 OO.ui.deferMsg = function () {
247 var args = arguments;
248 return function () {
249 return OO.ui.msg.apply( OO.ui, args );
250 };
251 };
252
253 /**
254 * Resolve a message.
255 *
256 * If the message is a function it will be executed, otherwise it will pass through directly.
257 *
258 * @param {Function|string} msg Deferred message, or message text
259 * @return {string} Resolved message
260 */
261 OO.ui.resolveMsg = function ( msg ) {
262 if ( $.isFunction( msg ) ) {
263 return msg();
264 }
265 return msg;
266 };
267
268 } )();
269
270 /**
271 * Element that can be marked as pending.
272 *
273 * @abstract
274 * @class
275 *
276 * @constructor
277 * @param {Object} [config] Configuration options
278 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
279 */
280 OO.ui.PendingElement = function OoUiPendingElement( config ) {
281 // Configuration initialization
282 config = config || {};
283
284 // Properties
285 this.pending = 0;
286 this.$pending = null;
287
288 // Initialisation
289 this.setPendingElement( config.$pending || this.$element );
290 };
291
292 /* Setup */
293
294 OO.initClass( OO.ui.PendingElement );
295
296 /* Methods */
297
298 /**
299 * Set the pending element (and clean up any existing one).
300 *
301 * @param {jQuery} $pending The element to set to pending.
302 */
303 OO.ui.PendingElement.prototype.setPendingElement = function ( $pending ) {
304 if ( this.$pending ) {
305 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
306 }
307
308 this.$pending = $pending;
309 if ( this.pending > 0 ) {
310 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
311 }
312 };
313
314 /**
315 * Check if input is pending.
316 *
317 * @return {boolean}
318 */
319 OO.ui.PendingElement.prototype.isPending = function () {
320 return !!this.pending;
321 };
322
323 /**
324 * Increase the pending stack.
325 *
326 * @chainable
327 */
328 OO.ui.PendingElement.prototype.pushPending = function () {
329 if ( this.pending === 0 ) {
330 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
331 this.updateThemeClasses();
332 }
333 this.pending++;
334
335 return this;
336 };
337
338 /**
339 * Reduce the pending stack.
340 *
341 * Clamped at zero.
342 *
343 * @chainable
344 */
345 OO.ui.PendingElement.prototype.popPending = function () {
346 if ( this.pending === 1 ) {
347 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
348 this.updateThemeClasses();
349 }
350 this.pending = Math.max( 0, this.pending - 1 );
351
352 return this;
353 };
354
355 /**
356 * ActionSets manage the behavior of the {@link OO.ui.ActionWidget action widgets} that comprise them.
357 * Actions can be made available for specific contexts (modes) and circumstances
358 * (abilities). Action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
359 *
360 * ActionSets contain two types of actions:
361 *
362 * - Special: Special actions are the first visible actions with special flags, such as 'safe' and 'primary', the default special flags. Additional special flags can be configured in subclasses with the static #specialFlags property.
363 * - Other: Other actions include all non-special visible actions.
364 *
365 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
366 *
367 * @example
368 * // Example: An action set used in a process dialog
369 * function MyProcessDialog( config ) {
370 * MyProcessDialog.super.call( this, config );
371 * }
372 * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
373 * MyProcessDialog.static.title = 'An action set in a process dialog';
374 * // An action set that uses modes ('edit' and 'help' mode, in this example).
375 * MyProcessDialog.static.actions = [
376 * { action: 'continue', modes: 'edit', label: 'Continue', flags: [ 'primary', 'constructive' ] },
377 * { action: 'help', modes: 'edit', label: 'Help' },
378 * { modes: 'edit', label: 'Cancel', flags: 'safe' },
379 * { action: 'back', modes: 'help', label: 'Back', flags: 'safe' }
380 * ];
381 *
382 * MyProcessDialog.prototype.initialize = function () {
383 * MyProcessDialog.super.prototype.initialize.apply( this, arguments );
384 * this.panel1 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
385 * 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>' );
386 * this.panel2 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
387 * 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>' );
388 * this.stackLayout = new OO.ui.StackLayout( {
389 * items: [ this.panel1, this.panel2 ]
390 * } );
391 * this.$body.append( this.stackLayout.$element );
392 * };
393 * MyProcessDialog.prototype.getSetupProcess = function ( data ) {
394 * return MyProcessDialog.super.prototype.getSetupProcess.call( this, data )
395 * .next( function () {
396 * this.actions.setMode( 'edit' );
397 * }, this );
398 * };
399 * MyProcessDialog.prototype.getActionProcess = function ( action ) {
400 * if ( action === 'help' ) {
401 * this.actions.setMode( 'help' );
402 * this.stackLayout.setItem( this.panel2 );
403 * } else if ( action === 'back' ) {
404 * this.actions.setMode( 'edit' );
405 * this.stackLayout.setItem( this.panel1 );
406 * } else if ( action === 'continue' ) {
407 * var dialog = this;
408 * return new OO.ui.Process( function () {
409 * dialog.close();
410 * } );
411 * }
412 * return MyProcessDialog.super.prototype.getActionProcess.call( this, action );
413 * };
414 * MyProcessDialog.prototype.getBodyHeight = function () {
415 * return this.panel1.$element.outerHeight( true );
416 * };
417 * var windowManager = new OO.ui.WindowManager();
418 * $( 'body' ).append( windowManager.$element );
419 * var dialog = new MyProcessDialog( {
420 * size: 'medium'
421 * } );
422 * windowManager.addWindows( [ dialog ] );
423 * windowManager.openWindow( dialog );
424 *
425 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
426 *
427 * @abstract
428 * @class
429 * @mixins OO.EventEmitter
430 *
431 * @constructor
432 * @param {Object} [config] Configuration options
433 */
434 OO.ui.ActionSet = function OoUiActionSet( config ) {
435 // Configuration initialization
436 config = config || {};
437
438 // Mixin constructors
439 OO.EventEmitter.call( this );
440
441 // Properties
442 this.list = [];
443 this.categories = {
444 actions: 'getAction',
445 flags: 'getFlags',
446 modes: 'getModes'
447 };
448 this.categorized = {};
449 this.special = {};
450 this.others = [];
451 this.organized = false;
452 this.changing = false;
453 this.changed = false;
454 };
455
456 /* Setup */
457
458 OO.mixinClass( OO.ui.ActionSet, OO.EventEmitter );
459
460 /* Static Properties */
461
462 /**
463 * Symbolic name of the flags used to identify special actions. Special actions are displayed in the
464 * header of a {@link OO.ui.ProcessDialog process dialog}.
465 * See the [OOjs UI documentation on MediaWiki][2] for more information and examples.
466 *
467 * [2]:https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
468 *
469 * @abstract
470 * @static
471 * @inheritable
472 * @property {string}
473 */
474 OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ];
475
476 /* Events */
477
478 /**
479 * @event click
480 *
481 * A 'click' event is emitted when an action is clicked.
482 *
483 * @param {OO.ui.ActionWidget} action Action that was clicked
484 */
485
486 /**
487 * @event resize
488 *
489 * A 'resize' event is emitted when an action widget is resized.
490 *
491 * @param {OO.ui.ActionWidget} action Action that was resized
492 */
493
494 /**
495 * @event add
496 *
497 * An 'add' event is emitted when actions are {@link #method-add added} to the action set.
498 *
499 * @param {OO.ui.ActionWidget[]} added Actions added
500 */
501
502 /**
503 * @event remove
504 *
505 * A 'remove' event is emitted when actions are {@link #method-remove removed}
506 * or {@link #clear cleared}.
507 *
508 * @param {OO.ui.ActionWidget[]} added Actions removed
509 */
510
511 /**
512 * @event change
513 *
514 * A 'change' event is emitted when actions are {@link #method-add added}, {@link #clear cleared},
515 * or {@link #method-remove removed} from the action set or when the {@link #setMode mode} is changed.
516 *
517 */
518
519 /* Methods */
520
521 /**
522 * Handle action change events.
523 *
524 * @private
525 * @fires change
526 */
527 OO.ui.ActionSet.prototype.onActionChange = function () {
528 this.organized = false;
529 if ( this.changing ) {
530 this.changed = true;
531 } else {
532 this.emit( 'change' );
533 }
534 };
535
536 /**
537 * Check if an action is one of the special actions.
538 *
539 * @param {OO.ui.ActionWidget} action Action to check
540 * @return {boolean} Action is special
541 */
542 OO.ui.ActionSet.prototype.isSpecial = function ( action ) {
543 var flag;
544
545 for ( flag in this.special ) {
546 if ( action === this.special[ flag ] ) {
547 return true;
548 }
549 }
550
551 return false;
552 };
553
554 /**
555 * Get action widgets based on the specified filter: ‘actions’, ‘flags’, ‘modes’, ‘visible’,
556 * or ‘disabled’.
557 *
558 * @param {Object} [filters] Filters to use, omit to get all actions
559 * @param {string|string[]} [filters.actions] Actions that action widgets must have
560 * @param {string|string[]} [filters.flags] Flags that action widgets must have (e.g., 'safe')
561 * @param {string|string[]} [filters.modes] Modes that action widgets must have
562 * @param {boolean} [filters.visible] Action widgets must be visible
563 * @param {boolean} [filters.disabled] Action widgets must be disabled
564 * @return {OO.ui.ActionWidget[]} Action widgets matching all criteria
565 */
566 OO.ui.ActionSet.prototype.get = function ( filters ) {
567 var i, len, list, category, actions, index, match, matches;
568
569 if ( filters ) {
570 this.organize();
571
572 // Collect category candidates
573 matches = [];
574 for ( category in this.categorized ) {
575 list = filters[ category ];
576 if ( list ) {
577 if ( !Array.isArray( list ) ) {
578 list = [ list ];
579 }
580 for ( i = 0, len = list.length; i < len; i++ ) {
581 actions = this.categorized[ category ][ list[ i ] ];
582 if ( Array.isArray( actions ) ) {
583 matches.push.apply( matches, actions );
584 }
585 }
586 }
587 }
588 // Remove by boolean filters
589 for ( i = 0, len = matches.length; i < len; i++ ) {
590 match = matches[ i ];
591 if (
592 ( filters.visible !== undefined && match.isVisible() !== filters.visible ) ||
593 ( filters.disabled !== undefined && match.isDisabled() !== filters.disabled )
594 ) {
595 matches.splice( i, 1 );
596 len--;
597 i--;
598 }
599 }
600 // Remove duplicates
601 for ( i = 0, len = matches.length; i < len; i++ ) {
602 match = matches[ i ];
603 index = matches.lastIndexOf( match );
604 while ( index !== i ) {
605 matches.splice( index, 1 );
606 len--;
607 index = matches.lastIndexOf( match );
608 }
609 }
610 return matches;
611 }
612 return this.list.slice();
613 };
614
615 /**
616 * Get 'special' actions.
617 *
618 * Special actions are the first visible action widgets with special flags, such as 'safe' and 'primary'.
619 * Special flags can be configured in subclasses by changing the static #specialFlags property.
620 *
621 * @return {OO.ui.ActionWidget[]|null} 'Special' action widgets.
622 */
623 OO.ui.ActionSet.prototype.getSpecial = function () {
624 this.organize();
625 return $.extend( {}, this.special );
626 };
627
628 /**
629 * Get 'other' actions.
630 *
631 * Other actions include all non-special visible action widgets.
632 *
633 * @return {OO.ui.ActionWidget[]} 'Other' action widgets
634 */
635 OO.ui.ActionSet.prototype.getOthers = function () {
636 this.organize();
637 return this.others.slice();
638 };
639
640 /**
641 * Set the mode (e.g., ‘edit’ or ‘view’). Only {@link OO.ui.ActionWidget#modes actions} configured
642 * to be available in the specified mode will be made visible. All other actions will be hidden.
643 *
644 * @param {string} mode The mode. Only actions configured to be available in the specified
645 * mode will be made visible.
646 * @chainable
647 * @fires toggle
648 * @fires change
649 */
650 OO.ui.ActionSet.prototype.setMode = function ( mode ) {
651 var i, len, action;
652
653 this.changing = true;
654 for ( i = 0, len = this.list.length; i < len; i++ ) {
655 action = this.list[ i ];
656 action.toggle( action.hasMode( mode ) );
657 }
658
659 this.organized = false;
660 this.changing = false;
661 this.emit( 'change' );
662
663 return this;
664 };
665
666 /**
667 * Set the abilities of the specified actions.
668 *
669 * Action widgets that are configured with the specified actions will be enabled
670 * or disabled based on the boolean values specified in the `actions`
671 * parameter.
672 *
673 * @param {Object.<string,boolean>} actions A list keyed by action name with boolean
674 * values that indicate whether or not the action should be enabled.
675 * @chainable
676 */
677 OO.ui.ActionSet.prototype.setAbilities = function ( actions ) {
678 var i, len, action, item;
679
680 for ( i = 0, len = this.list.length; i < len; i++ ) {
681 item = this.list[ i ];
682 action = item.getAction();
683 if ( actions[ action ] !== undefined ) {
684 item.setDisabled( !actions[ action ] );
685 }
686 }
687
688 return this;
689 };
690
691 /**
692 * Executes a function once per action.
693 *
694 * When making changes to multiple actions, use this method instead of iterating over the actions
695 * manually to defer emitting a #change event until after all actions have been changed.
696 *
697 * @param {Object|null} actions Filters to use to determine which actions to iterate over; see #get
698 * @param {Function} callback Callback to run for each action; callback is invoked with three
699 * arguments: the action, the action's index, the list of actions being iterated over
700 * @chainable
701 */
702 OO.ui.ActionSet.prototype.forEach = function ( filter, callback ) {
703 this.changed = false;
704 this.changing = true;
705 this.get( filter ).forEach( callback );
706 this.changing = false;
707 if ( this.changed ) {
708 this.emit( 'change' );
709 }
710
711 return this;
712 };
713
714 /**
715 * Add action widgets to the action set.
716 *
717 * @param {OO.ui.ActionWidget[]} actions Action widgets to add
718 * @chainable
719 * @fires add
720 * @fires change
721 */
722 OO.ui.ActionSet.prototype.add = function ( actions ) {
723 var i, len, action;
724
725 this.changing = true;
726 for ( i = 0, len = actions.length; i < len; i++ ) {
727 action = actions[ i ];
728 action.connect( this, {
729 click: [ 'emit', 'click', action ],
730 resize: [ 'emit', 'resize', action ],
731 toggle: [ 'onActionChange' ]
732 } );
733 this.list.push( action );
734 }
735 this.organized = false;
736 this.emit( 'add', actions );
737 this.changing = false;
738 this.emit( 'change' );
739
740 return this;
741 };
742
743 /**
744 * Remove action widgets from the set.
745 *
746 * To remove all actions, you may wish to use the #clear method instead.
747 *
748 * @param {OO.ui.ActionWidget[]} actions Action widgets to remove
749 * @chainable
750 * @fires remove
751 * @fires change
752 */
753 OO.ui.ActionSet.prototype.remove = function ( actions ) {
754 var i, len, index, action;
755
756 this.changing = true;
757 for ( i = 0, len = actions.length; i < len; i++ ) {
758 action = actions[ i ];
759 index = this.list.indexOf( action );
760 if ( index !== -1 ) {
761 action.disconnect( this );
762 this.list.splice( index, 1 );
763 }
764 }
765 this.organized = false;
766 this.emit( 'remove', actions );
767 this.changing = false;
768 this.emit( 'change' );
769
770 return this;
771 };
772
773 /**
774 * Remove all action widets from the set.
775 *
776 * To remove only specified actions, use the {@link #method-remove remove} method instead.
777 *
778 * @chainable
779 * @fires remove
780 * @fires change
781 */
782 OO.ui.ActionSet.prototype.clear = function () {
783 var i, len, action,
784 removed = this.list.slice();
785
786 this.changing = true;
787 for ( i = 0, len = this.list.length; i < len; i++ ) {
788 action = this.list[ i ];
789 action.disconnect( this );
790 }
791
792 this.list = [];
793
794 this.organized = false;
795 this.emit( 'remove', removed );
796 this.changing = false;
797 this.emit( 'change' );
798
799 return this;
800 };
801
802 /**
803 * Organize actions.
804 *
805 * This is called whenever organized information is requested. It will only reorganize the actions
806 * if something has changed since the last time it ran.
807 *
808 * @private
809 * @chainable
810 */
811 OO.ui.ActionSet.prototype.organize = function () {
812 var i, iLen, j, jLen, flag, action, category, list, item, special,
813 specialFlags = this.constructor.static.specialFlags;
814
815 if ( !this.organized ) {
816 this.categorized = {};
817 this.special = {};
818 this.others = [];
819 for ( i = 0, iLen = this.list.length; i < iLen; i++ ) {
820 action = this.list[ i ];
821 if ( action.isVisible() ) {
822 // Populate categories
823 for ( category in this.categories ) {
824 if ( !this.categorized[ category ] ) {
825 this.categorized[ category ] = {};
826 }
827 list = action[ this.categories[ category ] ]();
828 if ( !Array.isArray( list ) ) {
829 list = [ list ];
830 }
831 for ( j = 0, jLen = list.length; j < jLen; j++ ) {
832 item = list[ j ];
833 if ( !this.categorized[ category ][ item ] ) {
834 this.categorized[ category ][ item ] = [];
835 }
836 this.categorized[ category ][ item ].push( action );
837 }
838 }
839 // Populate special/others
840 special = false;
841 for ( j = 0, jLen = specialFlags.length; j < jLen; j++ ) {
842 flag = specialFlags[ j ];
843 if ( !this.special[ flag ] && action.hasFlag( flag ) ) {
844 this.special[ flag ] = action;
845 special = true;
846 break;
847 }
848 }
849 if ( !special ) {
850 this.others.push( action );
851 }
852 }
853 }
854 this.organized = true;
855 }
856
857 return this;
858 };
859
860 /**
861 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
862 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
863 * connected to them and can't be interacted with.
864 *
865 * @abstract
866 * @class
867 *
868 * @constructor
869 * @param {Object} [config] Configuration options
870 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
871 * to the top level (e.g., the outermost div) of the element. See the [OOjs UI documentation on MediaWiki][2]
872 * for an example.
873 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#cssExample
874 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
875 * @cfg {string} [text] Text to insert
876 * @cfg {Array} [content] An array of content elements to append (after #text).
877 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
878 * Instances of OO.ui.Element will have their $element appended.
879 * @cfg {jQuery} [$content] Content elements to append (after #text)
880 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
881 * Data can also be specified with the #setData method.
882 */
883 OO.ui.Element = function OoUiElement( config ) {
884 // Configuration initialization
885 config = config || {};
886
887 // Properties
888 this.$ = $;
889 this.visible = true;
890 this.data = config.data;
891 this.$element = config.$element ||
892 $( document.createElement( this.getTagName() ) );
893 this.elementGroup = null;
894 this.debouncedUpdateThemeClassesHandler = this.debouncedUpdateThemeClasses.bind( this );
895 this.updateThemeClassesPending = false;
896
897 // Initialization
898 if ( Array.isArray( config.classes ) ) {
899 this.$element.addClass( config.classes.join( ' ' ) );
900 }
901 if ( config.id ) {
902 this.$element.attr( 'id', config.id );
903 }
904 if ( config.text ) {
905 this.$element.text( config.text );
906 }
907 if ( config.content ) {
908 // The `content` property treats plain strings as text; use an
909 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
910 // appropriate $element appended.
911 this.$element.append( config.content.map( function ( v ) {
912 if ( typeof v === 'string' ) {
913 // Escape string so it is properly represented in HTML.
914 return document.createTextNode( v );
915 } else if ( v instanceof OO.ui.HtmlSnippet ) {
916 // Bypass escaping.
917 return v.toString();
918 } else if ( v instanceof OO.ui.Element ) {
919 return v.$element;
920 }
921 return v;
922 } ) );
923 }
924 if ( config.$content ) {
925 // The `$content` property treats plain strings as HTML.
926 this.$element.append( config.$content );
927 }
928 };
929
930 /* Setup */
931
932 OO.initClass( OO.ui.Element );
933
934 /* Static Properties */
935
936 /**
937 * The name of the HTML tag used by the element.
938 *
939 * The static value may be ignored if the #getTagName method is overridden.
940 *
941 * @static
942 * @inheritable
943 * @property {string}
944 */
945 OO.ui.Element.static.tagName = 'div';
946
947 /* Static Methods */
948
949 /**
950 * Reconstitute a JavaScript object corresponding to a widget created
951 * by the PHP implementation.
952 *
953 * @param {string|HTMLElement|jQuery} idOrNode
954 * A DOM id (if a string) or node for the widget to infuse.
955 * @return {OO.ui.Element}
956 * The `OO.ui.Element` corresponding to this (infusable) document node.
957 * For `Tag` objects emitted on the HTML side (used occasionally for content)
958 * the value returned is a newly-created Element wrapping around the existing
959 * DOM node.
960 */
961 OO.ui.Element.static.infuse = function ( idOrNode ) {
962 var obj = OO.ui.Element.static.unsafeInfuse( idOrNode, true );
963 // Verify that the type matches up.
964 // FIXME: uncomment after T89721 is fixed (see T90929)
965 /*
966 if ( !( obj instanceof this['class'] ) ) {
967 throw new Error( 'Infusion type mismatch!' );
968 }
969 */
970 return obj;
971 };
972
973 /**
974 * Implementation helper for `infuse`; skips the type check and has an
975 * extra property so that only the top-level invocation touches the DOM.
976 * @private
977 * @param {string|HTMLElement|jQuery} idOrNode
978 * @param {boolean} top True only for top-level invocation.
979 * @return {OO.ui.Element}
980 */
981 OO.ui.Element.static.unsafeInfuse = function ( idOrNode, top ) {
982 // look for a cached result of a previous infusion.
983 var id, $elem, data, cls, obj;
984 if ( typeof idOrNode === 'string' ) {
985 id = idOrNode;
986 $elem = $( document.getElementById( id ) );
987 } else {
988 $elem = $( idOrNode );
989 id = $elem.attr( 'id' );
990 }
991 data = $elem.data( 'ooui-infused' );
992 if ( data ) {
993 // cached!
994 if ( data === true ) {
995 throw new Error( 'Circular dependency! ' + id );
996 }
997 return data;
998 }
999 if ( !$elem.length ) {
1000 throw new Error( 'Widget not found: ' + id );
1001 }
1002 data = $elem.attr( 'data-ooui' );
1003 if ( !data ) {
1004 throw new Error( 'No infusion data found: ' + id );
1005 }
1006 try {
1007 data = $.parseJSON( data );
1008 } catch ( _ ) {
1009 data = null;
1010 }
1011 if ( !( data && data._ ) ) {
1012 throw new Error( 'No valid infusion data found: ' + id );
1013 }
1014 if ( data._ === 'Tag' ) {
1015 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
1016 return new OO.ui.Element( { $element: $elem } );
1017 }
1018 cls = OO.ui[data._];
1019 if ( !cls ) {
1020 throw new Error( 'Unknown widget type: ' + id );
1021 }
1022 $elem.data( 'ooui-infused', true ); // prevent loops
1023 data.id = id; // implicit
1024 data = OO.copy( data, null, function deserialize( value ) {
1025 if ( OO.isPlainObject( value ) ) {
1026 if ( value.tag ) {
1027 return OO.ui.Element.static.unsafeInfuse( value.tag, false );
1028 }
1029 if ( value.html ) {
1030 return new OO.ui.HtmlSnippet( value.html );
1031 }
1032 }
1033 } );
1034 // jscs:disable requireCapitalizedConstructors
1035 obj = new cls( data ); // rebuild widget
1036 // now replace old DOM with this new DOM.
1037 if ( top ) {
1038 $elem.replaceWith( obj.$element );
1039 }
1040 obj.$element.data( 'ooui-infused', obj );
1041 // set the 'data-ooui' attribute so we can identify infused widgets
1042 obj.$element.attr( 'data-ooui', '' );
1043 return obj;
1044 };
1045
1046 /**
1047 * Get a jQuery function within a specific document.
1048 *
1049 * @static
1050 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
1051 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
1052 * not in an iframe
1053 * @return {Function} Bound jQuery function
1054 */
1055 OO.ui.Element.static.getJQuery = function ( context, $iframe ) {
1056 function wrapper( selector ) {
1057 return $( selector, wrapper.context );
1058 }
1059
1060 wrapper.context = this.getDocument( context );
1061
1062 if ( $iframe ) {
1063 wrapper.$iframe = $iframe;
1064 }
1065
1066 return wrapper;
1067 };
1068
1069 /**
1070 * Get the document of an element.
1071 *
1072 * @static
1073 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
1074 * @return {HTMLDocument|null} Document object
1075 */
1076 OO.ui.Element.static.getDocument = function ( obj ) {
1077 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
1078 return ( obj[ 0 ] && obj[ 0 ].ownerDocument ) ||
1079 // Empty jQuery selections might have a context
1080 obj.context ||
1081 // HTMLElement
1082 obj.ownerDocument ||
1083 // Window
1084 obj.document ||
1085 // HTMLDocument
1086 ( obj.nodeType === 9 && obj ) ||
1087 null;
1088 };
1089
1090 /**
1091 * Get the window of an element or document.
1092 *
1093 * @static
1094 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
1095 * @return {Window} Window object
1096 */
1097 OO.ui.Element.static.getWindow = function ( obj ) {
1098 var doc = this.getDocument( obj );
1099 return doc.parentWindow || doc.defaultView;
1100 };
1101
1102 /**
1103 * Get the direction of an element or document.
1104 *
1105 * @static
1106 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
1107 * @return {string} Text direction, either 'ltr' or 'rtl'
1108 */
1109 OO.ui.Element.static.getDir = function ( obj ) {
1110 var isDoc, isWin;
1111
1112 if ( obj instanceof jQuery ) {
1113 obj = obj[ 0 ];
1114 }
1115 isDoc = obj.nodeType === 9;
1116 isWin = obj.document !== undefined;
1117 if ( isDoc || isWin ) {
1118 if ( isWin ) {
1119 obj = obj.document;
1120 }
1121 obj = obj.body;
1122 }
1123 return $( obj ).css( 'direction' );
1124 };
1125
1126 /**
1127 * Get the offset between two frames.
1128 *
1129 * TODO: Make this function not use recursion.
1130 *
1131 * @static
1132 * @param {Window} from Window of the child frame
1133 * @param {Window} [to=window] Window of the parent frame
1134 * @param {Object} [offset] Offset to start with, used internally
1135 * @return {Object} Offset object, containing left and top properties
1136 */
1137 OO.ui.Element.static.getFrameOffset = function ( from, to, offset ) {
1138 var i, len, frames, frame, rect;
1139
1140 if ( !to ) {
1141 to = window;
1142 }
1143 if ( !offset ) {
1144 offset = { top: 0, left: 0 };
1145 }
1146 if ( from.parent === from ) {
1147 return offset;
1148 }
1149
1150 // Get iframe element
1151 frames = from.parent.document.getElementsByTagName( 'iframe' );
1152 for ( i = 0, len = frames.length; i < len; i++ ) {
1153 if ( frames[ i ].contentWindow === from ) {
1154 frame = frames[ i ];
1155 break;
1156 }
1157 }
1158
1159 // Recursively accumulate offset values
1160 if ( frame ) {
1161 rect = frame.getBoundingClientRect();
1162 offset.left += rect.left;
1163 offset.top += rect.top;
1164 if ( from !== to ) {
1165 this.getFrameOffset( from.parent, offset );
1166 }
1167 }
1168 return offset;
1169 };
1170
1171 /**
1172 * Get the offset between two elements.
1173 *
1174 * The two elements may be in a different frame, but in that case the frame $element is in must
1175 * be contained in the frame $anchor is in.
1176 *
1177 * @static
1178 * @param {jQuery} $element Element whose position to get
1179 * @param {jQuery} $anchor Element to get $element's position relative to
1180 * @return {Object} Translated position coordinates, containing top and left properties
1181 */
1182 OO.ui.Element.static.getRelativePosition = function ( $element, $anchor ) {
1183 var iframe, iframePos,
1184 pos = $element.offset(),
1185 anchorPos = $anchor.offset(),
1186 elementDocument = this.getDocument( $element ),
1187 anchorDocument = this.getDocument( $anchor );
1188
1189 // If $element isn't in the same document as $anchor, traverse up
1190 while ( elementDocument !== anchorDocument ) {
1191 iframe = elementDocument.defaultView.frameElement;
1192 if ( !iframe ) {
1193 throw new Error( '$element frame is not contained in $anchor frame' );
1194 }
1195 iframePos = $( iframe ).offset();
1196 pos.left += iframePos.left;
1197 pos.top += iframePos.top;
1198 elementDocument = iframe.ownerDocument;
1199 }
1200 pos.left -= anchorPos.left;
1201 pos.top -= anchorPos.top;
1202 return pos;
1203 };
1204
1205 /**
1206 * Get element border sizes.
1207 *
1208 * @static
1209 * @param {HTMLElement} el Element to measure
1210 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1211 */
1212 OO.ui.Element.static.getBorders = function ( el ) {
1213 var doc = el.ownerDocument,
1214 win = doc.parentWindow || doc.defaultView,
1215 style = win && win.getComputedStyle ?
1216 win.getComputedStyle( el, null ) :
1217 el.currentStyle,
1218 $el = $( el ),
1219 top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0,
1220 left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0,
1221 bottom = parseFloat( style ? style.borderBottomWidth : $el.css( 'borderBottomWidth' ) ) || 0,
1222 right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0;
1223
1224 return {
1225 top: top,
1226 left: left,
1227 bottom: bottom,
1228 right: right
1229 };
1230 };
1231
1232 /**
1233 * Get dimensions of an element or window.
1234 *
1235 * @static
1236 * @param {HTMLElement|Window} el Element to measure
1237 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1238 */
1239 OO.ui.Element.static.getDimensions = function ( el ) {
1240 var $el, $win,
1241 doc = el.ownerDocument || el.document,
1242 win = doc.parentWindow || doc.defaultView;
1243
1244 if ( win === el || el === doc.documentElement ) {
1245 $win = $( win );
1246 return {
1247 borders: { top: 0, left: 0, bottom: 0, right: 0 },
1248 scroll: {
1249 top: $win.scrollTop(),
1250 left: $win.scrollLeft()
1251 },
1252 scrollbar: { right: 0, bottom: 0 },
1253 rect: {
1254 top: 0,
1255 left: 0,
1256 bottom: $win.innerHeight(),
1257 right: $win.innerWidth()
1258 }
1259 };
1260 } else {
1261 $el = $( el );
1262 return {
1263 borders: this.getBorders( el ),
1264 scroll: {
1265 top: $el.scrollTop(),
1266 left: $el.scrollLeft()
1267 },
1268 scrollbar: {
1269 right: $el.innerWidth() - el.clientWidth,
1270 bottom: $el.innerHeight() - el.clientHeight
1271 },
1272 rect: el.getBoundingClientRect()
1273 };
1274 }
1275 };
1276
1277 /**
1278 * Get scrollable object parent
1279 *
1280 * documentElement can't be used to get or set the scrollTop
1281 * property on Blink. Changing and testing its value lets us
1282 * use 'body' or 'documentElement' based on what is working.
1283 *
1284 * https://code.google.com/p/chromium/issues/detail?id=303131
1285 *
1286 * @static
1287 * @param {HTMLElement} el Element to find scrollable parent for
1288 * @return {HTMLElement} Scrollable parent
1289 */
1290 OO.ui.Element.static.getRootScrollableElement = function ( el ) {
1291 var scrollTop, body;
1292
1293 if ( OO.ui.scrollableElement === undefined ) {
1294 body = el.ownerDocument.body;
1295 scrollTop = body.scrollTop;
1296 body.scrollTop = 1;
1297
1298 if ( body.scrollTop === 1 ) {
1299 body.scrollTop = scrollTop;
1300 OO.ui.scrollableElement = 'body';
1301 } else {
1302 OO.ui.scrollableElement = 'documentElement';
1303 }
1304 }
1305
1306 return el.ownerDocument[ OO.ui.scrollableElement ];
1307 };
1308
1309 /**
1310 * Get closest scrollable container.
1311 *
1312 * Traverses up until either a scrollable element or the root is reached, in which case the window
1313 * will be returned.
1314 *
1315 * @static
1316 * @param {HTMLElement} el Element to find scrollable container for
1317 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1318 * @return {HTMLElement} Closest scrollable container
1319 */
1320 OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) {
1321 var i, val,
1322 props = [ 'overflow' ],
1323 $parent = $( el ).parent();
1324
1325 if ( dimension === 'x' || dimension === 'y' ) {
1326 props.push( 'overflow-' + dimension );
1327 }
1328
1329 while ( $parent.length ) {
1330 if ( $parent[ 0 ] === this.getRootScrollableElement( el ) ) {
1331 return $parent[ 0 ];
1332 }
1333 i = props.length;
1334 while ( i-- ) {
1335 val = $parent.css( props[ i ] );
1336 if ( val === 'auto' || val === 'scroll' ) {
1337 return $parent[ 0 ];
1338 }
1339 }
1340 $parent = $parent.parent();
1341 }
1342 return this.getDocument( el ).body;
1343 };
1344
1345 /**
1346 * Scroll element into view.
1347 *
1348 * @static
1349 * @param {HTMLElement} el Element to scroll into view
1350 * @param {Object} [config] Configuration options
1351 * @param {string} [config.duration] jQuery animation duration value
1352 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1353 * to scroll in both directions
1354 * @param {Function} [config.complete] Function to call when scrolling completes
1355 */
1356 OO.ui.Element.static.scrollIntoView = function ( el, config ) {
1357 // Configuration initialization
1358 config = config || {};
1359
1360 var rel, anim = {},
1361 callback = typeof config.complete === 'function' && config.complete,
1362 sc = this.getClosestScrollableContainer( el, config.direction ),
1363 $sc = $( sc ),
1364 eld = this.getDimensions( el ),
1365 scd = this.getDimensions( sc ),
1366 $win = $( this.getWindow( el ) );
1367
1368 // Compute the distances between the edges of el and the edges of the scroll viewport
1369 if ( $sc.is( 'html, body' ) ) {
1370 // If the scrollable container is the root, this is easy
1371 rel = {
1372 top: eld.rect.top,
1373 bottom: $win.innerHeight() - eld.rect.bottom,
1374 left: eld.rect.left,
1375 right: $win.innerWidth() - eld.rect.right
1376 };
1377 } else {
1378 // Otherwise, we have to subtract el's coordinates from sc's coordinates
1379 rel = {
1380 top: eld.rect.top - ( scd.rect.top + scd.borders.top ),
1381 bottom: scd.rect.bottom - scd.borders.bottom - scd.scrollbar.bottom - eld.rect.bottom,
1382 left: eld.rect.left - ( scd.rect.left + scd.borders.left ),
1383 right: scd.rect.right - scd.borders.right - scd.scrollbar.right - eld.rect.right
1384 };
1385 }
1386
1387 if ( !config.direction || config.direction === 'y' ) {
1388 if ( rel.top < 0 ) {
1389 anim.scrollTop = scd.scroll.top + rel.top;
1390 } else if ( rel.top > 0 && rel.bottom < 0 ) {
1391 anim.scrollTop = scd.scroll.top + Math.min( rel.top, -rel.bottom );
1392 }
1393 }
1394 if ( !config.direction || config.direction === 'x' ) {
1395 if ( rel.left < 0 ) {
1396 anim.scrollLeft = scd.scroll.left + rel.left;
1397 } else if ( rel.left > 0 && rel.right < 0 ) {
1398 anim.scrollLeft = scd.scroll.left + Math.min( rel.left, -rel.right );
1399 }
1400 }
1401 if ( !$.isEmptyObject( anim ) ) {
1402 $sc.stop( true ).animate( anim, config.duration || 'fast' );
1403 if ( callback ) {
1404 $sc.queue( function ( next ) {
1405 callback();
1406 next();
1407 } );
1408 }
1409 } else {
1410 if ( callback ) {
1411 callback();
1412 }
1413 }
1414 };
1415
1416 /**
1417 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1418 * and reserve space for them, because it probably doesn't.
1419 *
1420 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1421 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1422 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1423 * and then reattach (or show) them back.
1424 *
1425 * @static
1426 * @param {HTMLElement} el Element to reconsider the scrollbars on
1427 */
1428 OO.ui.Element.static.reconsiderScrollbars = function ( el ) {
1429 var i, len, nodes = [];
1430 // Detach all children
1431 while ( el.firstChild ) {
1432 nodes.push( el.firstChild );
1433 el.removeChild( el.firstChild );
1434 }
1435 // Force reflow
1436 void el.offsetHeight;
1437 // Reattach all children
1438 for ( i = 0, len = nodes.length; i < len; i++ ) {
1439 el.appendChild( nodes[ i ] );
1440 }
1441 };
1442
1443 /* Methods */
1444
1445 /**
1446 * Toggle visibility of an element.
1447 *
1448 * @param {boolean} [show] Make element visible, omit to toggle visibility
1449 * @fires visible
1450 * @chainable
1451 */
1452 OO.ui.Element.prototype.toggle = function ( show ) {
1453 show = show === undefined ? !this.visible : !!show;
1454
1455 if ( show !== this.isVisible() ) {
1456 this.visible = show;
1457 this.$element.toggleClass( 'oo-ui-element-hidden', !this.visible );
1458 this.emit( 'toggle', show );
1459 }
1460
1461 return this;
1462 };
1463
1464 /**
1465 * Check if element is visible.
1466 *
1467 * @return {boolean} element is visible
1468 */
1469 OO.ui.Element.prototype.isVisible = function () {
1470 return this.visible;
1471 };
1472
1473 /**
1474 * Get element data.
1475 *
1476 * @return {Mixed} Element data
1477 */
1478 OO.ui.Element.prototype.getData = function () {
1479 return this.data;
1480 };
1481
1482 /**
1483 * Set element data.
1484 *
1485 * @param {Mixed} Element data
1486 * @chainable
1487 */
1488 OO.ui.Element.prototype.setData = function ( data ) {
1489 this.data = data;
1490 return this;
1491 };
1492
1493 /**
1494 * Check if element supports one or more methods.
1495 *
1496 * @param {string|string[]} methods Method or list of methods to check
1497 * @return {boolean} All methods are supported
1498 */
1499 OO.ui.Element.prototype.supports = function ( methods ) {
1500 var i, len,
1501 support = 0;
1502
1503 methods = Array.isArray( methods ) ? methods : [ methods ];
1504 for ( i = 0, len = methods.length; i < len; i++ ) {
1505 if ( $.isFunction( this[ methods[ i ] ] ) ) {
1506 support++;
1507 }
1508 }
1509
1510 return methods.length === support;
1511 };
1512
1513 /**
1514 * Update the theme-provided classes.
1515 *
1516 * @localdoc This is called in element mixins and widget classes any time state changes.
1517 * Updating is debounced, minimizing overhead of changing multiple attributes and
1518 * guaranteeing that theme updates do not occur within an element's constructor
1519 */
1520 OO.ui.Element.prototype.updateThemeClasses = function () {
1521 if ( !this.updateThemeClassesPending ) {
1522 this.updateThemeClassesPending = true;
1523 setTimeout( this.debouncedUpdateThemeClassesHandler );
1524 }
1525 };
1526
1527 /**
1528 * @private
1529 */
1530 OO.ui.Element.prototype.debouncedUpdateThemeClasses = function () {
1531 OO.ui.theme.updateElementClasses( this );
1532 this.updateThemeClassesPending = false;
1533 };
1534
1535 /**
1536 * Get the HTML tag name.
1537 *
1538 * Override this method to base the result on instance information.
1539 *
1540 * @return {string} HTML tag name
1541 */
1542 OO.ui.Element.prototype.getTagName = function () {
1543 return this.constructor.static.tagName;
1544 };
1545
1546 /**
1547 * Check if the element is attached to the DOM
1548 * @return {boolean} The element is attached to the DOM
1549 */
1550 OO.ui.Element.prototype.isElementAttached = function () {
1551 return $.contains( this.getElementDocument(), this.$element[ 0 ] );
1552 };
1553
1554 /**
1555 * Get the DOM document.
1556 *
1557 * @return {HTMLDocument} Document object
1558 */
1559 OO.ui.Element.prototype.getElementDocument = function () {
1560 // Don't cache this in other ways either because subclasses could can change this.$element
1561 return OO.ui.Element.static.getDocument( this.$element );
1562 };
1563
1564 /**
1565 * Get the DOM window.
1566 *
1567 * @return {Window} Window object
1568 */
1569 OO.ui.Element.prototype.getElementWindow = function () {
1570 return OO.ui.Element.static.getWindow( this.$element );
1571 };
1572
1573 /**
1574 * Get closest scrollable container.
1575 */
1576 OO.ui.Element.prototype.getClosestScrollableElementContainer = function () {
1577 return OO.ui.Element.static.getClosestScrollableContainer( this.$element[ 0 ] );
1578 };
1579
1580 /**
1581 * Get group element is in.
1582 *
1583 * @return {OO.ui.GroupElement|null} Group element, null if none
1584 */
1585 OO.ui.Element.prototype.getElementGroup = function () {
1586 return this.elementGroup;
1587 };
1588
1589 /**
1590 * Set group element is in.
1591 *
1592 * @param {OO.ui.GroupElement|null} group Group element, null if none
1593 * @chainable
1594 */
1595 OO.ui.Element.prototype.setElementGroup = function ( group ) {
1596 this.elementGroup = group;
1597 return this;
1598 };
1599
1600 /**
1601 * Scroll element into view.
1602 *
1603 * @param {Object} [config] Configuration options
1604 */
1605 OO.ui.Element.prototype.scrollElementIntoView = function ( config ) {
1606 return OO.ui.Element.static.scrollIntoView( this.$element[ 0 ], config );
1607 };
1608
1609 /**
1610 * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way
1611 * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined.
1612 * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout},
1613 * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1614 * and {@link OO.ui.BookletLayout BookletLayout} for more information and examples.
1615 *
1616 * @abstract
1617 * @class
1618 * @extends OO.ui.Element
1619 * @mixins OO.EventEmitter
1620 *
1621 * @constructor
1622 * @param {Object} [config] Configuration options
1623 */
1624 OO.ui.Layout = function OoUiLayout( config ) {
1625 // Configuration initialization
1626 config = config || {};
1627
1628 // Parent constructor
1629 OO.ui.Layout.super.call( this, config );
1630
1631 // Mixin constructors
1632 OO.EventEmitter.call( this );
1633
1634 // Initialization
1635 this.$element.addClass( 'oo-ui-layout' );
1636 };
1637
1638 /* Setup */
1639
1640 OO.inheritClass( OO.ui.Layout, OO.ui.Element );
1641 OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
1642
1643 /**
1644 * Widgets are compositions of one or more OOjs UI elements that users can both view
1645 * and interact with. All widgets can be configured and modified via a standard API,
1646 * and their state can change dynamically according to a model.
1647 *
1648 * @abstract
1649 * @class
1650 * @extends OO.ui.Element
1651 * @mixins OO.EventEmitter
1652 *
1653 * @constructor
1654 * @param {Object} [config] Configuration options
1655 * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1656 * appearance reflects this state.
1657 */
1658 OO.ui.Widget = function OoUiWidget( config ) {
1659 // Initialize config
1660 config = $.extend( { disabled: false }, config );
1661
1662 // Parent constructor
1663 OO.ui.Widget.super.call( this, config );
1664
1665 // Mixin constructors
1666 OO.EventEmitter.call( this );
1667
1668 // Properties
1669 this.disabled = null;
1670 this.wasDisabled = null;
1671
1672 // Initialization
1673 this.$element.addClass( 'oo-ui-widget' );
1674 this.setDisabled( !!config.disabled );
1675 };
1676
1677 /* Setup */
1678
1679 OO.inheritClass( OO.ui.Widget, OO.ui.Element );
1680 OO.mixinClass( OO.ui.Widget, OO.EventEmitter );
1681
1682 /* Events */
1683
1684 /**
1685 * @event disable
1686 *
1687 * A 'disable' event is emitted when a widget is disabled.
1688 *
1689 * @param {boolean} disabled Widget is disabled
1690 */
1691
1692 /**
1693 * @event toggle
1694 *
1695 * A 'toggle' event is emitted when the visibility of the widget changes.
1696 *
1697 * @param {boolean} visible Widget is visible
1698 */
1699
1700 /* Methods */
1701
1702 /**
1703 * Check if the widget is disabled.
1704 *
1705 * @return {boolean} Widget is disabled
1706 */
1707 OO.ui.Widget.prototype.isDisabled = function () {
1708 return this.disabled;
1709 };
1710
1711 /**
1712 * Set the 'disabled' state of the widget.
1713 *
1714 * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1715 *
1716 * @param {boolean} disabled Disable widget
1717 * @chainable
1718 */
1719 OO.ui.Widget.prototype.setDisabled = function ( disabled ) {
1720 var isDisabled;
1721
1722 this.disabled = !!disabled;
1723 isDisabled = this.isDisabled();
1724 if ( isDisabled !== this.wasDisabled ) {
1725 this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled );
1726 this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled );
1727 this.$element.attr( 'aria-disabled', isDisabled.toString() );
1728 this.emit( 'disable', isDisabled );
1729 this.updateThemeClasses();
1730 }
1731 this.wasDisabled = isDisabled;
1732
1733 return this;
1734 };
1735
1736 /**
1737 * Update the disabled state, in case of changes in parent widget.
1738 *
1739 * @chainable
1740 */
1741 OO.ui.Widget.prototype.updateDisabled = function () {
1742 this.setDisabled( this.disabled );
1743 return this;
1744 };
1745
1746 /**
1747 * A window is a container for elements that are in a child frame. They are used with
1748 * a window manager (OO.ui.WindowManager), which is used to open and close the window and control
1749 * its presentation. The size of a window is specified using a symbolic name (e.g., ‘small’, ‘medium’,
1750 * ‘large’), which is interpreted by the window manager. If the requested size is not recognized,
1751 * the window manager will choose a sensible fallback.
1752 *
1753 * The lifecycle of a window has three primary stages (opening, opened, and closing) in which
1754 * different processes are executed:
1755 *
1756 * **opening**: The opening stage begins when the window manager's {@link OO.ui.WindowManager#openWindow
1757 * openWindow} or the window's {@link #open open} methods are used, and the window manager begins to open
1758 * the window.
1759 *
1760 * - {@link #getSetupProcess} method is called and its result executed
1761 * - {@link #getReadyProcess} method is called and its result executed
1762 *
1763 * **opened**: The window is now open
1764 *
1765 * **closing**: The closing stage begins when the window manager's
1766 * {@link OO.ui.WindowManager#closeWindow closeWindow}
1767 * or the window's {@link #close} methods are used, and the window manager begins to close the window.
1768 *
1769 * - {@link #getHoldProcess} method is called and its result executed
1770 * - {@link #getTeardownProcess} method is called and its result executed. The window is now closed
1771 *
1772 * Each of the window's processes (setup, ready, hold, and teardown) can be extended in subclasses
1773 * by overriding the window's #getSetupProcess, #getReadyProcess, #getHoldProcess and #getTeardownProcess
1774 * methods. Note that each {@link OO.ui.Process process} is executed in series, so asynchronous
1775 * processing can complete. Always assume window processes are executed asynchronously.
1776 *
1777 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
1778 *
1779 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows
1780 *
1781 * @abstract
1782 * @class
1783 * @extends OO.ui.Element
1784 * @mixins OO.EventEmitter
1785 *
1786 * @constructor
1787 * @param {Object} [config] Configuration options
1788 * @cfg {string} [size] Symbolic name of the dialog size: `small`, `medium`, `large`, `larger` or
1789 * `full`. If omitted, the value of the {@link #static-size static size} property will be used.
1790 */
1791 OO.ui.Window = function OoUiWindow( config ) {
1792 // Configuration initialization
1793 config = config || {};
1794
1795 // Parent constructor
1796 OO.ui.Window.super.call( this, config );
1797
1798 // Mixin constructors
1799 OO.EventEmitter.call( this );
1800
1801 // Properties
1802 this.manager = null;
1803 this.size = config.size || this.constructor.static.size;
1804 this.$frame = $( '<div>' );
1805 this.$overlay = $( '<div>' );
1806 this.$content = $( '<div>' );
1807
1808 // Initialization
1809 this.$overlay.addClass( 'oo-ui-window-overlay' );
1810 this.$content
1811 .addClass( 'oo-ui-window-content' )
1812 .attr( 'tabindex', 0 );
1813 this.$frame
1814 .addClass( 'oo-ui-window-frame' )
1815 .append( this.$content );
1816
1817 this.$element
1818 .addClass( 'oo-ui-window' )
1819 .append( this.$frame, this.$overlay );
1820
1821 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
1822 // that reference properties not initialized at that time of parent class construction
1823 // TODO: Find a better way to handle post-constructor setup
1824 this.visible = false;
1825 this.$element.addClass( 'oo-ui-element-hidden' );
1826 };
1827
1828 /* Setup */
1829
1830 OO.inheritClass( OO.ui.Window, OO.ui.Element );
1831 OO.mixinClass( OO.ui.Window, OO.EventEmitter );
1832
1833 /* Static Properties */
1834
1835 /**
1836 * Symbolic name of the window size: `small`, `medium`, `large`, `larger` or `full`.
1837 *
1838 * The static size is used if no #size is configured during construction.
1839 *
1840 * @static
1841 * @inheritable
1842 * @property {string}
1843 */
1844 OO.ui.Window.static.size = 'medium';
1845
1846 /* Methods */
1847
1848 /**
1849 * Handle mouse down events.
1850 *
1851 * @private
1852 * @param {jQuery.Event} e Mouse down event
1853 */
1854 OO.ui.Window.prototype.onMouseDown = function ( e ) {
1855 // Prevent clicking on the click-block from stealing focus
1856 if ( e.target === this.$element[ 0 ] ) {
1857 return false;
1858 }
1859 };
1860
1861 /**
1862 * Check if the window has been initialized.
1863 *
1864 * Initialization occurs when a window is added to a manager.
1865 *
1866 * @return {boolean} Window has been initialized
1867 */
1868 OO.ui.Window.prototype.isInitialized = function () {
1869 return !!this.manager;
1870 };
1871
1872 /**
1873 * Check if the window is visible.
1874 *
1875 * @return {boolean} Window is visible
1876 */
1877 OO.ui.Window.prototype.isVisible = function () {
1878 return this.visible;
1879 };
1880
1881 /**
1882 * Check if the window is opening.
1883 *
1884 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpening isOpening}
1885 * method.
1886 *
1887 * @return {boolean} Window is opening
1888 */
1889 OO.ui.Window.prototype.isOpening = function () {
1890 return this.manager.isOpening( this );
1891 };
1892
1893 /**
1894 * Check if the window is closing.
1895 *
1896 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isClosing isClosing} method.
1897 *
1898 * @return {boolean} Window is closing
1899 */
1900 OO.ui.Window.prototype.isClosing = function () {
1901 return this.manager.isClosing( this );
1902 };
1903
1904 /**
1905 * Check if the window is opened.
1906 *
1907 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpened isOpened} method.
1908 *
1909 * @return {boolean} Window is opened
1910 */
1911 OO.ui.Window.prototype.isOpened = function () {
1912 return this.manager.isOpened( this );
1913 };
1914
1915 /**
1916 * Get the window manager.
1917 *
1918 * All windows must be attached to a window manager, which is used to open
1919 * and close the window and control its presentation.
1920 *
1921 * @return {OO.ui.WindowManager} Manager of window
1922 */
1923 OO.ui.Window.prototype.getManager = function () {
1924 return this.manager;
1925 };
1926
1927 /**
1928 * Get the symbolic name of the window size (e.g., `small` or `medium`).
1929 *
1930 * @return {string} Symbolic name of the size: `small`, `medium`, `large`, `larger`, `full`
1931 */
1932 OO.ui.Window.prototype.getSize = function () {
1933 return this.size;
1934 };
1935
1936 /**
1937 * Disable transitions on window's frame for the duration of the callback function, then enable them
1938 * back.
1939 *
1940 * @private
1941 * @param {Function} callback Function to call while transitions are disabled
1942 */
1943 OO.ui.Window.prototype.withoutSizeTransitions = function ( callback ) {
1944 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
1945 // Disable transitions first, otherwise we'll get values from when the window was animating.
1946 var oldTransition,
1947 styleObj = this.$frame[ 0 ].style;
1948 oldTransition = styleObj.transition || styleObj.OTransition || styleObj.MsTransition ||
1949 styleObj.MozTransition || styleObj.WebkitTransition;
1950 styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
1951 styleObj.MozTransition = styleObj.WebkitTransition = 'none';
1952 callback();
1953 // Force reflow to make sure the style changes done inside callback really are not transitioned
1954 this.$frame.height();
1955 styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
1956 styleObj.MozTransition = styleObj.WebkitTransition = oldTransition;
1957 };
1958
1959 /**
1960 * Get the height of the full window contents (i.e., the window head, body and foot together).
1961 *
1962 * What consistitutes the head, body, and foot varies depending on the window type.
1963 * A {@link OO.ui.MessageDialog message dialog} displays a title and message in its body,
1964 * and any actions in the foot. A {@link OO.ui.ProcessDialog process dialog} displays a title
1965 * and special actions in the head, and dialog content in the body.
1966 *
1967 * To get just the height of the dialog body, use the #getBodyHeight method.
1968 *
1969 * @return {number} The height of the window contents (the dialog head, body and foot) in pixels
1970 */
1971 OO.ui.Window.prototype.getContentHeight = function () {
1972 var bodyHeight,
1973 win = this,
1974 bodyStyleObj = this.$body[ 0 ].style,
1975 frameStyleObj = this.$frame[ 0 ].style;
1976
1977 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
1978 // Disable transitions first, otherwise we'll get values from when the window was animating.
1979 this.withoutSizeTransitions( function () {
1980 var oldHeight = frameStyleObj.height,
1981 oldPosition = bodyStyleObj.position;
1982 frameStyleObj.height = '1px';
1983 // Force body to resize to new width
1984 bodyStyleObj.position = 'relative';
1985 bodyHeight = win.getBodyHeight();
1986 frameStyleObj.height = oldHeight;
1987 bodyStyleObj.position = oldPosition;
1988 } );
1989
1990 return (
1991 // Add buffer for border
1992 ( this.$frame.outerHeight() - this.$frame.innerHeight() ) +
1993 // Use combined heights of children
1994 ( this.$head.outerHeight( true ) + bodyHeight + this.$foot.outerHeight( true ) )
1995 );
1996 };
1997
1998 /**
1999 * Get the height of the window body.
2000 *
2001 * To get the height of the full window contents (the window body, head, and foot together),
2002 * use #getContentHeight.
2003 *
2004 * When this function is called, the window will temporarily have been resized
2005 * to height=1px, so .scrollHeight measurements can be taken accurately.
2006 *
2007 * @return {number} Height of the window body in pixels
2008 */
2009 OO.ui.Window.prototype.getBodyHeight = function () {
2010 return this.$body[ 0 ].scrollHeight;
2011 };
2012
2013 /**
2014 * Get the directionality of the frame (right-to-left or left-to-right).
2015 *
2016 * @return {string} Directionality: `'ltr'` or `'rtl'`
2017 */
2018 OO.ui.Window.prototype.getDir = function () {
2019 return this.dir;
2020 };
2021
2022 /**
2023 * Get the 'setup' process.
2024 *
2025 * The setup process is used to set up a window for use in a particular context,
2026 * based on the `data` argument. This method is called during the opening phase of the window’s
2027 * lifecycle.
2028 *
2029 * Override this method to add additional steps to the ‘setup’ process the parent method provides
2030 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2031 * of OO.ui.Process.
2032 *
2033 * To add window content that persists between openings, you may wish to use the #initialize method
2034 * instead.
2035 *
2036 * @abstract
2037 * @param {Object} [data] Window opening data
2038 * @return {OO.ui.Process} Setup process
2039 */
2040 OO.ui.Window.prototype.getSetupProcess = function () {
2041 return new OO.ui.Process();
2042 };
2043
2044 /**
2045 * Get the ‘ready’ process.
2046 *
2047 * The ready process is used to ready a window for use in a particular
2048 * context, based on the `data` argument. This method is called during the opening phase of
2049 * the window’s lifecycle, after the window has been {@link #getSetupProcess setup}.
2050 *
2051 * Override this method to add additional steps to the ‘ready’ process the parent method
2052 * provides using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next}
2053 * methods of OO.ui.Process.
2054 *
2055 * @abstract
2056 * @param {Object} [data] Window opening data
2057 * @return {OO.ui.Process} Ready process
2058 */
2059 OO.ui.Window.prototype.getReadyProcess = function () {
2060 return new OO.ui.Process();
2061 };
2062
2063 /**
2064 * Get the 'hold' process.
2065 *
2066 * The hold proccess is used to keep a window from being used in a particular context,
2067 * based on the `data` argument. This method is called during the closing phase of the window’s
2068 * lifecycle.
2069 *
2070 * Override this method to add additional steps to the 'hold' process the parent method provides
2071 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2072 * of OO.ui.Process.
2073 *
2074 * @abstract
2075 * @param {Object} [data] Window closing data
2076 * @return {OO.ui.Process} Hold process
2077 */
2078 OO.ui.Window.prototype.getHoldProcess = function () {
2079 return new OO.ui.Process();
2080 };
2081
2082 /**
2083 * Get the ‘teardown’ process.
2084 *
2085 * The teardown process is used to teardown a window after use. During teardown,
2086 * user interactions within the window are conveyed and the window is closed, based on the `data`
2087 * argument. This method is called during the closing phase of the window’s lifecycle.
2088 *
2089 * Override this method to add additional steps to the ‘teardown’ process the parent method provides
2090 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2091 * of OO.ui.Process.
2092 *
2093 * @abstract
2094 * @param {Object} [data] Window closing data
2095 * @return {OO.ui.Process} Teardown process
2096 */
2097 OO.ui.Window.prototype.getTeardownProcess = function () {
2098 return new OO.ui.Process();
2099 };
2100
2101 /**
2102 * Set the window manager.
2103 *
2104 * This will cause the window to initialize. Calling it more than once will cause an error.
2105 *
2106 * @param {OO.ui.WindowManager} manager Manager for this window
2107 * @throws {Error} An error is thrown if the method is called more than once
2108 * @chainable
2109 */
2110 OO.ui.Window.prototype.setManager = function ( manager ) {
2111 if ( this.manager ) {
2112 throw new Error( 'Cannot set window manager, window already has a manager' );
2113 }
2114
2115 this.manager = manager;
2116 this.initialize();
2117
2118 return this;
2119 };
2120
2121 /**
2122 * Set the window size by symbolic name (e.g., 'small' or 'medium')
2123 *
2124 * @param {string} size Symbolic name of size: `small`, `medium`, `large`, `larger` or
2125 * `full`
2126 * @chainable
2127 */
2128 OO.ui.Window.prototype.setSize = function ( size ) {
2129 this.size = size;
2130 this.updateSize();
2131 return this;
2132 };
2133
2134 /**
2135 * Update the window size.
2136 *
2137 * @throws {Error} An error is thrown if the window is not attached to a window manager
2138 * @chainable
2139 */
2140 OO.ui.Window.prototype.updateSize = function () {
2141 if ( !this.manager ) {
2142 throw new Error( 'Cannot update window size, must be attached to a manager' );
2143 }
2144
2145 this.manager.updateWindowSize( this );
2146
2147 return this;
2148 };
2149
2150 /**
2151 * Set window dimensions. This method is called by the {@link OO.ui.WindowManager window manager}
2152 * when the window is opening. In general, setDimensions should not be called directly.
2153 *
2154 * To set the size of the window, use the #setSize method.
2155 *
2156 * @param {Object} dim CSS dimension properties
2157 * @param {string|number} [dim.width] Width
2158 * @param {string|number} [dim.minWidth] Minimum width
2159 * @param {string|number} [dim.maxWidth] Maximum width
2160 * @param {string|number} [dim.width] Height, omit to set based on height of contents
2161 * @param {string|number} [dim.minWidth] Minimum height
2162 * @param {string|number} [dim.maxWidth] Maximum height
2163 * @chainable
2164 */
2165 OO.ui.Window.prototype.setDimensions = function ( dim ) {
2166 var height,
2167 win = this,
2168 styleObj = this.$frame[ 0 ].style;
2169
2170 // Calculate the height we need to set using the correct width
2171 if ( dim.height === undefined ) {
2172 this.withoutSizeTransitions( function () {
2173 var oldWidth = styleObj.width;
2174 win.$frame.css( 'width', dim.width || '' );
2175 height = win.getContentHeight();
2176 styleObj.width = oldWidth;
2177 } );
2178 } else {
2179 height = dim.height;
2180 }
2181
2182 this.$frame.css( {
2183 width: dim.width || '',
2184 minWidth: dim.minWidth || '',
2185 maxWidth: dim.maxWidth || '',
2186 height: height || '',
2187 minHeight: dim.minHeight || '',
2188 maxHeight: dim.maxHeight || ''
2189 } );
2190
2191 return this;
2192 };
2193
2194 /**
2195 * Initialize window contents.
2196 *
2197 * Before the window is opened for the first time, #initialize is called so that content that
2198 * persists between openings can be added to the window.
2199 *
2200 * To set up a window with new content each time the window opens, use #getSetupProcess.
2201 *
2202 * @throws {Error} An error is thrown if the window is not attached to a window manager
2203 * @chainable
2204 */
2205 OO.ui.Window.prototype.initialize = function () {
2206 if ( !this.manager ) {
2207 throw new Error( 'Cannot initialize window, must be attached to a manager' );
2208 }
2209
2210 // Properties
2211 this.$head = $( '<div>' );
2212 this.$body = $( '<div>' );
2213 this.$foot = $( '<div>' );
2214 this.dir = OO.ui.Element.static.getDir( this.$content ) || 'ltr';
2215 this.$document = $( this.getElementDocument() );
2216
2217 // Events
2218 this.$element.on( 'mousedown', this.onMouseDown.bind( this ) );
2219
2220 // Initialization
2221 this.$head.addClass( 'oo-ui-window-head' );
2222 this.$body.addClass( 'oo-ui-window-body' );
2223 this.$foot.addClass( 'oo-ui-window-foot' );
2224 this.$content.append( this.$head, this.$body, this.$foot );
2225
2226 return this;
2227 };
2228
2229 /**
2230 * Open the window.
2231 *
2232 * This method is a wrapper around a call to the window manager’s {@link OO.ui.WindowManager#openWindow openWindow}
2233 * method, which returns a promise resolved when the window is done opening.
2234 *
2235 * To customize the window each time it opens, use #getSetupProcess or #getReadyProcess.
2236 *
2237 * @param {Object} [data] Window opening data
2238 * @return {jQuery.Promise} Promise resolved with a value when the window is opened, or rejected
2239 * if the window fails to open. When the promise is resolved successfully, the first argument of the
2240 * value is a new promise, which is resolved when the window begins closing.
2241 * @throws {Error} An error is thrown if the window is not attached to a window manager
2242 */
2243 OO.ui.Window.prototype.open = function ( data ) {
2244 if ( !this.manager ) {
2245 throw new Error( 'Cannot open window, must be attached to a manager' );
2246 }
2247
2248 return this.manager.openWindow( this, data );
2249 };
2250
2251 /**
2252 * Close the window.
2253 *
2254 * This method is a wrapper around a call to the window
2255 * manager’s {@link OO.ui.WindowManager#closeWindow closeWindow} method,
2256 * which returns a closing promise resolved when the window is done closing.
2257 *
2258 * The window's #getHoldProcess and #getTeardownProcess methods are called during the closing
2259 * phase of the window’s lifecycle and can be used to specify closing behavior each time
2260 * the window closes.
2261 *
2262 * @param {Object} [data] Window closing data
2263 * @return {jQuery.Promise} Promise resolved when window is closed
2264 * @throws {Error} An error is thrown if the window is not attached to a window manager
2265 */
2266 OO.ui.Window.prototype.close = function ( data ) {
2267 if ( !this.manager ) {
2268 throw new Error( 'Cannot close window, must be attached to a manager' );
2269 }
2270
2271 return this.manager.closeWindow( this, data );
2272 };
2273
2274 /**
2275 * Setup window.
2276 *
2277 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2278 * by other systems.
2279 *
2280 * @param {Object} [data] Window opening data
2281 * @return {jQuery.Promise} Promise resolved when window is setup
2282 */
2283 OO.ui.Window.prototype.setup = function ( data ) {
2284 var win = this,
2285 deferred = $.Deferred();
2286
2287 this.toggle( true );
2288
2289 this.getSetupProcess( data ).execute().done( function () {
2290 // Force redraw by asking the browser to measure the elements' widths
2291 win.$element.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2292 win.$content.addClass( 'oo-ui-window-content-setup' ).width();
2293 deferred.resolve();
2294 } );
2295
2296 return deferred.promise();
2297 };
2298
2299 /**
2300 * Ready window.
2301 *
2302 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2303 * by other systems.
2304 *
2305 * @param {Object} [data] Window opening data
2306 * @return {jQuery.Promise} Promise resolved when window is ready
2307 */
2308 OO.ui.Window.prototype.ready = function ( data ) {
2309 var win = this,
2310 deferred = $.Deferred();
2311
2312 this.$content.focus();
2313 this.getReadyProcess( data ).execute().done( function () {
2314 // Force redraw by asking the browser to measure the elements' widths
2315 win.$element.addClass( 'oo-ui-window-ready' ).width();
2316 win.$content.addClass( 'oo-ui-window-content-ready' ).width();
2317 deferred.resolve();
2318 } );
2319
2320 return deferred.promise();
2321 };
2322
2323 /**
2324 * Hold window.
2325 *
2326 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2327 * by other systems.
2328 *
2329 * @param {Object} [data] Window closing data
2330 * @return {jQuery.Promise} Promise resolved when window is held
2331 */
2332 OO.ui.Window.prototype.hold = function ( data ) {
2333 var win = this,
2334 deferred = $.Deferred();
2335
2336 this.getHoldProcess( data ).execute().done( function () {
2337 // Get the focused element within the window's content
2338 var $focus = win.$content.find( OO.ui.Element.static.getDocument( win.$content ).activeElement );
2339
2340 // Blur the focused element
2341 if ( $focus.length ) {
2342 $focus[ 0 ].blur();
2343 }
2344
2345 // Force redraw by asking the browser to measure the elements' widths
2346 win.$element.removeClass( 'oo-ui-window-ready' ).width();
2347 win.$content.removeClass( 'oo-ui-window-content-ready' ).width();
2348 deferred.resolve();
2349 } );
2350
2351 return deferred.promise();
2352 };
2353
2354 /**
2355 * Teardown window.
2356 *
2357 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2358 * by other systems.
2359 *
2360 * @param {Object} [data] Window closing data
2361 * @return {jQuery.Promise} Promise resolved when window is torn down
2362 */
2363 OO.ui.Window.prototype.teardown = function ( data ) {
2364 var win = this;
2365
2366 return this.getTeardownProcess( data ).execute()
2367 .done( function () {
2368 // Force redraw by asking the browser to measure the elements' widths
2369 win.$element.removeClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2370 win.$content.removeClass( 'oo-ui-window-content-setup' ).width();
2371 win.toggle( false );
2372 } );
2373 };
2374
2375 /**
2376 * The Dialog class serves as the base class for the other types of dialogs.
2377 * Unless extended to include controls, the rendered dialog box is a simple window
2378 * that users can close by hitting the ‘Esc’ key. Dialog windows are used with OO.ui.WindowManager,
2379 * which opens, closes, and controls the presentation of the window. See the
2380 * [OOjs UI documentation on MediaWiki] [1] for more information.
2381 *
2382 * @example
2383 * // A simple dialog window.
2384 * function MyDialog( config ) {
2385 * MyDialog.super.call( this, config );
2386 * }
2387 * OO.inheritClass( MyDialog, OO.ui.Dialog );
2388 * MyDialog.prototype.initialize = function () {
2389 * MyDialog.super.prototype.initialize.call( this );
2390 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
2391 * this.content.$element.append( '<p>A simple dialog window. Press \'Esc\' to close.</p>' );
2392 * this.$body.append( this.content.$element );
2393 * };
2394 * MyDialog.prototype.getBodyHeight = function () {
2395 * return this.content.$element.outerHeight( true );
2396 * };
2397 * var myDialog = new MyDialog( {
2398 * size: 'medium'
2399 * } );
2400 * // Create and append a window manager, which opens and closes the window.
2401 * var windowManager = new OO.ui.WindowManager();
2402 * $( 'body' ).append( windowManager.$element );
2403 * windowManager.addWindows( [ myDialog ] );
2404 * // Open the window!
2405 * windowManager.openWindow( myDialog );
2406 *
2407 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Dialogs
2408 *
2409 * @abstract
2410 * @class
2411 * @extends OO.ui.Window
2412 * @mixins OO.ui.PendingElement
2413 *
2414 * @constructor
2415 * @param {Object} [config] Configuration options
2416 */
2417 OO.ui.Dialog = function OoUiDialog( config ) {
2418 // Parent constructor
2419 OO.ui.Dialog.super.call( this, config );
2420
2421 // Mixin constructors
2422 OO.ui.PendingElement.call( this );
2423
2424 // Properties
2425 this.actions = new OO.ui.ActionSet();
2426 this.attachedActions = [];
2427 this.currentAction = null;
2428 this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
2429
2430 // Events
2431 this.actions.connect( this, {
2432 click: 'onActionClick',
2433 resize: 'onActionResize',
2434 change: 'onActionsChange'
2435 } );
2436
2437 // Initialization
2438 this.$element
2439 .addClass( 'oo-ui-dialog' )
2440 .attr( 'role', 'dialog' );
2441 };
2442
2443 /* Setup */
2444
2445 OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
2446 OO.mixinClass( OO.ui.Dialog, OO.ui.PendingElement );
2447
2448 /* Static Properties */
2449
2450 /**
2451 * Symbolic name of dialog.
2452 *
2453 * The dialog class must have a symbolic name in order to be registered with OO.Factory.
2454 * Please see the [OOjs UI documentation on MediaWiki] [3] for more information.
2455 *
2456 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
2457 *
2458 * @abstract
2459 * @static
2460 * @inheritable
2461 * @property {string}
2462 */
2463 OO.ui.Dialog.static.name = '';
2464
2465 /**
2466 * The dialog title.
2467 *
2468 * The title can be specified as a plaintext string, a {@link OO.ui.LabelElement Label} node, or a function
2469 * that will produce a Label node or string. The title can also be specified with data passed to the
2470 * constructor (see #getSetupProcess). In this case, the static value will be overriden.
2471 *
2472 * @abstract
2473 * @static
2474 * @inheritable
2475 * @property {jQuery|string|Function}
2476 */
2477 OO.ui.Dialog.static.title = '';
2478
2479 /**
2480 * An array of configured {@link OO.ui.ActionWidget action widgets}.
2481 *
2482 * Actions can also be specified with data passed to the constructor (see #getSetupProcess). In this case, the static
2483 * value will be overriden.
2484 *
2485 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
2486 *
2487 * @static
2488 * @inheritable
2489 * @property {Object[]}
2490 */
2491 OO.ui.Dialog.static.actions = [];
2492
2493 /**
2494 * Close the dialog when the 'Esc' key is pressed.
2495 *
2496 * @static
2497 * @abstract
2498 * @inheritable
2499 * @property {boolean}
2500 */
2501 OO.ui.Dialog.static.escapable = true;
2502
2503 /* Methods */
2504
2505 /**
2506 * Handle frame document key down events.
2507 *
2508 * @private
2509 * @param {jQuery.Event} e Key down event
2510 */
2511 OO.ui.Dialog.prototype.onDocumentKeyDown = function ( e ) {
2512 if ( e.which === OO.ui.Keys.ESCAPE ) {
2513 this.close();
2514 e.preventDefault();
2515 e.stopPropagation();
2516 }
2517 };
2518
2519 /**
2520 * Handle action resized events.
2521 *
2522 * @private
2523 * @param {OO.ui.ActionWidget} action Action that was resized
2524 */
2525 OO.ui.Dialog.prototype.onActionResize = function () {
2526 // Override in subclass
2527 };
2528
2529 /**
2530 * Handle action click events.
2531 *
2532 * @private
2533 * @param {OO.ui.ActionWidget} action Action that was clicked
2534 */
2535 OO.ui.Dialog.prototype.onActionClick = function ( action ) {
2536 if ( !this.isPending() ) {
2537 this.executeAction( action.getAction() );
2538 }
2539 };
2540
2541 /**
2542 * Handle actions change event.
2543 *
2544 * @private
2545 */
2546 OO.ui.Dialog.prototype.onActionsChange = function () {
2547 this.detachActions();
2548 if ( !this.isClosing() ) {
2549 this.attachActions();
2550 }
2551 };
2552
2553 /**
2554 * Get the set of actions used by the dialog.
2555 *
2556 * @return {OO.ui.ActionSet}
2557 */
2558 OO.ui.Dialog.prototype.getActions = function () {
2559 return this.actions;
2560 };
2561
2562 /**
2563 * Get a process for taking action.
2564 *
2565 * When you override this method, you can create a new OO.ui.Process and return it, or add additional
2566 * accept steps to the process the parent method provides using the {@link OO.ui.Process#first 'first'}
2567 * and {@link OO.ui.Process#next 'next'} methods of OO.ui.Process.
2568 *
2569 * @abstract
2570 * @param {string} [action] Symbolic name of action
2571 * @return {OO.ui.Process} Action process
2572 */
2573 OO.ui.Dialog.prototype.getActionProcess = function ( action ) {
2574 return new OO.ui.Process()
2575 .next( function () {
2576 if ( !action ) {
2577 // An empty action always closes the dialog without data, which should always be
2578 // safe and make no changes
2579 this.close();
2580 }
2581 }, this );
2582 };
2583
2584 /**
2585 * @inheritdoc
2586 *
2587 * @param {Object} [data] Dialog opening data
2588 * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use
2589 * the {@link #static-title static title}
2590 * @param {Object[]} [data.actions] List of configuration options for each
2591 * {@link OO.ui.ActionWidget action widget}, omit to use {@link #static-actions static actions}.
2592 */
2593 OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
2594 data = data || {};
2595
2596 // Parent method
2597 return OO.ui.Dialog.super.prototype.getSetupProcess.call( this, data )
2598 .next( function () {
2599 var config = this.constructor.static,
2600 actions = data.actions !== undefined ? data.actions : config.actions;
2601
2602 this.title.setLabel(
2603 data.title !== undefined ? data.title : this.constructor.static.title
2604 );
2605 this.actions.add( this.getActionWidgets( actions ) );
2606
2607 if ( this.constructor.static.escapable ) {
2608 this.$document.on( 'keydown', this.onDocumentKeyDownHandler );
2609 }
2610 }, this );
2611 };
2612
2613 /**
2614 * @inheritdoc
2615 */
2616 OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
2617 // Parent method
2618 return OO.ui.Dialog.super.prototype.getTeardownProcess.call( this, data )
2619 .first( function () {
2620 if ( this.constructor.static.escapable ) {
2621 this.$document.off( 'keydown', this.onDocumentKeyDownHandler );
2622 }
2623
2624 this.actions.clear();
2625 this.currentAction = null;
2626 }, this );
2627 };
2628
2629 /**
2630 * @inheritdoc
2631 */
2632 OO.ui.Dialog.prototype.initialize = function () {
2633 // Parent method
2634 OO.ui.Dialog.super.prototype.initialize.call( this );
2635
2636 // Properties
2637 this.title = new OO.ui.LabelWidget();
2638
2639 // Initialization
2640 this.$content.addClass( 'oo-ui-dialog-content' );
2641 this.setPendingElement( this.$head );
2642 };
2643
2644 /**
2645 * Get action widgets from a list of configs
2646 *
2647 * @param {Object[]} actions Action widget configs
2648 * @return {OO.ui.ActionWidget[]} Action widgets
2649 */
2650 OO.ui.Dialog.prototype.getActionWidgets = function ( actions ) {
2651 var i, len, widgets = [];
2652 for ( i = 0, len = actions.length; i < len; i++ ) {
2653 widgets.push(
2654 new OO.ui.ActionWidget( actions[ i ] )
2655 );
2656 }
2657 return widgets;
2658 };
2659
2660 /**
2661 * Attach action actions.
2662 *
2663 * @protected
2664 */
2665 OO.ui.Dialog.prototype.attachActions = function () {
2666 // Remember the list of potentially attached actions
2667 this.attachedActions = this.actions.get();
2668 };
2669
2670 /**
2671 * Detach action actions.
2672 *
2673 * @protected
2674 * @chainable
2675 */
2676 OO.ui.Dialog.prototype.detachActions = function () {
2677 var i, len;
2678
2679 // Detach all actions that may have been previously attached
2680 for ( i = 0, len = this.attachedActions.length; i < len; i++ ) {
2681 this.attachedActions[ i ].$element.detach();
2682 }
2683 this.attachedActions = [];
2684 };
2685
2686 /**
2687 * Execute an action.
2688 *
2689 * @param {string} action Symbolic name of action to execute
2690 * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
2691 */
2692 OO.ui.Dialog.prototype.executeAction = function ( action ) {
2693 this.pushPending();
2694 this.currentAction = action;
2695 return this.getActionProcess( action ).execute()
2696 .always( this.popPending.bind( this ) );
2697 };
2698
2699 /**
2700 * Window managers are used to open and close {@link OO.ui.Window windows} and control their presentation.
2701 * Managed windows are mutually exclusive. If a new window is opened while a current window is opening
2702 * or is opened, the current window will be closed and any ongoing {@link OO.ui.Process process} will be cancelled. Windows
2703 * themselves are persistent and—rather than being torn down when closed—can be repopulated with the
2704 * pertinent data and reused.
2705 *
2706 * Over the lifecycle of a window, the window manager makes available three promises: `opening`,
2707 * `opened`, and `closing`, which represent the primary stages of the cycle:
2708 *
2709 * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s
2710 * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window.
2711 *
2712 * - an `opening` event is emitted with an `opening` promise
2713 * - the #getSetupDelay method is called and the returned value is used to time a pause in execution before
2714 * the window’s {@link OO.ui.Window#getSetupProcess getSetupProcess} method is called on the
2715 * window and its result executed
2716 * - a `setup` progress notification is emitted from the `opening` promise
2717 * - the #getReadyDelay method is called the returned value is used to time a pause in execution before
2718 * the window’s {@link OO.ui.Window#getReadyProcess getReadyProcess} method is called on the
2719 * window and its result executed
2720 * - a `ready` progress notification is emitted from the `opening` promise
2721 * - the `opening` promise is resolved with an `opened` promise
2722 *
2723 * **Opened**: the window is now open.
2724 *
2725 * **Closing**: the closing stage begins when the window manager's #closeWindow or the
2726 * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins
2727 * to close the window.
2728 *
2729 * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted
2730 * - the #getHoldDelay method is called and the returned value is used to time a pause in execution before
2731 * the window's {@link OO.ui.Window#getHoldProcess getHoldProces} method is called on the
2732 * window and its result executed
2733 * - a `hold` progress notification is emitted from the `closing` promise
2734 * - the #getTeardownDelay() method is called and the returned value is used to time a pause in execution before
2735 * the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method is called on the
2736 * window and its result executed
2737 * - a `teardown` progress notification is emitted from the `closing` promise
2738 * - the `closing` promise is resolved. The window is now closed
2739 *
2740 * See the [OOjs UI documentation on MediaWiki][1] for more information.
2741 *
2742 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
2743 *
2744 * @class
2745 * @extends OO.ui.Element
2746 * @mixins OO.EventEmitter
2747 *
2748 * @constructor
2749 * @param {Object} [config] Configuration options
2750 * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
2751 * Note that window classes that are instantiated with a factory must have
2752 * a {@link OO.ui.Dialog#static-name static name} property that specifies a symbolic name.
2753 * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
2754 */
2755 OO.ui.WindowManager = function OoUiWindowManager( config ) {
2756 // Configuration initialization
2757 config = config || {};
2758
2759 // Parent constructor
2760 OO.ui.WindowManager.super.call( this, config );
2761
2762 // Mixin constructors
2763 OO.EventEmitter.call( this );
2764
2765 // Properties
2766 this.factory = config.factory;
2767 this.modal = config.modal === undefined || !!config.modal;
2768 this.windows = {};
2769 this.opening = null;
2770 this.opened = null;
2771 this.closing = null;
2772 this.preparingToOpen = null;
2773 this.preparingToClose = null;
2774 this.currentWindow = null;
2775 this.globalEvents = false;
2776 this.$ariaHidden = null;
2777 this.onWindowResizeTimeout = null;
2778 this.onWindowResizeHandler = this.onWindowResize.bind( this );
2779 this.afterWindowResizeHandler = this.afterWindowResize.bind( this );
2780
2781 // Initialization
2782 this.$element
2783 .addClass( 'oo-ui-windowManager' )
2784 .toggleClass( 'oo-ui-windowManager-modal', this.modal );
2785 };
2786
2787 /* Setup */
2788
2789 OO.inheritClass( OO.ui.WindowManager, OO.ui.Element );
2790 OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter );
2791
2792 /* Events */
2793
2794 /**
2795 * An 'opening' event is emitted when the window begins to be opened.
2796 *
2797 * @event opening
2798 * @param {OO.ui.Window} win Window that's being opened
2799 * @param {jQuery.Promise} opening An `opening` promise resolved with a value when the window is opened successfully.
2800 * When the `opening` promise is resolved, the first argument of the value is an 'opened' promise, the second argument
2801 * is the opening data. The `opening` promise emits `setup` and `ready` notifications when those processes are complete.
2802 * @param {Object} data Window opening data
2803 */
2804
2805 /**
2806 * A 'closing' event is emitted when the window begins to be closed.
2807 *
2808 * @event closing
2809 * @param {OO.ui.Window} win Window that's being closed
2810 * @param {jQuery.Promise} closing A `closing` promise is resolved with a value when the window
2811 * is closed successfully. The promise emits `hold` and `teardown` notifications when those
2812 * processes are complete. When the `closing` promise is resolved, the first argument of its value
2813 * is the closing data.
2814 * @param {Object} data Window closing data
2815 */
2816
2817 /**
2818 * A 'resize' event is emitted when a window is resized.
2819 *
2820 * @event resize
2821 * @param {OO.ui.Window} win Window that was resized
2822 */
2823
2824 /* Static Properties */
2825
2826 /**
2827 * Map of the symbolic name of each window size and its CSS properties.
2828 *
2829 * @static
2830 * @inheritable
2831 * @property {Object}
2832 */
2833 OO.ui.WindowManager.static.sizes = {
2834 small: {
2835 width: 300
2836 },
2837 medium: {
2838 width: 500
2839 },
2840 large: {
2841 width: 700
2842 },
2843 larger: {
2844 width: 900
2845 },
2846 full: {
2847 // These can be non-numeric because they are never used in calculations
2848 width: '100%',
2849 height: '100%'
2850 }
2851 };
2852
2853 /**
2854 * Symbolic name of the default window size.
2855 *
2856 * The default size is used if the window's requested size is not recognized.
2857 *
2858 * @static
2859 * @inheritable
2860 * @property {string}
2861 */
2862 OO.ui.WindowManager.static.defaultSize = 'medium';
2863
2864 /* Methods */
2865
2866 /**
2867 * Handle window resize events.
2868 *
2869 * @private
2870 * @param {jQuery.Event} e Window resize event
2871 */
2872 OO.ui.WindowManager.prototype.onWindowResize = function () {
2873 clearTimeout( this.onWindowResizeTimeout );
2874 this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 );
2875 };
2876
2877 /**
2878 * Handle window resize events.
2879 *
2880 * @private
2881 * @param {jQuery.Event} e Window resize event
2882 */
2883 OO.ui.WindowManager.prototype.afterWindowResize = function () {
2884 if ( this.currentWindow ) {
2885 this.updateWindowSize( this.currentWindow );
2886 }
2887 };
2888
2889 /**
2890 * Check if window is opening.
2891 *
2892 * @return {boolean} Window is opening
2893 */
2894 OO.ui.WindowManager.prototype.isOpening = function ( win ) {
2895 return win === this.currentWindow && !!this.opening && this.opening.state() === 'pending';
2896 };
2897
2898 /**
2899 * Check if window is closing.
2900 *
2901 * @return {boolean} Window is closing
2902 */
2903 OO.ui.WindowManager.prototype.isClosing = function ( win ) {
2904 return win === this.currentWindow && !!this.closing && this.closing.state() === 'pending';
2905 };
2906
2907 /**
2908 * Check if window is opened.
2909 *
2910 * @return {boolean} Window is opened
2911 */
2912 OO.ui.WindowManager.prototype.isOpened = function ( win ) {
2913 return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending';
2914 };
2915
2916 /**
2917 * Check if a window is being managed.
2918 *
2919 * @param {OO.ui.Window} win Window to check
2920 * @return {boolean} Window is being managed
2921 */
2922 OO.ui.WindowManager.prototype.hasWindow = function ( win ) {
2923 var name;
2924
2925 for ( name in this.windows ) {
2926 if ( this.windows[ name ] === win ) {
2927 return true;
2928 }
2929 }
2930
2931 return false;
2932 };
2933
2934 /**
2935 * Get the number of milliseconds to wait after opening begins before executing the ‘setup’ process.
2936 *
2937 * @param {OO.ui.Window} win Window being opened
2938 * @param {Object} [data] Window opening data
2939 * @return {number} Milliseconds to wait
2940 */
2941 OO.ui.WindowManager.prototype.getSetupDelay = function () {
2942 return 0;
2943 };
2944
2945 /**
2946 * Get the number of milliseconds to wait after setup has finished before executing the ‘ready’ process.
2947 *
2948 * @param {OO.ui.Window} win Window being opened
2949 * @param {Object} [data] Window opening data
2950 * @return {number} Milliseconds to wait
2951 */
2952 OO.ui.WindowManager.prototype.getReadyDelay = function () {
2953 return 0;
2954 };
2955
2956 /**
2957 * Get the number of milliseconds to wait after closing has begun before executing the 'hold' process.
2958 *
2959 * @param {OO.ui.Window} win Window being closed
2960 * @param {Object} [data] Window closing data
2961 * @return {number} Milliseconds to wait
2962 */
2963 OO.ui.WindowManager.prototype.getHoldDelay = function () {
2964 return 0;
2965 };
2966
2967 /**
2968 * Get the number of milliseconds to wait after the ‘hold’ process has finished before
2969 * executing the ‘teardown’ process.
2970 *
2971 * @param {OO.ui.Window} win Window being closed
2972 * @param {Object} [data] Window closing data
2973 * @return {number} Milliseconds to wait
2974 */
2975 OO.ui.WindowManager.prototype.getTeardownDelay = function () {
2976 return this.modal ? 250 : 0;
2977 };
2978
2979 /**
2980 * Get a window by its symbolic name.
2981 *
2982 * If the window is not yet instantiated and its symbolic name is recognized by a factory, it will be
2983 * instantiated and added to the window manager automatically. Please see the [OOjs UI documentation on MediaWiki][3]
2984 * for more information about using factories.
2985 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
2986 *
2987 * @param {string} name Symbolic name of the window
2988 * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
2989 * @throws {Error} An error is thrown if the symbolic name is not recognized by the factory.
2990 * @throws {Error} An error is thrown if the named window is not recognized as a managed window.
2991 */
2992 OO.ui.WindowManager.prototype.getWindow = function ( name ) {
2993 var deferred = $.Deferred(),
2994 win = this.windows[ name ];
2995
2996 if ( !( win instanceof OO.ui.Window ) ) {
2997 if ( this.factory ) {
2998 if ( !this.factory.lookup( name ) ) {
2999 deferred.reject( new OO.ui.Error(
3000 'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
3001 ) );
3002 } else {
3003 win = this.factory.create( name );
3004 this.addWindows( [ win ] );
3005 deferred.resolve( win );
3006 }
3007 } else {
3008 deferred.reject( new OO.ui.Error(
3009 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
3010 ) );
3011 }
3012 } else {
3013 deferred.resolve( win );
3014 }
3015
3016 return deferred.promise();
3017 };
3018
3019 /**
3020 * Get current window.
3021 *
3022 * @return {OO.ui.Window|null} Currently opening/opened/closing window
3023 */
3024 OO.ui.WindowManager.prototype.getCurrentWindow = function () {
3025 return this.currentWindow;
3026 };
3027
3028 /**
3029 * Open a window.
3030 *
3031 * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
3032 * @param {Object} [data] Window opening data
3033 * @return {jQuery.Promise} An `opening` promise resolved when the window is done opening.
3034 * See {@link #event-opening 'opening' event} for more information about `opening` promises.
3035 * @fires opening
3036 */
3037 OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
3038 var manager = this,
3039 opening = $.Deferred();
3040
3041 // Argument handling
3042 if ( typeof win === 'string' ) {
3043 return this.getWindow( win ).then( function ( win ) {
3044 return manager.openWindow( win, data );
3045 } );
3046 }
3047
3048 // Error handling
3049 if ( !this.hasWindow( win ) ) {
3050 opening.reject( new OO.ui.Error(
3051 'Cannot open window: window is not attached to manager'
3052 ) );
3053 } else if ( this.preparingToOpen || this.opening || this.opened ) {
3054 opening.reject( new OO.ui.Error(
3055 'Cannot open window: another window is opening or open'
3056 ) );
3057 }
3058
3059 // Window opening
3060 if ( opening.state() !== 'rejected' ) {
3061 // If a window is currently closing, wait for it to complete
3062 this.preparingToOpen = $.when( this.closing );
3063 // Ensure handlers get called after preparingToOpen is set
3064 this.preparingToOpen.done( function () {
3065 if ( manager.modal ) {
3066 manager.toggleGlobalEvents( true );
3067 manager.toggleAriaIsolation( true );
3068 }
3069 manager.currentWindow = win;
3070 manager.opening = opening;
3071 manager.preparingToOpen = null;
3072 manager.emit( 'opening', win, opening, data );
3073 setTimeout( function () {
3074 win.setup( data ).then( function () {
3075 manager.updateWindowSize( win );
3076 manager.opening.notify( { state: 'setup' } );
3077 setTimeout( function () {
3078 win.ready( data ).then( function () {
3079 manager.opening.notify( { state: 'ready' } );
3080 manager.opening = null;
3081 manager.opened = $.Deferred();
3082 opening.resolve( manager.opened.promise(), data );
3083 } );
3084 }, manager.getReadyDelay() );
3085 } );
3086 }, manager.getSetupDelay() );
3087 } );
3088 }
3089
3090 return opening.promise();
3091 };
3092
3093 /**
3094 * Close a window.
3095 *
3096 * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
3097 * @param {Object} [data] Window closing data
3098 * @return {jQuery.Promise} A `closing` promise resolved when the window is done closing.
3099 * See {@link #event-closing 'closing' event} for more information about closing promises.
3100 * @throws {Error} An error is thrown if the window is not managed by the window manager.
3101 * @fires closing
3102 */
3103 OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
3104 var manager = this,
3105 closing = $.Deferred(),
3106 opened;
3107
3108 // Argument handling
3109 if ( typeof win === 'string' ) {
3110 win = this.windows[ win ];
3111 } else if ( !this.hasWindow( win ) ) {
3112 win = null;
3113 }
3114
3115 // Error handling
3116 if ( !win ) {
3117 closing.reject( new OO.ui.Error(
3118 'Cannot close window: window is not attached to manager'
3119 ) );
3120 } else if ( win !== this.currentWindow ) {
3121 closing.reject( new OO.ui.Error(
3122 'Cannot close window: window already closed with different data'
3123 ) );
3124 } else if ( this.preparingToClose || this.closing ) {
3125 closing.reject( new OO.ui.Error(
3126 'Cannot close window: window already closing with different data'
3127 ) );
3128 }
3129
3130 // Window closing
3131 if ( closing.state() !== 'rejected' ) {
3132 // If the window is currently opening, close it when it's done
3133 this.preparingToClose = $.when( this.opening );
3134 // Ensure handlers get called after preparingToClose is set
3135 this.preparingToClose.done( function () {
3136 manager.closing = closing;
3137 manager.preparingToClose = null;
3138 manager.emit( 'closing', win, closing, data );
3139 opened = manager.opened;
3140 manager.opened = null;
3141 opened.resolve( closing.promise(), data );
3142 setTimeout( function () {
3143 win.hold( data ).then( function () {
3144 closing.notify( { state: 'hold' } );
3145 setTimeout( function () {
3146 win.teardown( data ).then( function () {
3147 closing.notify( { state: 'teardown' } );
3148 if ( manager.modal ) {
3149 manager.toggleGlobalEvents( false );
3150 manager.toggleAriaIsolation( false );
3151 }
3152 manager.closing = null;
3153 manager.currentWindow = null;
3154 closing.resolve( data );
3155 } );
3156 }, manager.getTeardownDelay() );
3157 } );
3158 }, manager.getHoldDelay() );
3159 } );
3160 }
3161
3162 return closing.promise();
3163 };
3164
3165 /**
3166 * Add windows to the window manager.
3167 *
3168 * Windows can be added by reference, symbolic name, or explicitly defined symbolic names.
3169 * See the [OOjs ui documentation on MediaWiki] [2] for examples.
3170 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
3171 *
3172 * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows An array of window objects specified
3173 * by reference, symbolic name, or explicitly defined symbolic names.
3174 * @throws {Error} An error is thrown if a window is added by symbolic name, but has neither an
3175 * explicit nor a statically configured symbolic name.
3176 */
3177 OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
3178 var i, len, win, name, list;
3179
3180 if ( Array.isArray( windows ) ) {
3181 // Convert to map of windows by looking up symbolic names from static configuration
3182 list = {};
3183 for ( i = 0, len = windows.length; i < len; i++ ) {
3184 name = windows[ i ].constructor.static.name;
3185 if ( typeof name !== 'string' ) {
3186 throw new Error( 'Cannot add window' );
3187 }
3188 list[ name ] = windows[ i ];
3189 }
3190 } else if ( OO.isPlainObject( windows ) ) {
3191 list = windows;
3192 }
3193
3194 // Add windows
3195 for ( name in list ) {
3196 win = list[ name ];
3197 this.windows[ name ] = win.toggle( false );
3198 this.$element.append( win.$element );
3199 win.setManager( this );
3200 }
3201 };
3202
3203 /**
3204 * Remove the specified windows from the windows manager.
3205 *
3206 * Windows will be closed before they are removed. If you wish to remove all windows, you may wish to use
3207 * the #clearWindows method instead. If you no longer need the window manager and want to ensure that it no
3208 * longer listens to events, use the #destroy method.
3209 *
3210 * @param {string[]} names Symbolic names of windows to remove
3211 * @return {jQuery.Promise} Promise resolved when window is closed and removed
3212 * @throws {Error} An error is thrown if the named windows are not managed by the window manager.
3213 */
3214 OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
3215 var i, len, win, name, cleanupWindow,
3216 manager = this,
3217 promises = [],
3218 cleanup = function ( name, win ) {
3219 delete manager.windows[ name ];
3220 win.$element.detach();
3221 };
3222
3223 for ( i = 0, len = names.length; i < len; i++ ) {
3224 name = names[ i ];
3225 win = this.windows[ name ];
3226 if ( !win ) {
3227 throw new Error( 'Cannot remove window' );
3228 }
3229 cleanupWindow = cleanup.bind( null, name, win );
3230 promises.push( this.closeWindow( name ).then( cleanupWindow, cleanupWindow ) );
3231 }
3232
3233 return $.when.apply( $, promises );
3234 };
3235
3236 /**
3237 * Remove all windows from the window manager.
3238 *
3239 * Windows will be closed before they are removed. Note that the window manager, though not in use, will still
3240 * listen to events. If the window manager will not be used again, you may wish to use the #destroy method instead.
3241 * To remove just a subset of windows, use the #removeWindows method.
3242 *
3243 * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
3244 */
3245 OO.ui.WindowManager.prototype.clearWindows = function () {
3246 return this.removeWindows( Object.keys( this.windows ) );
3247 };
3248
3249 /**
3250 * Set dialog size. In general, this method should not be called directly.
3251 *
3252 * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
3253 *
3254 * @chainable
3255 */
3256 OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) {
3257 // Bypass for non-current, and thus invisible, windows
3258 if ( win !== this.currentWindow ) {
3259 return;
3260 }
3261
3262 var viewport = OO.ui.Element.static.getDimensions( win.getElementWindow() ),
3263 sizes = this.constructor.static.sizes,
3264 size = win.getSize();
3265
3266 if ( !sizes[ size ] ) {
3267 size = this.constructor.static.defaultSize;
3268 }
3269 if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[ size ].width ) {
3270 size = 'full';
3271 }
3272
3273 this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', size === 'full' );
3274 this.$element.toggleClass( 'oo-ui-windowManager-floating', size !== 'full' );
3275 win.setDimensions( sizes[ size ] );
3276
3277 this.emit( 'resize', win );
3278
3279 return this;
3280 };
3281
3282 /**
3283 * Bind or unbind global events for scrolling.
3284 *
3285 * @private
3286 * @param {boolean} [on] Bind global events
3287 * @chainable
3288 */
3289 OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) {
3290 on = on === undefined ? !!this.globalEvents : !!on;
3291
3292 var scrollWidth, bodyMargin,
3293 $body = $( this.getElementDocument().body ),
3294 // We could have multiple window managers open so only modify
3295 // the body css at the bottom of the stack
3296 stackDepth = $body.data( 'windowManagerGlobalEvents' ) || 0 ;
3297
3298 if ( on ) {
3299 if ( !this.globalEvents ) {
3300 $( this.getElementWindow() ).on( {
3301 // Start listening for top-level window dimension changes
3302 'orientationchange resize': this.onWindowResizeHandler
3303 } );
3304 if ( stackDepth === 0 ) {
3305 scrollWidth = window.innerWidth - document.documentElement.clientWidth;
3306 bodyMargin = parseFloat( $body.css( 'margin-right' ) ) || 0;
3307 $body.css( {
3308 overflow: 'hidden',
3309 'margin-right': bodyMargin + scrollWidth
3310 } );
3311 }
3312 stackDepth++;
3313 this.globalEvents = true;
3314 }
3315 } else if ( this.globalEvents ) {
3316 $( this.getElementWindow() ).off( {
3317 // Stop listening for top-level window dimension changes
3318 'orientationchange resize': this.onWindowResizeHandler
3319 } );
3320 stackDepth--;
3321 if ( stackDepth === 0 ) {
3322 $body.css( {
3323 overflow: '',
3324 'margin-right': ''
3325 } );
3326 }
3327 this.globalEvents = false;
3328 }
3329 $body.data( 'windowManagerGlobalEvents', stackDepth );
3330
3331 return this;
3332 };
3333
3334 /**
3335 * Toggle screen reader visibility of content other than the window manager.
3336 *
3337 * @private
3338 * @param {boolean} [isolate] Make only the window manager visible to screen readers
3339 * @chainable
3340 */
3341 OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) {
3342 isolate = isolate === undefined ? !this.$ariaHidden : !!isolate;
3343
3344 if ( isolate ) {
3345 if ( !this.$ariaHidden ) {
3346 // Hide everything other than the window manager from screen readers
3347 this.$ariaHidden = $( 'body' )
3348 .children()
3349 .not( this.$element.parentsUntil( 'body' ).last() )
3350 .attr( 'aria-hidden', '' );
3351 }
3352 } else if ( this.$ariaHidden ) {
3353 // Restore screen reader visibility
3354 this.$ariaHidden.removeAttr( 'aria-hidden' );
3355 this.$ariaHidden = null;
3356 }
3357
3358 return this;
3359 };
3360
3361 /**
3362 * Destroy the window manager.
3363 *
3364 * Destroying the window manager ensures that it will no longer listen to events. If you would like to
3365 * continue using the window manager, but wish to remove all windows from it, use the #clearWindows method
3366 * instead.
3367 */
3368 OO.ui.WindowManager.prototype.destroy = function () {
3369 this.toggleGlobalEvents( false );
3370 this.toggleAriaIsolation( false );
3371 this.clearWindows();
3372 this.$element.remove();
3373 };
3374
3375 /**
3376 * Errors contain a required message (either a string or jQuery selection) that is used to describe what went wrong
3377 * in a {@link OO.ui.Process process}. The error's #recoverable and #warning configurations are used to customize the
3378 * appearance and functionality of the error interface.
3379 *
3380 * The basic error interface contains a formatted error message as well as two buttons: 'Dismiss' and 'Try again' (i.e., the error
3381 * is 'recoverable' by default). If the error is not recoverable, the 'Try again' button will not be rendered and the widget
3382 * that initiated the failed process will be disabled.
3383 *
3384 * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button, which will try the
3385 * process again.
3386 *
3387 * For an example of error interfaces, please see the [OOjs UI documentation on MediaWiki][1].
3388 *
3389 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Processes_and_errors
3390 *
3391 * @class
3392 *
3393 * @constructor
3394 * @param {string|jQuery} message Description of error
3395 * @param {Object} [config] Configuration options
3396 * @cfg {boolean} [recoverable=true] Error is recoverable.
3397 * By default, errors are recoverable, and users can try the process again.
3398 * @cfg {boolean} [warning=false] Error is a warning.
3399 * If the error is a warning, the error interface will include a
3400 * 'Dismiss' and a 'Continue' button. It is the responsibility of the developer to ensure that the warning
3401 * is not triggered a second time if the user chooses to continue.
3402 */
3403 OO.ui.Error = function OoUiError( message, config ) {
3404 // Allow passing positional parameters inside the config object
3405 if ( OO.isPlainObject( message ) && config === undefined ) {
3406 config = message;
3407 message = config.message;
3408 }
3409
3410 // Configuration initialization
3411 config = config || {};
3412
3413 // Properties
3414 this.message = message instanceof jQuery ? message : String( message );
3415 this.recoverable = config.recoverable === undefined || !!config.recoverable;
3416 this.warning = !!config.warning;
3417 };
3418
3419 /* Setup */
3420
3421 OO.initClass( OO.ui.Error );
3422
3423 /* Methods */
3424
3425 /**
3426 * Check if the error is recoverable.
3427 *
3428 * If the error is recoverable, users are able to try the process again.
3429 *
3430 * @return {boolean} Error is recoverable
3431 */
3432 OO.ui.Error.prototype.isRecoverable = function () {
3433 return this.recoverable;
3434 };
3435
3436 /**
3437 * Check if the error is a warning.
3438 *
3439 * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button.
3440 *
3441 * @return {boolean} Error is warning
3442 */
3443 OO.ui.Error.prototype.isWarning = function () {
3444 return this.warning;
3445 };
3446
3447 /**
3448 * Get error message as DOM nodes.
3449 *
3450 * @return {jQuery} Error message in DOM nodes
3451 */
3452 OO.ui.Error.prototype.getMessage = function () {
3453 return this.message instanceof jQuery ?
3454 this.message.clone() :
3455 $( '<div>' ).text( this.message ).contents();
3456 };
3457
3458 /**
3459 * Get the error message text.
3460 *
3461 * @return {string} Error message
3462 */
3463 OO.ui.Error.prototype.getMessageText = function () {
3464 return this.message instanceof jQuery ? this.message.text() : this.message;
3465 };
3466
3467 /**
3468 * Wraps an HTML snippet for use with configuration values which default
3469 * to strings. This bypasses the default html-escaping done to string
3470 * values.
3471 *
3472 * @class
3473 *
3474 * @constructor
3475 * @param {string} [content] HTML content
3476 */
3477 OO.ui.HtmlSnippet = function OoUiHtmlSnippet( content ) {
3478 // Properties
3479 this.content = content;
3480 };
3481
3482 /* Setup */
3483
3484 OO.initClass( OO.ui.HtmlSnippet );
3485
3486 /* Methods */
3487
3488 /**
3489 * Render into HTML.
3490 *
3491 * @return {string} Unchanged HTML snippet.
3492 */
3493 OO.ui.HtmlSnippet.prototype.toString = function () {
3494 return this.content;
3495 };
3496
3497 /**
3498 * A Process is a list of steps that are called in sequence. The step can be a number, a jQuery promise,
3499 * or a function:
3500 *
3501 * - **number**: the process will wait for the specified number of milliseconds before proceeding.
3502 * - **promise**: the process will continue to the next step when the promise is successfully resolved
3503 * or stop if the promise is rejected.
3504 * - **function**: the process will execute the function. The process will stop if the function returns
3505 * either a boolean `false` or a promise that is rejected; if the function returns a number, the process
3506 * will wait for that number of milliseconds before proceeding.
3507 *
3508 * If the process fails, an {@link OO.ui.Error error} is generated. Depending on how the error is
3509 * configured, users can dismiss the error and try the process again, or not. If a process is stopped,
3510 * its remaining steps will not be performed.
3511 *
3512 * @class
3513 *
3514 * @constructor
3515 * @param {number|jQuery.Promise|Function} step Number of miliseconds to wait before proceeding, promise
3516 * that must be resolved before proceeding, or a function to execute. See #createStep for more information. see #createStep for more information
3517 * @param {Object} [context=null] Execution context of the function. The context is ignored if the step is
3518 * a number or promise.
3519 * @return {Object} Step object, with `callback` and `context` properties
3520 */
3521 OO.ui.Process = function ( step, context ) {
3522 // Properties
3523 this.steps = [];
3524
3525 // Initialization
3526 if ( step !== undefined ) {
3527 this.next( step, context );
3528 }
3529 };
3530
3531 /* Setup */
3532
3533 OO.initClass( OO.ui.Process );
3534
3535 /* Methods */
3536
3537 /**
3538 * Start the process.
3539 *
3540 * @return {jQuery.Promise} Promise that is resolved when all steps have successfully completed.
3541 * If any of the steps return a promise that is rejected or a boolean false, this promise is rejected
3542 * and any remaining steps are not performed.
3543 */
3544 OO.ui.Process.prototype.execute = function () {
3545 var i, len, promise;
3546
3547 /**
3548 * Continue execution.
3549 *
3550 * @ignore
3551 * @param {Array} step A function and the context it should be called in
3552 * @return {Function} Function that continues the process
3553 */
3554 function proceed( step ) {
3555 return function () {
3556 // Execute step in the correct context
3557 var deferred,
3558 result = step.callback.call( step.context );
3559
3560 if ( result === false ) {
3561 // Use rejected promise for boolean false results
3562 return $.Deferred().reject( [] ).promise();
3563 }
3564 if ( typeof result === 'number' ) {
3565 if ( result < 0 ) {
3566 throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
3567 }
3568 // Use a delayed promise for numbers, expecting them to be in milliseconds
3569 deferred = $.Deferred();
3570 setTimeout( deferred.resolve, result );
3571 return deferred.promise();
3572 }
3573 if ( result instanceof OO.ui.Error ) {
3574 // Use rejected promise for error
3575 return $.Deferred().reject( [ result ] ).promise();
3576 }
3577 if ( Array.isArray( result ) && result.length && result[ 0 ] instanceof OO.ui.Error ) {
3578 // Use rejected promise for list of errors
3579 return $.Deferred().reject( result ).promise();
3580 }
3581 // Duck-type the object to see if it can produce a promise
3582 if ( result && $.isFunction( result.promise ) ) {
3583 // Use a promise generated from the result
3584 return result.promise();
3585 }
3586 // Use resolved promise for other results
3587 return $.Deferred().resolve().promise();
3588 };
3589 }
3590
3591 if ( this.steps.length ) {
3592 // Generate a chain reaction of promises
3593 promise = proceed( this.steps[ 0 ] )();
3594 for ( i = 1, len = this.steps.length; i < len; i++ ) {
3595 promise = promise.then( proceed( this.steps[ i ] ) );
3596 }
3597 } else {
3598 promise = $.Deferred().resolve().promise();
3599 }
3600
3601 return promise;
3602 };
3603
3604 /**
3605 * Create a process step.
3606 *
3607 * @private
3608 * @param {number|jQuery.Promise|Function} step
3609 *
3610 * - Number of milliseconds to wait before proceeding
3611 * - Promise that must be resolved before proceeding
3612 * - Function to execute
3613 * - If the function returns a boolean false the process will stop
3614 * - If the function returns a promise, the process will continue to the next
3615 * step when the promise is resolved or stop if the promise is rejected
3616 * - If the function returns a number, the process will wait for that number of
3617 * milliseconds before proceeding
3618 * @param {Object} [context=null] Execution context of the function. The context is
3619 * ignored if the step is a number or promise.
3620 * @return {Object} Step object, with `callback` and `context` properties
3621 */
3622 OO.ui.Process.prototype.createStep = function ( step, context ) {
3623 if ( typeof step === 'number' || $.isFunction( step.promise ) ) {
3624 return {
3625 callback: function () {
3626 return step;
3627 },
3628 context: null
3629 };
3630 }
3631 if ( $.isFunction( step ) ) {
3632 return {
3633 callback: step,
3634 context: context
3635 };
3636 }
3637 throw new Error( 'Cannot create process step: number, promise or function expected' );
3638 };
3639
3640 /**
3641 * Add step to the beginning of the process.
3642 *
3643 * @inheritdoc #createStep
3644 * @return {OO.ui.Process} this
3645 * @chainable
3646 */
3647 OO.ui.Process.prototype.first = function ( step, context ) {
3648 this.steps.unshift( this.createStep( step, context ) );
3649 return this;
3650 };
3651
3652 /**
3653 * Add step to the end of the process.
3654 *
3655 * @inheritdoc #createStep
3656 * @return {OO.ui.Process} this
3657 * @chainable
3658 */
3659 OO.ui.Process.prototype.next = function ( step, context ) {
3660 this.steps.push( this.createStep( step, context ) );
3661 return this;
3662 };
3663
3664 /**
3665 * Factory for tools.
3666 *
3667 * @class
3668 * @extends OO.Factory
3669 * @constructor
3670 */
3671 OO.ui.ToolFactory = function OoUiToolFactory() {
3672 // Parent constructor
3673 OO.ui.ToolFactory.super.call( this );
3674 };
3675
3676 /* Setup */
3677
3678 OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
3679
3680 /* Methods */
3681
3682 /**
3683 * Get tools from the factory
3684 *
3685 * @param {Array} include Included tools
3686 * @param {Array} exclude Excluded tools
3687 * @param {Array} promote Promoted tools
3688 * @param {Array} demote Demoted tools
3689 * @return {string[]} List of tools
3690 */
3691 OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
3692 var i, len, included, promoted, demoted,
3693 auto = [],
3694 used = {};
3695
3696 // Collect included and not excluded tools
3697 included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
3698
3699 // Promotion
3700 promoted = this.extract( promote, used );
3701 demoted = this.extract( demote, used );
3702
3703 // Auto
3704 for ( i = 0, len = included.length; i < len; i++ ) {
3705 if ( !used[ included[ i ] ] ) {
3706 auto.push( included[ i ] );
3707 }
3708 }
3709
3710 return promoted.concat( auto ).concat( demoted );
3711 };
3712
3713 /**
3714 * Get a flat list of names from a list of names or groups.
3715 *
3716 * Tools can be specified in the following ways:
3717 *
3718 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
3719 * - All tools in a group: `{ group: 'group-name' }`
3720 * - All tools: `'*'`
3721 *
3722 * @private
3723 * @param {Array|string} collection List of tools
3724 * @param {Object} [used] Object with names that should be skipped as properties; extracted
3725 * names will be added as properties
3726 * @return {string[]} List of extracted names
3727 */
3728 OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
3729 var i, len, item, name, tool,
3730 names = [];
3731
3732 if ( collection === '*' ) {
3733 for ( name in this.registry ) {
3734 tool = this.registry[ name ];
3735 if (
3736 // Only add tools by group name when auto-add is enabled
3737 tool.static.autoAddToCatchall &&
3738 // Exclude already used tools
3739 ( !used || !used[ name ] )
3740 ) {
3741 names.push( name );
3742 if ( used ) {
3743 used[ name ] = true;
3744 }
3745 }
3746 }
3747 } else if ( Array.isArray( collection ) ) {
3748 for ( i = 0, len = collection.length; i < len; i++ ) {
3749 item = collection[ i ];
3750 // Allow plain strings as shorthand for named tools
3751 if ( typeof item === 'string' ) {
3752 item = { name: item };
3753 }
3754 if ( OO.isPlainObject( item ) ) {
3755 if ( item.group ) {
3756 for ( name in this.registry ) {
3757 tool = this.registry[ name ];
3758 if (
3759 // Include tools with matching group
3760 tool.static.group === item.group &&
3761 // Only add tools by group name when auto-add is enabled
3762 tool.static.autoAddToGroup &&
3763 // Exclude already used tools
3764 ( !used || !used[ name ] )
3765 ) {
3766 names.push( name );
3767 if ( used ) {
3768 used[ name ] = true;
3769 }
3770 }
3771 }
3772 // Include tools with matching name and exclude already used tools
3773 } else if ( item.name && ( !used || !used[ item.name ] ) ) {
3774 names.push( item.name );
3775 if ( used ) {
3776 used[ item.name ] = true;
3777 }
3778 }
3779 }
3780 }
3781 }
3782 return names;
3783 };
3784
3785 /**
3786 * Factory for tool groups.
3787 *
3788 * @class
3789 * @extends OO.Factory
3790 * @constructor
3791 */
3792 OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() {
3793 // Parent constructor
3794 OO.Factory.call( this );
3795
3796 var i, l,
3797 defaultClasses = this.constructor.static.getDefaultClasses();
3798
3799 // Register default toolgroups
3800 for ( i = 0, l = defaultClasses.length; i < l; i++ ) {
3801 this.register( defaultClasses[ i ] );
3802 }
3803 };
3804
3805 /* Setup */
3806
3807 OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory );
3808
3809 /* Static Methods */
3810
3811 /**
3812 * Get a default set of classes to be registered on construction
3813 *
3814 * @return {Function[]} Default classes
3815 */
3816 OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
3817 return [
3818 OO.ui.BarToolGroup,
3819 OO.ui.ListToolGroup,
3820 OO.ui.MenuToolGroup
3821 ];
3822 };
3823
3824 /**
3825 * Theme logic.
3826 *
3827 * @abstract
3828 * @class
3829 *
3830 * @constructor
3831 * @param {Object} [config] Configuration options
3832 */
3833 OO.ui.Theme = function OoUiTheme( config ) {
3834 // Configuration initialization
3835 config = config || {};
3836 };
3837
3838 /* Setup */
3839
3840 OO.initClass( OO.ui.Theme );
3841
3842 /* Methods */
3843
3844 /**
3845 * Get a list of classes to be applied to a widget.
3846 *
3847 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
3848 * otherwise state transitions will not work properly.
3849 *
3850 * @param {OO.ui.Element} element Element for which to get classes
3851 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
3852 */
3853 OO.ui.Theme.prototype.getElementClasses = function ( /* element */ ) {
3854 return { on: [], off: [] };
3855 };
3856
3857 /**
3858 * Update CSS classes provided by the theme.
3859 *
3860 * For elements with theme logic hooks, this should be called any time there's a state change.
3861 *
3862 * @param {OO.ui.Element} element Element for which to update classes
3863 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
3864 */
3865 OO.ui.Theme.prototype.updateElementClasses = function ( element ) {
3866 var classes = this.getElementClasses( element );
3867
3868 element.$element
3869 .removeClass( classes.off.join( ' ' ) )
3870 .addClass( classes.on.join( ' ' ) );
3871 };
3872
3873 /**
3874 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
3875 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
3876 * order in which users will navigate through the focusable elements via the "tab" key.
3877 *
3878 * @example
3879 * // TabIndexedElement is mixed into the ButtonWidget class
3880 * // to provide a tabIndex property.
3881 * var button1 = new OO.ui.ButtonWidget( {
3882 * label: 'fourth',
3883 * tabIndex: 4
3884 * } );
3885 * var button2 = new OO.ui.ButtonWidget( {
3886 * label: 'second',
3887 * tabIndex: 2
3888 * } );
3889 * var button3 = new OO.ui.ButtonWidget( {
3890 * label: 'third',
3891 * tabIndex: 3
3892 * } );
3893 * var button4 = new OO.ui.ButtonWidget( {
3894 * label: 'first',
3895 * tabIndex: 1
3896 * } );
3897 * $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
3898 *
3899 * @abstract
3900 * @class
3901 *
3902 * @constructor
3903 * @param {Object} [config] Configuration options
3904 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
3905 * the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
3906 * functionality will be applied to it instead.
3907 * @cfg {number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
3908 * order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
3909 * to remove the element from the tab-navigation flow.
3910 */
3911 OO.ui.TabIndexedElement = function OoUiTabIndexedElement( config ) {
3912 // Configuration initialization
3913 config = $.extend( { tabIndex: 0 }, config );
3914
3915 // Properties
3916 this.$tabIndexed = null;
3917 this.tabIndex = null;
3918
3919 // Events
3920 this.connect( this, { disable: 'onDisable' } );
3921
3922 // Initialization
3923 this.setTabIndex( config.tabIndex );
3924 this.setTabIndexedElement( config.$tabIndexed || this.$element );
3925 };
3926
3927 /* Setup */
3928
3929 OO.initClass( OO.ui.TabIndexedElement );
3930
3931 /* Methods */
3932
3933 /**
3934 * Set the element that should use the tabindex functionality.
3935 *
3936 * This method is used to retarget a tabindex mixin so that its functionality applies
3937 * to the specified element. If an element is currently using the functionality, the mixin’s
3938 * effect on that element is removed before the new element is set up.
3939 *
3940 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
3941 * @chainable
3942 */
3943 OO.ui.TabIndexedElement.prototype.setTabIndexedElement = function ( $tabIndexed ) {
3944 var tabIndex = this.tabIndex;
3945 // Remove attributes from old $tabIndexed
3946 this.setTabIndex( null );
3947 // Force update of new $tabIndexed
3948 this.$tabIndexed = $tabIndexed;
3949 this.tabIndex = tabIndex;
3950 return this.updateTabIndex();
3951 };
3952
3953 /**
3954 * Set the value of the tabindex.
3955 *
3956 * @param {number|null} tabIndex Tabindex value, or `null` for no tabindex
3957 * @chainable
3958 */
3959 OO.ui.TabIndexedElement.prototype.setTabIndex = function ( tabIndex ) {
3960 tabIndex = typeof tabIndex === 'number' ? tabIndex : null;
3961
3962 if ( this.tabIndex !== tabIndex ) {
3963 this.tabIndex = tabIndex;
3964 this.updateTabIndex();
3965 }
3966
3967 return this;
3968 };
3969
3970 /**
3971 * Update the `tabindex` attribute, in case of changes to tab index or
3972 * disabled state.
3973 *
3974 * @private
3975 * @chainable
3976 */
3977 OO.ui.TabIndexedElement.prototype.updateTabIndex = function () {
3978 if ( this.$tabIndexed ) {
3979 if ( this.tabIndex !== null ) {
3980 // Do not index over disabled elements
3981 this.$tabIndexed.attr( {
3982 tabindex: this.isDisabled() ? -1 : this.tabIndex,
3983 // ChromeVox and NVDA do not seem to inherit this from parent elements
3984 'aria-disabled': this.isDisabled().toString()
3985 } );
3986 } else {
3987 this.$tabIndexed.removeAttr( 'tabindex aria-disabled' );
3988 }
3989 }
3990 return this;
3991 };
3992
3993 /**
3994 * Handle disable events.
3995 *
3996 * @private
3997 * @param {boolean} disabled Element is disabled
3998 */
3999 OO.ui.TabIndexedElement.prototype.onDisable = function () {
4000 this.updateTabIndex();
4001 };
4002
4003 /**
4004 * Get the value of the tabindex.
4005 *
4006 * @return {number|null} Tabindex value
4007 */
4008 OO.ui.TabIndexedElement.prototype.getTabIndex = function () {
4009 return this.tabIndex;
4010 };
4011
4012 /**
4013 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
4014 * interface element that can be configured with access keys for accessibility.
4015 * See the [OOjs UI documentation on MediaWiki] [1] for examples.
4016 *
4017 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
4018 * @abstract
4019 * @class
4020 *
4021 * @constructor
4022 * @param {Object} [config] Configuration options
4023 * @cfg {jQuery} [$button] The button element created by the class.
4024 * If this configuration is omitted, the button element will use a generated `<a>`.
4025 * @cfg {boolean} [framed=true] Render the button with a frame
4026 * @cfg {string} [accessKey] Button's access key
4027 */
4028 OO.ui.ButtonElement = function OoUiButtonElement( config ) {
4029 // Configuration initialization
4030 config = config || {};
4031
4032 // Properties
4033 this.$button = null;
4034 this.framed = null;
4035 this.accessKey = null;
4036 this.active = false;
4037 this.onMouseUpHandler = this.onMouseUp.bind( this );
4038 this.onMouseDownHandler = this.onMouseDown.bind( this );
4039 this.onKeyDownHandler = this.onKeyDown.bind( this );
4040 this.onKeyUpHandler = this.onKeyUp.bind( this );
4041 this.onClickHandler = this.onClick.bind( this );
4042 this.onKeyPressHandler = this.onKeyPress.bind( this );
4043
4044 // Initialization
4045 this.$element.addClass( 'oo-ui-buttonElement' );
4046 this.toggleFramed( config.framed === undefined || config.framed );
4047 this.setAccessKey( config.accessKey );
4048 this.setButtonElement( config.$button || $( '<a>' ) );
4049 };
4050
4051 /* Setup */
4052
4053 OO.initClass( OO.ui.ButtonElement );
4054
4055 /* Static Properties */
4056
4057 /**
4058 * Cancel mouse down events.
4059 *
4060 * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
4061 * Classes such as {@link OO.ui.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
4062 * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
4063 * parent widget.
4064 *
4065 * @static
4066 * @inheritable
4067 * @property {boolean}
4068 */
4069 OO.ui.ButtonElement.static.cancelButtonMouseDownEvents = true;
4070
4071 /* Events */
4072
4073 /**
4074 * A 'click' event is emitted when the button element is clicked.
4075 *
4076 * @event click
4077 */
4078
4079 /* Methods */
4080
4081 /**
4082 * Set the button element.
4083 *
4084 * This method is used to retarget a button mixin so that its functionality applies to
4085 * the specified button element instead of the one created by the class. If a button element
4086 * is already set, the method will remove the mixin’s effect on that element.
4087 *
4088 * @param {jQuery} $button Element to use as button
4089 */
4090 OO.ui.ButtonElement.prototype.setButtonElement = function ( $button ) {
4091 if ( this.$button ) {
4092 this.$button
4093 .removeClass( 'oo-ui-buttonElement-button' )
4094 .removeAttr( 'role accesskey' )
4095 .off( {
4096 mousedown: this.onMouseDownHandler,
4097 keydown: this.onKeyDownHandler,
4098 click: this.onClickHandler,
4099 keypress: this.onKeyPressHandler
4100 } );
4101 }
4102
4103 this.$button = $button
4104 .addClass( 'oo-ui-buttonElement-button' )
4105 .attr( { role: 'button', accesskey: this.accessKey } )
4106 .on( {
4107 mousedown: this.onMouseDownHandler,
4108 keydown: this.onKeyDownHandler,
4109 click: this.onClickHandler,
4110 keypress: this.onKeyPressHandler
4111 } );
4112 };
4113
4114 /**
4115 * Handles mouse down events.
4116 *
4117 * @protected
4118 * @param {jQuery.Event} e Mouse down event
4119 */
4120 OO.ui.ButtonElement.prototype.onMouseDown = function ( e ) {
4121 if ( this.isDisabled() || e.which !== 1 ) {
4122 return;
4123 }
4124 this.$element.addClass( 'oo-ui-buttonElement-pressed' );
4125 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
4126 // reliably remove the pressed class
4127 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
4128 // Prevent change of focus unless specifically configured otherwise
4129 if ( this.constructor.static.cancelButtonMouseDownEvents ) {
4130 return false;
4131 }
4132 };
4133
4134 /**
4135 * Handles mouse up events.
4136 *
4137 * @protected
4138 * @param {jQuery.Event} e Mouse up event
4139 */
4140 OO.ui.ButtonElement.prototype.onMouseUp = function ( e ) {
4141 if ( this.isDisabled() || e.which !== 1 ) {
4142 return;
4143 }
4144 this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
4145 // Stop listening for mouseup, since we only needed this once
4146 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
4147 };
4148
4149 /**
4150 * Handles mouse click events.
4151 *
4152 * @protected
4153 * @param {jQuery.Event} e Mouse click event
4154 * @fires click
4155 */
4156 OO.ui.ButtonElement.prototype.onClick = function ( e ) {
4157 if ( !this.isDisabled() && e.which === 1 ) {
4158 this.emit( 'click' );
4159 }
4160 return false;
4161 };
4162
4163 /**
4164 * Handles key down events.
4165 *
4166 * @protected
4167 * @param {jQuery.Event} e Key down event
4168 */
4169 OO.ui.ButtonElement.prototype.onKeyDown = function ( e ) {
4170 if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
4171 return;
4172 }
4173 this.$element.addClass( 'oo-ui-buttonElement-pressed' );
4174 // Run the keyup handler no matter where the key is when the button is let go, so we can
4175 // reliably remove the pressed class
4176 this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler, true );
4177 };
4178
4179 /**
4180 * Handles key up events.
4181 *
4182 * @protected
4183 * @param {jQuery.Event} e Key up event
4184 */
4185 OO.ui.ButtonElement.prototype.onKeyUp = function ( e ) {
4186 if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
4187 return;
4188 }
4189 this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
4190 // Stop listening for keyup, since we only needed this once
4191 this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler, true );
4192 };
4193
4194 /**
4195 * Handles key press events.
4196 *
4197 * @protected
4198 * @param {jQuery.Event} e Key press event
4199 * @fires click
4200 */
4201 OO.ui.ButtonElement.prototype.onKeyPress = function ( e ) {
4202 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
4203 this.emit( 'click' );
4204 return false;
4205 }
4206 };
4207
4208 /**
4209 * Check if button has a frame.
4210 *
4211 * @return {boolean} Button is framed
4212 */
4213 OO.ui.ButtonElement.prototype.isFramed = function () {
4214 return this.framed;
4215 };
4216
4217 /**
4218 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
4219 *
4220 * @param {boolean} [framed] Make button framed, omit to toggle
4221 * @chainable
4222 */
4223 OO.ui.ButtonElement.prototype.toggleFramed = function ( framed ) {
4224 framed = framed === undefined ? !this.framed : !!framed;
4225 if ( framed !== this.framed ) {
4226 this.framed = framed;
4227 this.$element
4228 .toggleClass( 'oo-ui-buttonElement-frameless', !framed )
4229 .toggleClass( 'oo-ui-buttonElement-framed', framed );
4230 this.updateThemeClasses();
4231 }
4232
4233 return this;
4234 };
4235
4236 /**
4237 * Set the button's access key.
4238 *
4239 * @param {string} accessKey Button's access key, use empty string to remove
4240 * @chainable
4241 */
4242 OO.ui.ButtonElement.prototype.setAccessKey = function ( accessKey ) {
4243 accessKey = typeof accessKey === 'string' && accessKey.length ? accessKey : null;
4244
4245 if ( this.accessKey !== accessKey ) {
4246 if ( this.$button ) {
4247 if ( accessKey !== null ) {
4248 this.$button.attr( 'accesskey', accessKey );
4249 } else {
4250 this.$button.removeAttr( 'accesskey' );
4251 }
4252 }
4253 this.accessKey = accessKey;
4254 }
4255
4256 return this;
4257 };
4258
4259 /**
4260 * Set the button to its 'active' state.
4261 *
4262 * The active state occurs when a {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} or
4263 * a {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} is pressed. This method does nothing
4264 * for other button types.
4265 *
4266 * @param {boolean} [value] Make button active
4267 * @chainable
4268 */
4269 OO.ui.ButtonElement.prototype.setActive = function ( value ) {
4270 this.$element.toggleClass( 'oo-ui-buttonElement-active', !!value );
4271 return this;
4272 };
4273
4274 /**
4275 * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
4276 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
4277 * items from the group is done through the interface the class provides.
4278 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
4279 *
4280 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
4281 *
4282 * @abstract
4283 * @class
4284 *
4285 * @constructor
4286 * @param {Object} [config] Configuration options
4287 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
4288 * is omitted, the group element will use a generated `<div>`.
4289 */
4290 OO.ui.GroupElement = function OoUiGroupElement( config ) {
4291 // Configuration initialization
4292 config = config || {};
4293
4294 // Properties
4295 this.$group = null;
4296 this.items = [];
4297 this.aggregateItemEvents = {};
4298
4299 // Initialization
4300 this.setGroupElement( config.$group || $( '<div>' ) );
4301 };
4302
4303 /* Methods */
4304
4305 /**
4306 * Set the group element.
4307 *
4308 * If an element is already set, items will be moved to the new element.
4309 *
4310 * @param {jQuery} $group Element to use as group
4311 */
4312 OO.ui.GroupElement.prototype.setGroupElement = function ( $group ) {
4313 var i, len;
4314
4315 this.$group = $group;
4316 for ( i = 0, len = this.items.length; i < len; i++ ) {
4317 this.$group.append( this.items[ i ].$element );
4318 }
4319 };
4320
4321 /**
4322 * Check if a group contains no items.
4323 *
4324 * @return {boolean} Group is empty
4325 */
4326 OO.ui.GroupElement.prototype.isEmpty = function () {
4327 return !this.items.length;
4328 };
4329
4330 /**
4331 * Get all items in the group.
4332 *
4333 * The method returns an array of item references (e.g., [button1, button2, button3]) and is useful
4334 * when synchronizing groups of items, or whenever the references are required (e.g., when removing items
4335 * from a group).
4336 *
4337 * @return {OO.ui.Element[]} An array of items.
4338 */
4339 OO.ui.GroupElement.prototype.getItems = function () {
4340 return this.items.slice( 0 );
4341 };
4342
4343 /**
4344 * Get an item by its data.
4345 *
4346 * Only the first item with matching data will be returned. To return all matching items,
4347 * use the #getItemsFromData method.
4348 *
4349 * @param {Object} data Item data to search for
4350 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
4351 */
4352 OO.ui.GroupElement.prototype.getItemFromData = function ( data ) {
4353 var i, len, item,
4354 hash = OO.getHash( data );
4355
4356 for ( i = 0, len = this.items.length; i < len; i++ ) {
4357 item = this.items[ i ];
4358 if ( hash === OO.getHash( item.getData() ) ) {
4359 return item;
4360 }
4361 }
4362
4363 return null;
4364 };
4365
4366 /**
4367 * Get items by their data.
4368 *
4369 * All items with matching data will be returned. To return only the first match, use the #getItemFromData method instead.
4370 *
4371 * @param {Object} data Item data to search for
4372 * @return {OO.ui.Element[]} Items with equivalent data
4373 */
4374 OO.ui.GroupElement.prototype.getItemsFromData = function ( data ) {
4375 var i, len, item,
4376 hash = OO.getHash( data ),
4377 items = [];
4378
4379 for ( i = 0, len = this.items.length; i < len; i++ ) {
4380 item = this.items[ i ];
4381 if ( hash === OO.getHash( item.getData() ) ) {
4382 items.push( item );
4383 }
4384 }
4385
4386 return items;
4387 };
4388
4389 /**
4390 * Aggregate the events emitted by the group.
4391 *
4392 * When events are aggregated, the group will listen to all contained items for the event,
4393 * and then emit the event under a new name. The new event will contain an additional leading
4394 * parameter containing the item that emitted the original event. Other arguments emitted from
4395 * the original event are passed through.
4396 *
4397 * @param {Object.<string,string|null>} events An object keyed by the name of the event that should be
4398 * aggregated (e.g., ‘click’) and the value of the new name to use (e.g., ‘groupClick’).
4399 * A `null` value will remove aggregated events.
4400
4401 * @throws {Error} An error is thrown if aggregation already exists.
4402 */
4403 OO.ui.GroupElement.prototype.aggregate = function ( events ) {
4404 var i, len, item, add, remove, itemEvent, groupEvent;
4405
4406 for ( itemEvent in events ) {
4407 groupEvent = events[ itemEvent ];
4408
4409 // Remove existing aggregated event
4410 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4411 // Don't allow duplicate aggregations
4412 if ( groupEvent ) {
4413 throw new Error( 'Duplicate item event aggregation for ' + itemEvent );
4414 }
4415 // Remove event aggregation from existing items
4416 for ( i = 0, len = this.items.length; i < len; i++ ) {
4417 item = this.items[ i ];
4418 if ( item.connect && item.disconnect ) {
4419 remove = {};
4420 remove[ itemEvent ] = [ 'emit', groupEvent, item ];
4421 item.disconnect( this, remove );
4422 }
4423 }
4424 // Prevent future items from aggregating event
4425 delete this.aggregateItemEvents[ itemEvent ];
4426 }
4427
4428 // Add new aggregate event
4429 if ( groupEvent ) {
4430 // Make future items aggregate event
4431 this.aggregateItemEvents[ itemEvent ] = groupEvent;
4432 // Add event aggregation to existing items
4433 for ( i = 0, len = this.items.length; i < len; i++ ) {
4434 item = this.items[ i ];
4435 if ( item.connect && item.disconnect ) {
4436 add = {};
4437 add[ itemEvent ] = [ 'emit', groupEvent, item ];
4438 item.connect( this, add );
4439 }
4440 }
4441 }
4442 }
4443 };
4444
4445 /**
4446 * Add items to the group.
4447 *
4448 * Items will be added to the end of the group array unless the optional `index` parameter specifies
4449 * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
4450 *
4451 * @param {OO.ui.Element[]} items An array of items to add to the group
4452 * @param {number} [index] Index of the insertion point
4453 * @chainable
4454 */
4455 OO.ui.GroupElement.prototype.addItems = function ( items, index ) {
4456 var i, len, item, event, events, currentIndex,
4457 itemElements = [];
4458
4459 for ( i = 0, len = items.length; i < len; i++ ) {
4460 item = items[ i ];
4461
4462 // Check if item exists then remove it first, effectively "moving" it
4463 currentIndex = $.inArray( item, this.items );
4464 if ( currentIndex >= 0 ) {
4465 this.removeItems( [ item ] );
4466 // Adjust index to compensate for removal
4467 if ( currentIndex < index ) {
4468 index--;
4469 }
4470 }
4471 // Add the item
4472 if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
4473 events = {};
4474 for ( event in this.aggregateItemEvents ) {
4475 events[ event ] = [ 'emit', this.aggregateItemEvents[ event ], item ];
4476 }
4477 item.connect( this, events );
4478 }
4479 item.setElementGroup( this );
4480 itemElements.push( item.$element.get( 0 ) );
4481 }
4482
4483 if ( index === undefined || index < 0 || index >= this.items.length ) {
4484 this.$group.append( itemElements );
4485 this.items.push.apply( this.items, items );
4486 } else if ( index === 0 ) {
4487 this.$group.prepend( itemElements );
4488 this.items.unshift.apply( this.items, items );
4489 } else {
4490 this.items[ index ].$element.before( itemElements );
4491 this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
4492 }
4493
4494 return this;
4495 };
4496
4497 /**
4498 * Remove the specified items from a group.
4499 *
4500 * Removed items are detached (not removed) from the DOM so that they may be reused.
4501 * To remove all items from a group, you may wish to use the #clearItems method instead.
4502 *
4503 * @param {OO.ui.Element[]} items An array of items to remove
4504 * @chainable
4505 */
4506 OO.ui.GroupElement.prototype.removeItems = function ( items ) {
4507 var i, len, item, index, remove, itemEvent;
4508
4509 // Remove specific items
4510 for ( i = 0, len = items.length; i < len; i++ ) {
4511 item = items[ i ];
4512 index = $.inArray( item, this.items );
4513 if ( index !== -1 ) {
4514 if (
4515 item.connect && item.disconnect &&
4516 !$.isEmptyObject( this.aggregateItemEvents )
4517 ) {
4518 remove = {};
4519 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4520 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
4521 }
4522 item.disconnect( this, remove );
4523 }
4524 item.setElementGroup( null );
4525 this.items.splice( index, 1 );
4526 item.$element.detach();
4527 }
4528 }
4529
4530 return this;
4531 };
4532
4533 /**
4534 * Clear all items from the group.
4535 *
4536 * Cleared items are detached from the DOM, not removed, so that they may be reused.
4537 * To remove only a subset of items from a group, use the #removeItems method.
4538 *
4539 * @chainable
4540 */
4541 OO.ui.GroupElement.prototype.clearItems = function () {
4542 var i, len, item, remove, itemEvent;
4543
4544 // Remove all items
4545 for ( i = 0, len = this.items.length; i < len; i++ ) {
4546 item = this.items[ i ];
4547 if (
4548 item.connect && item.disconnect &&
4549 !$.isEmptyObject( this.aggregateItemEvents )
4550 ) {
4551 remove = {};
4552 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4553 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
4554 }
4555 item.disconnect( this, remove );
4556 }
4557 item.setElementGroup( null );
4558 item.$element.detach();
4559 }
4560
4561 this.items = [];
4562 return this;
4563 };
4564
4565 /**
4566 * DraggableElement is a mixin class used to create elements that can be clicked
4567 * and dragged by a mouse to a new position within a group. This class must be used
4568 * in conjunction with OO.ui.DraggableGroupElement, which provides a container for
4569 * the draggable elements.
4570 *
4571 * @abstract
4572 * @class
4573 *
4574 * @constructor
4575 */
4576 OO.ui.DraggableElement = function OoUiDraggableElement() {
4577 // Properties
4578 this.index = null;
4579
4580 // Initialize and events
4581 this.$element
4582 .attr( 'draggable', true )
4583 .addClass( 'oo-ui-draggableElement' )
4584 .on( {
4585 dragstart: this.onDragStart.bind( this ),
4586 dragover: this.onDragOver.bind( this ),
4587 dragend: this.onDragEnd.bind( this ),
4588 drop: this.onDrop.bind( this )
4589 } );
4590 };
4591
4592 OO.initClass( OO.ui.DraggableElement );
4593
4594 /* Events */
4595
4596 /**
4597 * @event dragstart
4598 *
4599 * A dragstart event is emitted when the user clicks and begins dragging an item.
4600 * @param {OO.ui.DraggableElement} item The item the user has clicked and is dragging with the mouse.
4601 */
4602
4603 /**
4604 * @event dragend
4605 * A dragend event is emitted when the user drags an item and releases the mouse,
4606 * thus terminating the drag operation.
4607 */
4608
4609 /**
4610 * @event drop
4611 * A drop event is emitted when the user drags an item and then releases the mouse button
4612 * over a valid target.
4613 */
4614
4615 /* Static Properties */
4616
4617 /**
4618 * @inheritdoc OO.ui.ButtonElement
4619 */
4620 OO.ui.DraggableElement.static.cancelButtonMouseDownEvents = false;
4621
4622 /* Methods */
4623
4624 /**
4625 * Respond to dragstart event.
4626 *
4627 * @private
4628 * @param {jQuery.Event} event jQuery event
4629 * @fires dragstart
4630 */
4631 OO.ui.DraggableElement.prototype.onDragStart = function ( e ) {
4632 var dataTransfer = e.originalEvent.dataTransfer;
4633 // Define drop effect
4634 dataTransfer.dropEffect = 'none';
4635 dataTransfer.effectAllowed = 'move';
4636 // We must set up a dataTransfer data property or Firefox seems to
4637 // ignore the fact the element is draggable.
4638 try {
4639 dataTransfer.setData( 'application-x/OOjs-UI-draggable', this.getIndex() );
4640 } catch ( err ) {
4641 // The above is only for firefox. No need to set a catch clause
4642 // if it fails, move on.
4643 }
4644 // Add dragging class
4645 this.$element.addClass( 'oo-ui-draggableElement-dragging' );
4646 // Emit event
4647 this.emit( 'dragstart', this );
4648 return true;
4649 };
4650
4651 /**
4652 * Respond to dragend event.
4653 *
4654 * @private
4655 * @fires dragend
4656 */
4657 OO.ui.DraggableElement.prototype.onDragEnd = function () {
4658 this.$element.removeClass( 'oo-ui-draggableElement-dragging' );
4659 this.emit( 'dragend' );
4660 };
4661
4662 /**
4663 * Handle drop event.
4664 *
4665 * @private
4666 * @param {jQuery.Event} event jQuery event
4667 * @fires drop
4668 */
4669 OO.ui.DraggableElement.prototype.onDrop = function ( e ) {
4670 e.preventDefault();
4671 this.emit( 'drop', e );
4672 };
4673
4674 /**
4675 * In order for drag/drop to work, the dragover event must
4676 * return false and stop propogation.
4677 *
4678 * @private
4679 */
4680 OO.ui.DraggableElement.prototype.onDragOver = function ( e ) {
4681 e.preventDefault();
4682 };
4683
4684 /**
4685 * Set item index.
4686 * Store it in the DOM so we can access from the widget drag event
4687 *
4688 * @private
4689 * @param {number} Item index
4690 */
4691 OO.ui.DraggableElement.prototype.setIndex = function ( index ) {
4692 if ( this.index !== index ) {
4693 this.index = index;
4694 this.$element.data( 'index', index );
4695 }
4696 };
4697
4698 /**
4699 * Get item index
4700 *
4701 * @private
4702 * @return {number} Item index
4703 */
4704 OO.ui.DraggableElement.prototype.getIndex = function () {
4705 return this.index;
4706 };
4707
4708 /**
4709 * DraggableGroupElement is a mixin class used to create a group element to
4710 * contain draggable elements, which are items that can be clicked and dragged by a mouse.
4711 * The class is used with OO.ui.DraggableElement.
4712 *
4713 * @abstract
4714 * @class
4715 * @mixins OO.ui.GroupElement
4716 *
4717 * @constructor
4718 * @param {Object} [config] Configuration options
4719 * @cfg {string} [orientation] Item orientation: 'horizontal' or 'vertical'. The orientation
4720 * should match the layout of the items. Items displayed in a single row
4721 * or in several rows should use horizontal orientation. The vertical orientation should only be
4722 * used when the items are displayed in a single column. Defaults to 'vertical'
4723 */
4724 OO.ui.DraggableGroupElement = function OoUiDraggableGroupElement( config ) {
4725 // Configuration initialization
4726 config = config || {};
4727
4728 // Parent constructor
4729 OO.ui.GroupElement.call( this, config );
4730
4731 // Properties
4732 this.orientation = config.orientation || 'vertical';
4733 this.dragItem = null;
4734 this.itemDragOver = null;
4735 this.itemKeys = {};
4736 this.sideInsertion = '';
4737
4738 // Events
4739 this.aggregate( {
4740 dragstart: 'itemDragStart',
4741 dragend: 'itemDragEnd',
4742 drop: 'itemDrop'
4743 } );
4744 this.connect( this, {
4745 itemDragStart: 'onItemDragStart',
4746 itemDrop: 'onItemDrop',
4747 itemDragEnd: 'onItemDragEnd'
4748 } );
4749 this.$element.on( {
4750 dragover: $.proxy( this.onDragOver, this ),
4751 dragleave: $.proxy( this.onDragLeave, this )
4752 } );
4753
4754 // Initialize
4755 if ( Array.isArray( config.items ) ) {
4756 this.addItems( config.items );
4757 }
4758 this.$placeholder = $( '<div>' )
4759 .addClass( 'oo-ui-draggableGroupElement-placeholder' );
4760 this.$element
4761 .addClass( 'oo-ui-draggableGroupElement' )
4762 .append( this.$status )
4763 .toggleClass( 'oo-ui-draggableGroupElement-horizontal', this.orientation === 'horizontal' )
4764 .prepend( this.$placeholder );
4765 };
4766
4767 /* Setup */
4768 OO.mixinClass( OO.ui.DraggableGroupElement, OO.ui.GroupElement );
4769
4770 /* Events */
4771
4772 /**
4773 * A 'reorder' event is emitted when the order of items in the group changes.
4774 *
4775 * @event reorder
4776 * @param {OO.ui.DraggableElement} item Reordered item
4777 * @param {number} [newIndex] New index for the item
4778 */
4779
4780 /* Methods */
4781
4782 /**
4783 * Respond to item drag start event
4784 *
4785 * @private
4786 * @param {OO.ui.DraggableElement} item Dragged item
4787 */
4788 OO.ui.DraggableGroupElement.prototype.onItemDragStart = function ( item ) {
4789 var i, len;
4790
4791 // Map the index of each object
4792 for ( i = 0, len = this.items.length; i < len; i++ ) {
4793 this.items[ i ].setIndex( i );
4794 }
4795
4796 if ( this.orientation === 'horizontal' ) {
4797 // Set the height of the indicator
4798 this.$placeholder.css( {
4799 height: item.$element.outerHeight(),
4800 width: 2
4801 } );
4802 } else {
4803 // Set the width of the indicator
4804 this.$placeholder.css( {
4805 height: 2,
4806 width: item.$element.outerWidth()
4807 } );
4808 }
4809 this.setDragItem( item );
4810 };
4811
4812 /**
4813 * Respond to item drag end event
4814 *
4815 * @private
4816 */
4817 OO.ui.DraggableGroupElement.prototype.onItemDragEnd = function () {
4818 this.unsetDragItem();
4819 return false;
4820 };
4821
4822 /**
4823 * Handle drop event and switch the order of the items accordingly
4824 *
4825 * @private
4826 * @param {OO.ui.DraggableElement} item Dropped item
4827 * @fires reorder
4828 */
4829 OO.ui.DraggableGroupElement.prototype.onItemDrop = function ( item ) {
4830 var toIndex = item.getIndex();
4831 // Check if the dropped item is from the current group
4832 // TODO: Figure out a way to configure a list of legally droppable
4833 // elements even if they are not yet in the list
4834 if ( this.getDragItem() ) {
4835 // If the insertion point is 'after', the insertion index
4836 // is shifted to the right (or to the left in RTL, hence 'after')
4837 if ( this.sideInsertion === 'after' ) {
4838 toIndex++;
4839 }
4840 // Emit change event
4841 this.emit( 'reorder', this.getDragItem(), toIndex );
4842 }
4843 this.unsetDragItem();
4844 // Return false to prevent propogation
4845 return false;
4846 };
4847
4848 /**
4849 * Handle dragleave event.
4850 *
4851 * @private
4852 */
4853 OO.ui.DraggableGroupElement.prototype.onDragLeave = function () {
4854 // This means the item was dragged outside the widget
4855 this.$placeholder
4856 .css( 'left', 0 )
4857 .addClass( 'oo-ui-element-hidden' );
4858 };
4859
4860 /**
4861 * Respond to dragover event
4862 *
4863 * @private
4864 * @param {jQuery.Event} event Event details
4865 */
4866 OO.ui.DraggableGroupElement.prototype.onDragOver = function ( e ) {
4867 var dragOverObj, $optionWidget, itemOffset, itemMidpoint, itemBoundingRect,
4868 itemSize, cssOutput, dragPosition, itemIndex, itemPosition,
4869 clientX = e.originalEvent.clientX,
4870 clientY = e.originalEvent.clientY;
4871
4872 // Get the OptionWidget item we are dragging over
4873 dragOverObj = this.getElementDocument().elementFromPoint( clientX, clientY );
4874 $optionWidget = $( dragOverObj ).closest( '.oo-ui-draggableElement' );
4875 if ( $optionWidget[ 0 ] ) {
4876 itemOffset = $optionWidget.offset();
4877 itemBoundingRect = $optionWidget[ 0 ].getBoundingClientRect();
4878 itemPosition = $optionWidget.position();
4879 itemIndex = $optionWidget.data( 'index' );
4880 }
4881
4882 if (
4883 itemOffset &&
4884 this.isDragging() &&
4885 itemIndex !== this.getDragItem().getIndex()
4886 ) {
4887 if ( this.orientation === 'horizontal' ) {
4888 // Calculate where the mouse is relative to the item width
4889 itemSize = itemBoundingRect.width;
4890 itemMidpoint = itemBoundingRect.left + itemSize / 2;
4891 dragPosition = clientX;
4892 // Which side of the item we hover over will dictate
4893 // where the placeholder will appear, on the left or
4894 // on the right
4895 cssOutput = {
4896 left: dragPosition < itemMidpoint ? itemPosition.left : itemPosition.left + itemSize,
4897 top: itemPosition.top
4898 };
4899 } else {
4900 // Calculate where the mouse is relative to the item height
4901 itemSize = itemBoundingRect.height;
4902 itemMidpoint = itemBoundingRect.top + itemSize / 2;
4903 dragPosition = clientY;
4904 // Which side of the item we hover over will dictate
4905 // where the placeholder will appear, on the top or
4906 // on the bottom
4907 cssOutput = {
4908 top: dragPosition < itemMidpoint ? itemPosition.top : itemPosition.top + itemSize,
4909 left: itemPosition.left
4910 };
4911 }
4912 // Store whether we are before or after an item to rearrange
4913 // For horizontal layout, we need to account for RTL, as this is flipped
4914 if ( this.orientation === 'horizontal' && this.$element.css( 'direction' ) === 'rtl' ) {
4915 this.sideInsertion = dragPosition < itemMidpoint ? 'after' : 'before';
4916 } else {
4917 this.sideInsertion = dragPosition < itemMidpoint ? 'before' : 'after';
4918 }
4919 // Add drop indicator between objects
4920 this.$placeholder
4921 .css( cssOutput )
4922 .removeClass( 'oo-ui-element-hidden' );
4923 } else {
4924 // This means the item was dragged outside the widget
4925 this.$placeholder
4926 .css( 'left', 0 )
4927 .addClass( 'oo-ui-element-hidden' );
4928 }
4929 // Prevent default
4930 e.preventDefault();
4931 };
4932
4933 /**
4934 * Set a dragged item
4935 *
4936 * @param {OO.ui.DraggableElement} item Dragged item
4937 */
4938 OO.ui.DraggableGroupElement.prototype.setDragItem = function ( item ) {
4939 this.dragItem = item;
4940 };
4941
4942 /**
4943 * Unset the current dragged item
4944 */
4945 OO.ui.DraggableGroupElement.prototype.unsetDragItem = function () {
4946 this.dragItem = null;
4947 this.itemDragOver = null;
4948 this.$placeholder.addClass( 'oo-ui-element-hidden' );
4949 this.sideInsertion = '';
4950 };
4951
4952 /**
4953 * Get the item that is currently being dragged.
4954 *
4955 * @return {OO.ui.DraggableElement|null} The currently dragged item, or `null` if no item is being dragged
4956 */
4957 OO.ui.DraggableGroupElement.prototype.getDragItem = function () {
4958 return this.dragItem;
4959 };
4960
4961 /**
4962 * Check if an item in the group is currently being dragged.
4963 *
4964 * @return {Boolean} Item is being dragged
4965 */
4966 OO.ui.DraggableGroupElement.prototype.isDragging = function () {
4967 return this.getDragItem() !== null;
4968 };
4969
4970 /**
4971 * IconElement is often mixed into other classes to generate an icon.
4972 * Icons are graphics, about the size of normal text. They are used to aid the user
4973 * in locating a control or to convey information in a space-efficient way. See the
4974 * [OOjs UI documentation on MediaWiki] [1] for a list of icons
4975 * included in the library.
4976 *
4977 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
4978 *
4979 * @abstract
4980 * @class
4981 *
4982 * @constructor
4983 * @param {Object} [config] Configuration options
4984 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
4985 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
4986 * the icon element be set to an existing icon instead of the one generated by this class, set a
4987 * value using a jQuery selection. For example:
4988 *
4989 * // Use a <div> tag instead of a <span>
4990 * $icon: $("<div>")
4991 * // Use an existing icon element instead of the one generated by the class
4992 * $icon: this.$element
4993 * // Use an icon element from a child widget
4994 * $icon: this.childwidget.$element
4995 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
4996 * symbolic names. A map is used for i18n purposes and contains a `default` icon
4997 * name and additional names keyed by language code. The `default` name is used when no icon is keyed
4998 * by the user's language.
4999 *
5000 * Example of an i18n map:
5001 *
5002 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
5003 * See the [OOjs UI documentation on MediaWiki] [2] for a list of icons included in the library.
5004 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
5005 * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
5006 * text. The icon title is displayed when users move the mouse over the icon.
5007 */
5008 OO.ui.IconElement = function OoUiIconElement( config ) {
5009 // Configuration initialization
5010 config = config || {};
5011
5012 // Properties
5013 this.$icon = null;
5014 this.icon = null;
5015 this.iconTitle = null;
5016
5017 // Initialization
5018 this.setIcon( config.icon || this.constructor.static.icon );
5019 this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle );
5020 this.setIconElement( config.$icon || $( '<span>' ) );
5021 };
5022
5023 /* Setup */
5024
5025 OO.initClass( OO.ui.IconElement );
5026
5027 /* Static Properties */
5028
5029 /**
5030 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
5031 * for i18n purposes and contains a `default` icon name and additional names keyed by
5032 * language code. The `default` name is used when no icon is keyed by the user's language.
5033 *
5034 * Example of an i18n map:
5035 *
5036 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
5037 *
5038 * Note: the static property will be overridden if the #icon configuration is used.
5039 *
5040 * @static
5041 * @inheritable
5042 * @property {Object|string}
5043 */
5044 OO.ui.IconElement.static.icon = null;
5045
5046 /**
5047 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
5048 * function that returns title text, or `null` for no title.
5049 *
5050 * The static property will be overridden if the #iconTitle configuration is used.
5051 *
5052 * @static
5053 * @inheritable
5054 * @property {string|Function|null}
5055 */
5056 OO.ui.IconElement.static.iconTitle = null;
5057
5058 /* Methods */
5059
5060 /**
5061 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
5062 * applies to the specified icon element instead of the one created by the class. If an icon
5063 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
5064 * and mixin methods will no longer affect the element.
5065 *
5066 * @param {jQuery} $icon Element to use as icon
5067 */
5068 OO.ui.IconElement.prototype.setIconElement = function ( $icon ) {
5069 if ( this.$icon ) {
5070 this.$icon
5071 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon )
5072 .removeAttr( 'title' );
5073 }
5074
5075 this.$icon = $icon
5076 .addClass( 'oo-ui-iconElement-icon' )
5077 .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon );
5078 if ( this.iconTitle !== null ) {
5079 this.$icon.attr( 'title', this.iconTitle );
5080 }
5081 };
5082
5083 /**
5084 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
5085 * The icon parameter can also be set to a map of icon names. See the #icon config setting
5086 * for an example.
5087 *
5088 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
5089 * by language code, or `null` to remove the icon.
5090 * @chainable
5091 */
5092 OO.ui.IconElement.prototype.setIcon = function ( icon ) {
5093 icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
5094 icon = typeof icon === 'string' && icon.trim().length ? icon.trim() : null;
5095
5096 if ( this.icon !== icon ) {
5097 if ( this.$icon ) {
5098 if ( this.icon !== null ) {
5099 this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
5100 }
5101 if ( icon !== null ) {
5102 this.$icon.addClass( 'oo-ui-icon-' + icon );
5103 }
5104 }
5105 this.icon = icon;
5106 }
5107
5108 this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon );
5109 this.updateThemeClasses();
5110
5111 return this;
5112 };
5113
5114 /**
5115 * Set the icon title. Use `null` to remove the title.
5116 *
5117 * @param {string|Function|null} iconTitle A text string used as the icon title,
5118 * a function that returns title text, or `null` for no title.
5119 * @chainable
5120 */
5121 OO.ui.IconElement.prototype.setIconTitle = function ( iconTitle ) {
5122 iconTitle = typeof iconTitle === 'function' ||
5123 ( typeof iconTitle === 'string' && iconTitle.length ) ?
5124 OO.ui.resolveMsg( iconTitle ) : null;
5125
5126 if ( this.iconTitle !== iconTitle ) {
5127 this.iconTitle = iconTitle;
5128 if ( this.$icon ) {
5129 if ( this.iconTitle !== null ) {
5130 this.$icon.attr( 'title', iconTitle );
5131 } else {
5132 this.$icon.removeAttr( 'title' );
5133 }
5134 }
5135 }
5136
5137 return this;
5138 };
5139
5140 /**
5141 * Get the symbolic name of the icon.
5142 *
5143 * @return {string} Icon name
5144 */
5145 OO.ui.IconElement.prototype.getIcon = function () {
5146 return this.icon;
5147 };
5148
5149 /**
5150 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
5151 *
5152 * @return {string} Icon title text
5153 */
5154 OO.ui.IconElement.prototype.getIconTitle = function () {
5155 return this.iconTitle;
5156 };
5157
5158 /**
5159 * IndicatorElement is often mixed into other classes to generate an indicator.
5160 * Indicators are small graphics that are generally used in two ways:
5161 *
5162 * - To draw attention to the status of an item. For example, an indicator might be
5163 * used to show that an item in a list has errors that need to be resolved.
5164 * - To clarify the function of a control that acts in an exceptional way (a button
5165 * that opens a menu instead of performing an action directly, for example).
5166 *
5167 * For a list of indicators included in the library, please see the
5168 * [OOjs UI documentation on MediaWiki] [1].
5169 *
5170 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
5171 *
5172 * @abstract
5173 * @class
5174 *
5175 * @constructor
5176 * @param {Object} [config] Configuration options
5177 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
5178 * configuration is omitted, the indicator element will use a generated `<span>`.
5179 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
5180 * See the [OOjs UI documentation on MediaWiki][2] for a list of indicators included
5181 * in the library.
5182 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
5183 * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
5184 * or a function that returns title text. The indicator title is displayed when users move
5185 * the mouse over the indicator.
5186 */
5187 OO.ui.IndicatorElement = function OoUiIndicatorElement( config ) {
5188 // Configuration initialization
5189 config = config || {};
5190
5191 // Properties
5192 this.$indicator = null;
5193 this.indicator = null;
5194 this.indicatorTitle = null;
5195
5196 // Initialization
5197 this.setIndicator( config.indicator || this.constructor.static.indicator );
5198 this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle );
5199 this.setIndicatorElement( config.$indicator || $( '<span>' ) );
5200 };
5201
5202 /* Setup */
5203
5204 OO.initClass( OO.ui.IndicatorElement );
5205
5206 /* Static Properties */
5207
5208 /**
5209 * Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
5210 * The static property will be overridden if the #indicator configuration is used.
5211 *
5212 * @static
5213 * @inheritable
5214 * @property {string|null}
5215 */
5216 OO.ui.IndicatorElement.static.indicator = null;
5217
5218 /**
5219 * A text string used as the indicator title, a function that returns title text, or `null`
5220 * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
5221 *
5222 * @static
5223 * @inheritable
5224 * @property {string|Function|null}
5225 */
5226 OO.ui.IndicatorElement.static.indicatorTitle = null;
5227
5228 /* Methods */
5229
5230 /**
5231 * Set the indicator element.
5232 *
5233 * If an element is already set, it will be cleaned up before setting up the new element.
5234 *
5235 * @param {jQuery} $indicator Element to use as indicator
5236 */
5237 OO.ui.IndicatorElement.prototype.setIndicatorElement = function ( $indicator ) {
5238 if ( this.$indicator ) {
5239 this.$indicator
5240 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator )
5241 .removeAttr( 'title' );
5242 }
5243
5244 this.$indicator = $indicator
5245 .addClass( 'oo-ui-indicatorElement-indicator' )
5246 .toggleClass( 'oo-ui-indicator-' + this.indicator, !!this.indicator );
5247 if ( this.indicatorTitle !== null ) {
5248 this.$indicator.attr( 'title', this.indicatorTitle );
5249 }
5250 };
5251
5252 /**
5253 * Set the indicator by its symbolic name: ‘alert’, ‘down’, ‘next’, ‘previous’, ‘required’, ‘up’. Use `null` to remove the indicator.
5254 *
5255 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
5256 * @chainable
5257 */
5258 OO.ui.IndicatorElement.prototype.setIndicator = function ( indicator ) {
5259 indicator = typeof indicator === 'string' && indicator.length ? indicator.trim() : null;
5260
5261 if ( this.indicator !== indicator ) {
5262 if ( this.$indicator ) {
5263 if ( this.indicator !== null ) {
5264 this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
5265 }
5266 if ( indicator !== null ) {
5267 this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
5268 }
5269 }
5270 this.indicator = indicator;
5271 }
5272
5273 this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator );
5274 this.updateThemeClasses();
5275
5276 return this;
5277 };
5278
5279 /**
5280 * Set the indicator title.
5281 *
5282 * The title is displayed when a user moves the mouse over the indicator.
5283 *
5284 * @param {string|Function|null} indicator Indicator title text, a function that returns text, or
5285 * `null` for no indicator title
5286 * @chainable
5287 */
5288 OO.ui.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
5289 indicatorTitle = typeof indicatorTitle === 'function' ||
5290 ( typeof indicatorTitle === 'string' && indicatorTitle.length ) ?
5291 OO.ui.resolveMsg( indicatorTitle ) : null;
5292
5293 if ( this.indicatorTitle !== indicatorTitle ) {
5294 this.indicatorTitle = indicatorTitle;
5295 if ( this.$indicator ) {
5296 if ( this.indicatorTitle !== null ) {
5297 this.$indicator.attr( 'title', indicatorTitle );
5298 } else {
5299 this.$indicator.removeAttr( 'title' );
5300 }
5301 }
5302 }
5303
5304 return this;
5305 };
5306
5307 /**
5308 * Get the symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
5309 *
5310 * @return {string} Symbolic name of indicator
5311 */
5312 OO.ui.IndicatorElement.prototype.getIndicator = function () {
5313 return this.indicator;
5314 };
5315
5316 /**
5317 * Get the indicator title.
5318 *
5319 * The title is displayed when a user moves the mouse over the indicator.
5320 *
5321 * @return {string} Indicator title text
5322 */
5323 OO.ui.IndicatorElement.prototype.getIndicatorTitle = function () {
5324 return this.indicatorTitle;
5325 };
5326
5327 /**
5328 * LabelElement is often mixed into other classes to generate a label, which
5329 * helps identify the function of an interface element.
5330 * See the [OOjs UI documentation on MediaWiki] [1] for more information.
5331 *
5332 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
5333 *
5334 * @abstract
5335 * @class
5336 *
5337 * @constructor
5338 * @param {Object} [config] Configuration options
5339 * @cfg {jQuery} [$label] The label element created by the class. If this
5340 * configuration is omitted, the label element will use a generated `<span>`.
5341 * @cfg {jQuery|string|Function} [label] The label text. The label can be specified as a plaintext string,
5342 * a jQuery selection of elements, or a function that will produce a string in the future. See the
5343 * [OOjs UI documentation on MediaWiki] [2] for examples.
5344 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
5345 * @cfg {boolean} [autoFitLabel=true] Fit the label to the width of the parent element.
5346 * The label will be truncated to fit if necessary.
5347 */
5348 OO.ui.LabelElement = function OoUiLabelElement( config ) {
5349 // Configuration initialization
5350 config = config || {};
5351
5352 // Properties
5353 this.$label = null;
5354 this.label = null;
5355 this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel;
5356
5357 // Initialization
5358 this.setLabel( config.label || this.constructor.static.label );
5359 this.setLabelElement( config.$label || $( '<span>' ) );
5360 };
5361
5362 /* Setup */
5363
5364 OO.initClass( OO.ui.LabelElement );
5365
5366 /* Events */
5367
5368 /**
5369 * @event labelChange
5370 * @param {string} value
5371 */
5372
5373 /* Static Properties */
5374
5375 /**
5376 * The label text. The label can be specified as a plaintext string, a function that will
5377 * produce a string in the future, or `null` for no label. The static value will
5378 * be overridden if a label is specified with the #label config option.
5379 *
5380 * @static
5381 * @inheritable
5382 * @property {string|Function|null}
5383 */
5384 OO.ui.LabelElement.static.label = null;
5385
5386 /* Methods */
5387
5388 /**
5389 * Set the label element.
5390 *
5391 * If an element is already set, it will be cleaned up before setting up the new element.
5392 *
5393 * @param {jQuery} $label Element to use as label
5394 */
5395 OO.ui.LabelElement.prototype.setLabelElement = function ( $label ) {
5396 if ( this.$label ) {
5397 this.$label.removeClass( 'oo-ui-labelElement-label' ).empty();
5398 }
5399
5400 this.$label = $label.addClass( 'oo-ui-labelElement-label' );
5401 this.setLabelContent( this.label );
5402 };
5403
5404 /**
5405 * Set the label.
5406 *
5407 * An empty string will result in the label being hidden. A string containing only whitespace will
5408 * be converted to a single `&nbsp;`.
5409 *
5410 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
5411 * text; or null for no label
5412 * @chainable
5413 */
5414 OO.ui.LabelElement.prototype.setLabel = function ( label ) {
5415 label = typeof label === 'function' ? OO.ui.resolveMsg( label ) : label;
5416 label = ( ( typeof label === 'string' && label.length ) || label instanceof jQuery || label instanceof OO.ui.HtmlSnippet ) ? label : null;
5417
5418 this.$element.toggleClass( 'oo-ui-labelElement', !!label );
5419
5420 if ( this.label !== label ) {
5421 if ( this.$label ) {
5422 this.setLabelContent( label );
5423 }
5424 this.label = label;
5425 this.emit( 'labelChange' );
5426 }
5427
5428 return this;
5429 };
5430
5431 /**
5432 * Get the label.
5433 *
5434 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
5435 * text; or null for no label
5436 */
5437 OO.ui.LabelElement.prototype.getLabel = function () {
5438 return this.label;
5439 };
5440
5441 /**
5442 * Fit the label.
5443 *
5444 * @chainable
5445 */
5446 OO.ui.LabelElement.prototype.fitLabel = function () {
5447 if ( this.$label && this.$label.autoEllipsis && this.autoFitLabel ) {
5448 this.$label.autoEllipsis( { hasSpan: false, tooltip: true } );
5449 }
5450
5451 return this;
5452 };
5453
5454 /**
5455 * Set the content of the label.
5456 *
5457 * Do not call this method until after the label element has been set by #setLabelElement.
5458 *
5459 * @private
5460 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
5461 * text; or null for no label
5462 */
5463 OO.ui.LabelElement.prototype.setLabelContent = function ( label ) {
5464 if ( typeof label === 'string' ) {
5465 if ( label.match( /^\s*$/ ) ) {
5466 // Convert whitespace only string to a single non-breaking space
5467 this.$label.html( '&nbsp;' );
5468 } else {
5469 this.$label.text( label );
5470 }
5471 } else if ( label instanceof OO.ui.HtmlSnippet ) {
5472 this.$label.html( label.toString() );
5473 } else if ( label instanceof jQuery ) {
5474 this.$label.empty().append( label );
5475 } else {
5476 this.$label.empty();
5477 }
5478 };
5479
5480 /**
5481 * LookupElement is a mixin that creates a {@link OO.ui.TextInputMenuSelectWidget menu} of suggested values for
5482 * a {@link OO.ui.TextInputWidget text input widget}. Suggested values are based on the characters the user types
5483 * into the text input field and, in general, the menu is only displayed when the user types. If a suggested value is chosen
5484 * from the lookup menu, that value becomes the value of the input field.
5485 *
5486 * Note that a new menu of suggested items is displayed when a value is chosen from the lookup menu. If this is
5487 * not the desired behavior, disable lookup menus with the #setLookupsDisabled method, then set the value, then
5488 * re-enable lookups.
5489 *
5490 * See the [OOjs UI demos][1] for an example.
5491 *
5492 * [1]: https://tools.wmflabs.org/oojs-ui/oojs-ui/demos/index.html#widgets-apex-vector-ltr
5493 *
5494 * @class
5495 * @abstract
5496 *
5497 * @constructor
5498 * @param {Object} [config] Configuration options
5499 * @cfg {jQuery} [$overlay] Overlay for the lookup menu; defaults to relative positioning
5500 * @cfg {jQuery} [$container=this.$element] The container element. The lookup menu is rendered beneath the specified element.
5501 * @cfg {boolean} [allowSuggestionsWhenEmpty=false] Request and display a lookup menu when the text input is empty.
5502 * By default, the lookup menu is not generated and displayed until the user begins to type.
5503 */
5504 OO.ui.LookupElement = function OoUiLookupElement( config ) {
5505 // Configuration initialization
5506 config = config || {};
5507
5508 // Properties
5509 this.$overlay = config.$overlay || this.$element;
5510 this.lookupMenu = new OO.ui.TextInputMenuSelectWidget( this, {
5511 widget: this,
5512 input: this,
5513 $container: config.$container
5514 } );
5515
5516 this.allowSuggestionsWhenEmpty = config.allowSuggestionsWhenEmpty || false;
5517
5518 this.lookupCache = {};
5519 this.lookupQuery = null;
5520 this.lookupRequest = null;
5521 this.lookupsDisabled = false;
5522 this.lookupInputFocused = false;
5523
5524 // Events
5525 this.$input.on( {
5526 focus: this.onLookupInputFocus.bind( this ),
5527 blur: this.onLookupInputBlur.bind( this ),
5528 mousedown: this.onLookupInputMouseDown.bind( this )
5529 } );
5530 this.connect( this, { change: 'onLookupInputChange' } );
5531 this.lookupMenu.connect( this, {
5532 toggle: 'onLookupMenuToggle',
5533 choose: 'onLookupMenuItemChoose'
5534 } );
5535
5536 // Initialization
5537 this.$element.addClass( 'oo-ui-lookupElement' );
5538 this.lookupMenu.$element.addClass( 'oo-ui-lookupElement-menu' );
5539 this.$overlay.append( this.lookupMenu.$element );
5540 };
5541
5542 /* Methods */
5543
5544 /**
5545 * Handle input focus event.
5546 *
5547 * @protected
5548 * @param {jQuery.Event} e Input focus event
5549 */
5550 OO.ui.LookupElement.prototype.onLookupInputFocus = function () {
5551 this.lookupInputFocused = true;
5552 this.populateLookupMenu();
5553 };
5554
5555 /**
5556 * Handle input blur event.
5557 *
5558 * @protected
5559 * @param {jQuery.Event} e Input blur event
5560 */
5561 OO.ui.LookupElement.prototype.onLookupInputBlur = function () {
5562 this.closeLookupMenu();
5563 this.lookupInputFocused = false;
5564 };
5565
5566 /**
5567 * Handle input mouse down event.
5568 *
5569 * @protected
5570 * @param {jQuery.Event} e Input mouse down event
5571 */
5572 OO.ui.LookupElement.prototype.onLookupInputMouseDown = function () {
5573 // Only open the menu if the input was already focused.
5574 // This way we allow the user to open the menu again after closing it with Esc
5575 // by clicking in the input. Opening (and populating) the menu when initially
5576 // clicking into the input is handled by the focus handler.
5577 if ( this.lookupInputFocused && !this.lookupMenu.isVisible() ) {
5578 this.populateLookupMenu();
5579 }
5580 };
5581
5582 /**
5583 * Handle input change event.
5584 *
5585 * @protected
5586 * @param {string} value New input value
5587 */
5588 OO.ui.LookupElement.prototype.onLookupInputChange = function () {
5589 if ( this.lookupInputFocused ) {
5590 this.populateLookupMenu();
5591 }
5592 };
5593
5594 /**
5595 * Handle the lookup menu being shown/hidden.
5596 *
5597 * @protected
5598 * @param {boolean} visible Whether the lookup menu is now visible.
5599 */
5600 OO.ui.LookupElement.prototype.onLookupMenuToggle = function ( visible ) {
5601 if ( !visible ) {
5602 // When the menu is hidden, abort any active request and clear the menu.
5603 // This has to be done here in addition to closeLookupMenu(), because
5604 // MenuSelectWidget will close itself when the user presses Esc.
5605 this.abortLookupRequest();
5606 this.lookupMenu.clearItems();
5607 }
5608 };
5609
5610 /**
5611 * Handle menu item 'choose' event, updating the text input value to the value of the clicked item.
5612 *
5613 * @protected
5614 * @param {OO.ui.MenuOptionWidget} item Selected item
5615 */
5616 OO.ui.LookupElement.prototype.onLookupMenuItemChoose = function ( item ) {
5617 this.setValue( item.getData() );
5618 };
5619
5620 /**
5621 * Get lookup menu.
5622 *
5623 * @private
5624 * @return {OO.ui.TextInputMenuSelectWidget}
5625 */
5626 OO.ui.LookupElement.prototype.getLookupMenu = function () {
5627 return this.lookupMenu;
5628 };
5629
5630 /**
5631 * Disable or re-enable lookups.
5632 *
5633 * When lookups are disabled, calls to #populateLookupMenu will be ignored.
5634 *
5635 * @param {boolean} disabled Disable lookups
5636 */
5637 OO.ui.LookupElement.prototype.setLookupsDisabled = function ( disabled ) {
5638 this.lookupsDisabled = !!disabled;
5639 };
5640
5641 /**
5642 * Open the menu. If there are no entries in the menu, this does nothing.
5643 *
5644 * @private
5645 * @chainable
5646 */
5647 OO.ui.LookupElement.prototype.openLookupMenu = function () {
5648 if ( !this.lookupMenu.isEmpty() ) {
5649 this.lookupMenu.toggle( true );
5650 }
5651 return this;
5652 };
5653
5654 /**
5655 * Close the menu, empty it, and abort any pending request.
5656 *
5657 * @private
5658 * @chainable
5659 */
5660 OO.ui.LookupElement.prototype.closeLookupMenu = function () {
5661 this.lookupMenu.toggle( false );
5662 this.abortLookupRequest();
5663 this.lookupMenu.clearItems();
5664 return this;
5665 };
5666
5667 /**
5668 * Request menu items based on the input's current value, and when they arrive,
5669 * populate the menu with these items and show the menu.
5670 *
5671 * If lookups have been disabled with #setLookupsDisabled, this function does nothing.
5672 *
5673 * @private
5674 * @chainable
5675 */
5676 OO.ui.LookupElement.prototype.populateLookupMenu = function () {
5677 var widget = this,
5678 value = this.getValue();
5679
5680 if ( this.lookupsDisabled ) {
5681 return;
5682 }
5683
5684 // If the input is empty, clear the menu, unless suggestions when empty are allowed.
5685 if ( !this.allowSuggestionsWhenEmpty && value === '' ) {
5686 this.closeLookupMenu();
5687 // Skip population if there is already a request pending for the current value
5688 } else if ( value !== this.lookupQuery ) {
5689 this.getLookupMenuItems()
5690 .done( function ( items ) {
5691 widget.lookupMenu.clearItems();
5692 if ( items.length ) {
5693 widget.lookupMenu
5694 .addItems( items )
5695 .toggle( true );
5696 widget.initializeLookupMenuSelection();
5697 } else {
5698 widget.lookupMenu.toggle( false );
5699 }
5700 } )
5701 .fail( function () {
5702 widget.lookupMenu.clearItems();
5703 } );
5704 }
5705
5706 return this;
5707 };
5708
5709 /**
5710 * Select and highlight the first selectable item in the menu.
5711 *
5712 * @private
5713 * @chainable
5714 */
5715 OO.ui.LookupElement.prototype.initializeLookupMenuSelection = function () {
5716 if ( !this.lookupMenu.getSelectedItem() ) {
5717 this.lookupMenu.selectItem( this.lookupMenu.getFirstSelectableItem() );
5718 }
5719 this.lookupMenu.highlightItem( this.lookupMenu.getSelectedItem() );
5720 };
5721
5722 /**
5723 * Get lookup menu items for the current query.
5724 *
5725 * @private
5726 * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument of
5727 * the done event. If the request was aborted to make way for a subsequent request, this promise
5728 * will not be rejected: it will remain pending forever.
5729 */
5730 OO.ui.LookupElement.prototype.getLookupMenuItems = function () {
5731 var widget = this,
5732 value = this.getValue(),
5733 deferred = $.Deferred(),
5734 ourRequest;
5735
5736 this.abortLookupRequest();
5737 if ( Object.prototype.hasOwnProperty.call( this.lookupCache, value ) ) {
5738 deferred.resolve( this.getLookupMenuOptionsFromData( this.lookupCache[ value ] ) );
5739 } else {
5740 this.pushPending();
5741 this.lookupQuery = value;
5742 ourRequest = this.lookupRequest = this.getLookupRequest();
5743 ourRequest
5744 .always( function () {
5745 // We need to pop pending even if this is an old request, otherwise
5746 // the widget will remain pending forever.
5747 // TODO: this assumes that an aborted request will fail or succeed soon after
5748 // being aborted, or at least eventually. It would be nice if we could popPending()
5749 // at abort time, but only if we knew that we hadn't already called popPending()
5750 // for that request.
5751 widget.popPending();
5752 } )
5753 .done( function ( response ) {
5754 // If this is an old request (and aborting it somehow caused it to still succeed),
5755 // ignore its success completely
5756 if ( ourRequest === widget.lookupRequest ) {
5757 widget.lookupQuery = null;
5758 widget.lookupRequest = null;
5759 widget.lookupCache[ value ] = widget.getLookupCacheDataFromResponse( response );
5760 deferred.resolve( widget.getLookupMenuOptionsFromData( widget.lookupCache[ value ] ) );
5761 }
5762 } )
5763 .fail( function () {
5764 // If this is an old request (or a request failing because it's being aborted),
5765 // ignore its failure completely
5766 if ( ourRequest === widget.lookupRequest ) {
5767 widget.lookupQuery = null;
5768 widget.lookupRequest = null;
5769 deferred.reject();
5770 }
5771 } );
5772 }
5773 return deferred.promise();
5774 };
5775
5776 /**
5777 * Abort the currently pending lookup request, if any.
5778 *
5779 * @private
5780 */
5781 OO.ui.LookupElement.prototype.abortLookupRequest = function () {
5782 var oldRequest = this.lookupRequest;
5783 if ( oldRequest ) {
5784 // First unset this.lookupRequest to the fail handler will notice
5785 // that the request is no longer current
5786 this.lookupRequest = null;
5787 this.lookupQuery = null;
5788 oldRequest.abort();
5789 }
5790 };
5791
5792 /**
5793 * Get a new request object of the current lookup query value.
5794 *
5795 * @protected
5796 * @abstract
5797 * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
5798 */
5799 OO.ui.LookupElement.prototype.getLookupRequest = function () {
5800 // Stub, implemented in subclass
5801 return null;
5802 };
5803
5804 /**
5805 * Pre-process data returned by the request from #getLookupRequest.
5806 *
5807 * The return value of this function will be cached, and any further queries for the given value
5808 * will use the cache rather than doing API requests.
5809 *
5810 * @protected
5811 * @abstract
5812 * @param {Mixed} response Response from server
5813 * @return {Mixed} Cached result data
5814 */
5815 OO.ui.LookupElement.prototype.getLookupCacheDataFromResponse = function () {
5816 // Stub, implemented in subclass
5817 return [];
5818 };
5819
5820 /**
5821 * Get a list of menu option widgets from the (possibly cached) data returned by
5822 * #getLookupCacheDataFromResponse.
5823 *
5824 * @protected
5825 * @abstract
5826 * @param {Mixed} data Cached result data, usually an array
5827 * @return {OO.ui.MenuOptionWidget[]} Menu items
5828 */
5829 OO.ui.LookupElement.prototype.getLookupMenuOptionsFromData = function () {
5830 // Stub, implemented in subclass
5831 return [];
5832 };
5833
5834 /**
5835 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
5836 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
5837 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
5838 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
5839 *
5840 * @abstract
5841 * @class
5842 *
5843 * @constructor
5844 * @param {Object} [config] Configuration options
5845 * @cfg {Object} [popup] Configuration to pass to popup
5846 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
5847 */
5848 OO.ui.PopupElement = function OoUiPopupElement( config ) {
5849 // Configuration initialization
5850 config = config || {};
5851
5852 // Properties
5853 this.popup = new OO.ui.PopupWidget( $.extend(
5854 { autoClose: true },
5855 config.popup,
5856 { $autoCloseIgnore: this.$element }
5857 ) );
5858 };
5859
5860 /* Methods */
5861
5862 /**
5863 * Get popup.
5864 *
5865 * @return {OO.ui.PopupWidget} Popup widget
5866 */
5867 OO.ui.PopupElement.prototype.getPopup = function () {
5868 return this.popup;
5869 };
5870
5871 /**
5872 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
5873 * additional functionality to an element created by another class. The class provides
5874 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
5875 * which are used to customize the look and feel of a widget to better describe its
5876 * importance and functionality.
5877 *
5878 * The library currently contains the following styling flags for general use:
5879 *
5880 * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
5881 * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
5882 * - **constructive**: Constructive styling is applied to convey that the widget will create something.
5883 *
5884 * The flags affect the appearance of the buttons:
5885 *
5886 * @example
5887 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
5888 * var button1 = new OO.ui.ButtonWidget( {
5889 * label: 'Constructive',
5890 * flags: 'constructive'
5891 * } );
5892 * var button2 = new OO.ui.ButtonWidget( {
5893 * label: 'Destructive',
5894 * flags: 'destructive'
5895 * } );
5896 * var button3 = new OO.ui.ButtonWidget( {
5897 * label: 'Progressive',
5898 * flags: 'progressive'
5899 * } );
5900 * $( 'body' ).append( button1.$element, button2.$element, button3.$element );
5901 *
5902 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
5903 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
5904 *
5905 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
5906 *
5907 * @abstract
5908 * @class
5909 *
5910 * @constructor
5911 * @param {Object} [config] Configuration options
5912 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'constructive' or 'primary') to apply.
5913 * Please see the [OOjs UI documentation on MediaWiki] [2] for more information about available flags.
5914 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
5915 * @cfg {jQuery} [$flagged] The flagged element. By default,
5916 * the flagged functionality is applied to the element created by the class ($element).
5917 * If a different element is specified, the flagged functionality will be applied to it instead.
5918 */
5919 OO.ui.FlaggedElement = function OoUiFlaggedElement( config ) {
5920 // Configuration initialization
5921 config = config || {};
5922
5923 // Properties
5924 this.flags = {};
5925 this.$flagged = null;
5926
5927 // Initialization
5928 this.setFlags( config.flags );
5929 this.setFlaggedElement( config.$flagged || this.$element );
5930 };
5931
5932 /* Events */
5933
5934 /**
5935 * @event flag
5936 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
5937 * parameter contains the name of each modified flag and indicates whether it was
5938 * added or removed.
5939 *
5940 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
5941 * that the flag was added, `false` that the flag was removed.
5942 */
5943
5944 /* Methods */
5945
5946 /**
5947 * Set the flagged element.
5948 *
5949 * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
5950 * If an element is already set, the method will remove the mixin’s effect on that element.
5951 *
5952 * @param {jQuery} $flagged Element that should be flagged
5953 */
5954 OO.ui.FlaggedElement.prototype.setFlaggedElement = function ( $flagged ) {
5955 var classNames = Object.keys( this.flags ).map( function ( flag ) {
5956 return 'oo-ui-flaggedElement-' + flag;
5957 } ).join( ' ' );
5958
5959 if ( this.$flagged ) {
5960 this.$flagged.removeClass( classNames );
5961 }
5962
5963 this.$flagged = $flagged.addClass( classNames );
5964 };
5965
5966 /**
5967 * Check if the specified flag is set.
5968 *
5969 * @param {string} flag Name of flag
5970 * @return {boolean} The flag is set
5971 */
5972 OO.ui.FlaggedElement.prototype.hasFlag = function ( flag ) {
5973 return flag in this.flags;
5974 };
5975
5976 /**
5977 * Get the names of all flags set.
5978 *
5979 * @return {string[]} Flag names
5980 */
5981 OO.ui.FlaggedElement.prototype.getFlags = function () {
5982 return Object.keys( this.flags );
5983 };
5984
5985 /**
5986 * Clear all flags.
5987 *
5988 * @chainable
5989 * @fires flag
5990 */
5991 OO.ui.FlaggedElement.prototype.clearFlags = function () {
5992 var flag, className,
5993 changes = {},
5994 remove = [],
5995 classPrefix = 'oo-ui-flaggedElement-';
5996
5997 for ( flag in this.flags ) {
5998 className = classPrefix + flag;
5999 changes[ flag ] = false;
6000 delete this.flags[ flag ];
6001 remove.push( className );
6002 }
6003
6004 if ( this.$flagged ) {
6005 this.$flagged.removeClass( remove.join( ' ' ) );
6006 }
6007
6008 this.updateThemeClasses();
6009 this.emit( 'flag', changes );
6010
6011 return this;
6012 };
6013
6014 /**
6015 * Add one or more flags.
6016 *
6017 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
6018 * or an object keyed by flag name with a boolean value that indicates whether the flag should
6019 * be added (`true`) or removed (`false`).
6020 * @chainable
6021 * @fires flag
6022 */
6023 OO.ui.FlaggedElement.prototype.setFlags = function ( flags ) {
6024 var i, len, flag, className,
6025 changes = {},
6026 add = [],
6027 remove = [],
6028 classPrefix = 'oo-ui-flaggedElement-';
6029
6030 if ( typeof flags === 'string' ) {
6031 className = classPrefix + flags;
6032 // Set
6033 if ( !this.flags[ flags ] ) {
6034 this.flags[ flags ] = true;
6035 add.push( className );
6036 }
6037 } else if ( Array.isArray( flags ) ) {
6038 for ( i = 0, len = flags.length; i < len; i++ ) {
6039 flag = flags[ i ];
6040 className = classPrefix + flag;
6041 // Set
6042 if ( !this.flags[ flag ] ) {
6043 changes[ flag ] = true;
6044 this.flags[ flag ] = true;
6045 add.push( className );
6046 }
6047 }
6048 } else if ( OO.isPlainObject( flags ) ) {
6049 for ( flag in flags ) {
6050 className = classPrefix + flag;
6051 if ( flags[ flag ] ) {
6052 // Set
6053 if ( !this.flags[ flag ] ) {
6054 changes[ flag ] = true;
6055 this.flags[ flag ] = true;
6056 add.push( className );
6057 }
6058 } else {
6059 // Remove
6060 if ( this.flags[ flag ] ) {
6061 changes[ flag ] = false;
6062 delete this.flags[ flag ];
6063 remove.push( className );
6064 }
6065 }
6066 }
6067 }
6068
6069 if ( this.$flagged ) {
6070 this.$flagged
6071 .addClass( add.join( ' ' ) )
6072 .removeClass( remove.join( ' ' ) );
6073 }
6074
6075 this.updateThemeClasses();
6076 this.emit( 'flag', changes );
6077
6078 return this;
6079 };
6080
6081 /**
6082 * TitledElement is mixed into other classes to provide a `title` attribute.
6083 * Titles are rendered by the browser and are made visible when the user moves
6084 * the mouse over the element. Titles are not visible on touch devices.
6085 *
6086 * @example
6087 * // TitledElement provides a 'title' attribute to the
6088 * // ButtonWidget class
6089 * var button = new OO.ui.ButtonWidget( {
6090 * label: 'Button with Title',
6091 * title: 'I am a button'
6092 * } );
6093 * $( 'body' ).append( button.$element );
6094 *
6095 * @abstract
6096 * @class
6097 *
6098 * @constructor
6099 * @param {Object} [config] Configuration options
6100 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
6101 * If this config is omitted, the title functionality is applied to $element, the
6102 * element created by the class.
6103 * @cfg {string|Function} [title] The title text or a function that returns text. If
6104 * this config is omitted, the value of the {@link #static-title static title} property is used.
6105 */
6106 OO.ui.TitledElement = function OoUiTitledElement( config ) {
6107 // Configuration initialization
6108 config = config || {};
6109
6110 // Properties
6111 this.$titled = null;
6112 this.title = null;
6113
6114 // Initialization
6115 this.setTitle( config.title || this.constructor.static.title );
6116 this.setTitledElement( config.$titled || this.$element );
6117 };
6118
6119 /* Setup */
6120
6121 OO.initClass( OO.ui.TitledElement );
6122
6123 /* Static Properties */
6124
6125 /**
6126 * The title text, a function that returns text, or `null` for no title. The value of the static property
6127 * is overridden if the #title config option is used.
6128 *
6129 * @static
6130 * @inheritable
6131 * @property {string|Function|null}
6132 */
6133 OO.ui.TitledElement.static.title = null;
6134
6135 /* Methods */
6136
6137 /**
6138 * Set the titled element.
6139 *
6140 * This method is used to retarget a titledElement mixin so that its functionality applies to the specified element.
6141 * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
6142 *
6143 * @param {jQuery} $titled Element that should use the 'titled' functionality
6144 */
6145 OO.ui.TitledElement.prototype.setTitledElement = function ( $titled ) {
6146 if ( this.$titled ) {
6147 this.$titled.removeAttr( 'title' );
6148 }
6149
6150 this.$titled = $titled;
6151 if ( this.title ) {
6152 this.$titled.attr( 'title', this.title );
6153 }
6154 };
6155
6156 /**
6157 * Set title.
6158 *
6159 * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
6160 * @chainable
6161 */
6162 OO.ui.TitledElement.prototype.setTitle = function ( title ) {
6163 title = typeof title === 'string' ? OO.ui.resolveMsg( title ) : null;
6164
6165 if ( this.title !== title ) {
6166 if ( this.$titled ) {
6167 if ( title !== null ) {
6168 this.$titled.attr( 'title', title );
6169 } else {
6170 this.$titled.removeAttr( 'title' );
6171 }
6172 }
6173 this.title = title;
6174 }
6175
6176 return this;
6177 };
6178
6179 /**
6180 * Get title.
6181 *
6182 * @return {string} Title string
6183 */
6184 OO.ui.TitledElement.prototype.getTitle = function () {
6185 return this.title;
6186 };
6187
6188 /**
6189 * Element that can be automatically clipped to visible boundaries.
6190 *
6191 * Whenever the element's natural height changes, you have to call
6192 * #clip to make sure it's still clipping correctly.
6193 *
6194 * @abstract
6195 * @class
6196 *
6197 * @constructor
6198 * @param {Object} [config] Configuration options
6199 * @cfg {jQuery} [$clippable] Nodes to clip, assigned to #$clippable, omit to use #$element
6200 */
6201 OO.ui.ClippableElement = function OoUiClippableElement( config ) {
6202 // Configuration initialization
6203 config = config || {};
6204
6205 // Properties
6206 this.$clippable = null;
6207 this.clipping = false;
6208 this.clippedHorizontally = false;
6209 this.clippedVertically = false;
6210 this.$clippableContainer = null;
6211 this.$clippableScroller = null;
6212 this.$clippableWindow = null;
6213 this.idealWidth = null;
6214 this.idealHeight = null;
6215 this.onClippableContainerScrollHandler = this.clip.bind( this );
6216 this.onClippableWindowResizeHandler = this.clip.bind( this );
6217
6218 // Initialization
6219 this.setClippableElement( config.$clippable || this.$element );
6220 };
6221
6222 /* Methods */
6223
6224 /**
6225 * Set clippable element.
6226 *
6227 * If an element is already set, it will be cleaned up before setting up the new element.
6228 *
6229 * @param {jQuery} $clippable Element to make clippable
6230 */
6231 OO.ui.ClippableElement.prototype.setClippableElement = function ( $clippable ) {
6232 if ( this.$clippable ) {
6233 this.$clippable.removeClass( 'oo-ui-clippableElement-clippable' );
6234 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
6235 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
6236 }
6237
6238 this.$clippable = $clippable.addClass( 'oo-ui-clippableElement-clippable' );
6239 this.clip();
6240 };
6241
6242 /**
6243 * Toggle clipping.
6244 *
6245 * Do not turn clipping on until after the element is attached to the DOM and visible.
6246 *
6247 * @param {boolean} [clipping] Enable clipping, omit to toggle
6248 * @chainable
6249 */
6250 OO.ui.ClippableElement.prototype.toggleClipping = function ( clipping ) {
6251 clipping = clipping === undefined ? !this.clipping : !!clipping;
6252
6253 if ( this.clipping !== clipping ) {
6254 this.clipping = clipping;
6255 if ( clipping ) {
6256 this.$clippableContainer = $( this.getClosestScrollableElementContainer() );
6257 // If the clippable container is the root, we have to listen to scroll events and check
6258 // jQuery.scrollTop on the window because of browser inconsistencies
6259 this.$clippableScroller = this.$clippableContainer.is( 'html, body' ) ?
6260 $( OO.ui.Element.static.getWindow( this.$clippableContainer ) ) :
6261 this.$clippableContainer;
6262 this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler );
6263 this.$clippableWindow = $( this.getElementWindow() )
6264 .on( 'resize', this.onClippableWindowResizeHandler );
6265 // Initial clip after visible
6266 this.clip();
6267 } else {
6268 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
6269 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
6270
6271 this.$clippableContainer = null;
6272 this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler );
6273 this.$clippableScroller = null;
6274 this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
6275 this.$clippableWindow = null;
6276 }
6277 }
6278
6279 return this;
6280 };
6281
6282 /**
6283 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
6284 *
6285 * @return {boolean} Element will be clipped to the visible area
6286 */
6287 OO.ui.ClippableElement.prototype.isClipping = function () {
6288 return this.clipping;
6289 };
6290
6291 /**
6292 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
6293 *
6294 * @return {boolean} Part of the element is being clipped
6295 */
6296 OO.ui.ClippableElement.prototype.isClipped = function () {
6297 return this.clippedHorizontally || this.clippedVertically;
6298 };
6299
6300 /**
6301 * Check if the right of the element is being clipped by the nearest scrollable container.
6302 *
6303 * @return {boolean} Part of the element is being clipped
6304 */
6305 OO.ui.ClippableElement.prototype.isClippedHorizontally = function () {
6306 return this.clippedHorizontally;
6307 };
6308
6309 /**
6310 * Check if the bottom of the element is being clipped by the nearest scrollable container.
6311 *
6312 * @return {boolean} Part of the element is being clipped
6313 */
6314 OO.ui.ClippableElement.prototype.isClippedVertically = function () {
6315 return this.clippedVertically;
6316 };
6317
6318 /**
6319 * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
6320 *
6321 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
6322 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
6323 */
6324 OO.ui.ClippableElement.prototype.setIdealSize = function ( width, height ) {
6325 this.idealWidth = width;
6326 this.idealHeight = height;
6327
6328 if ( !this.clipping ) {
6329 // Update dimensions
6330 this.$clippable.css( { width: width, height: height } );
6331 }
6332 // While clipping, idealWidth and idealHeight are not considered
6333 };
6334
6335 /**
6336 * Clip element to visible boundaries and allow scrolling when needed. Call this method when
6337 * the element's natural height changes.
6338 *
6339 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
6340 * overlapped by, the visible area of the nearest scrollable container.
6341 *
6342 * @chainable
6343 */
6344 OO.ui.ClippableElement.prototype.clip = function () {
6345 if ( !this.clipping ) {
6346 // this.$clippableContainer and this.$clippableWindow are null, so the below will fail
6347 return this;
6348 }
6349
6350 var buffer = 7, // Chosen by fair dice roll
6351 cOffset = this.$clippable.offset(),
6352 $container = this.$clippableContainer.is( 'html, body' ) ?
6353 this.$clippableWindow : this.$clippableContainer,
6354 ccOffset = $container.offset() || { top: 0, left: 0 },
6355 ccHeight = $container.innerHeight() - buffer,
6356 ccWidth = $container.innerWidth() - buffer,
6357 cHeight = this.$clippable.outerHeight() + buffer,
6358 cWidth = this.$clippable.outerWidth() + buffer,
6359 scrollTop = this.$clippableScroller.scrollTop(),
6360 scrollLeft = this.$clippableScroller.scrollLeft(),
6361 desiredWidth = cOffset.left < 0 ?
6362 cWidth + cOffset.left :
6363 ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left,
6364 desiredHeight = cOffset.top < 0 ?
6365 cHeight + cOffset.top :
6366 ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top,
6367 naturalWidth = this.$clippable.prop( 'scrollWidth' ),
6368 naturalHeight = this.$clippable.prop( 'scrollHeight' ),
6369 clipWidth = desiredWidth < naturalWidth,
6370 clipHeight = desiredHeight < naturalHeight;
6371
6372 if ( clipWidth ) {
6373 this.$clippable.css( { overflowX: 'scroll', width: desiredWidth } );
6374 } else {
6375 this.$clippable.css( { width: this.idealWidth || '', overflowX: '' } );
6376 }
6377 if ( clipHeight ) {
6378 this.$clippable.css( { overflowY: 'scroll', height: desiredHeight } );
6379 } else {
6380 this.$clippable.css( { height: this.idealHeight || '', overflowY: '' } );
6381 }
6382
6383 // If we stopped clipping in at least one of the dimensions
6384 if ( !clipWidth || !clipHeight ) {
6385 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
6386 }
6387
6388 this.clippedHorizontally = clipWidth;
6389 this.clippedVertically = clipHeight;
6390
6391 return this;
6392 };
6393
6394 /**
6395 * Generic toolbar tool.
6396 *
6397 * @abstract
6398 * @class
6399 * @extends OO.ui.Widget
6400 * @mixins OO.ui.IconElement
6401 * @mixins OO.ui.FlaggedElement
6402 * @mixins OO.ui.TabIndexedElement
6403 *
6404 * @constructor
6405 * @param {OO.ui.ToolGroup} toolGroup
6406 * @param {Object} [config] Configuration options
6407 * @cfg {string|Function} [title] Title text or a function that returns text
6408 */
6409 OO.ui.Tool = function OoUiTool( toolGroup, config ) {
6410 // Allow passing positional parameters inside the config object
6411 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
6412 config = toolGroup;
6413 toolGroup = config.toolGroup;
6414 }
6415
6416 // Configuration initialization
6417 config = config || {};
6418
6419 // Parent constructor
6420 OO.ui.Tool.super.call( this, config );
6421
6422 // Properties
6423 this.toolGroup = toolGroup;
6424 this.toolbar = this.toolGroup.getToolbar();
6425 this.active = false;
6426 this.$title = $( '<span>' );
6427 this.$accel = $( '<span>' );
6428 this.$link = $( '<a>' );
6429 this.title = null;
6430
6431 // Mixin constructors
6432 OO.ui.IconElement.call( this, config );
6433 OO.ui.FlaggedElement.call( this, config );
6434 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$link } ) );
6435
6436 // Events
6437 this.toolbar.connect( this, { updateState: 'onUpdateState' } );
6438
6439 // Initialization
6440 this.$title.addClass( 'oo-ui-tool-title' );
6441 this.$accel
6442 .addClass( 'oo-ui-tool-accel' )
6443 .prop( {
6444 // This may need to be changed if the key names are ever localized,
6445 // but for now they are essentially written in English
6446 dir: 'ltr',
6447 lang: 'en'
6448 } );
6449 this.$link
6450 .addClass( 'oo-ui-tool-link' )
6451 .append( this.$icon, this.$title, this.$accel )
6452 .attr( 'role', 'button' );
6453 this.$element
6454 .data( 'oo-ui-tool', this )
6455 .addClass(
6456 'oo-ui-tool ' + 'oo-ui-tool-name-' +
6457 this.constructor.static.name.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' )
6458 )
6459 .append( this.$link );
6460 this.setTitle( config.title || this.constructor.static.title );
6461 };
6462
6463 /* Setup */
6464
6465 OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
6466 OO.mixinClass( OO.ui.Tool, OO.ui.IconElement );
6467 OO.mixinClass( OO.ui.Tool, OO.ui.FlaggedElement );
6468 OO.mixinClass( OO.ui.Tool, OO.ui.TabIndexedElement );
6469
6470 /* Events */
6471
6472 /**
6473 * @event select
6474 */
6475
6476 /* Static Properties */
6477
6478 /**
6479 * @static
6480 * @inheritdoc
6481 */
6482 OO.ui.Tool.static.tagName = 'span';
6483
6484 /**
6485 * Symbolic name of tool.
6486 *
6487 * @abstract
6488 * @static
6489 * @inheritable
6490 * @property {string}
6491 */
6492 OO.ui.Tool.static.name = '';
6493
6494 /**
6495 * Tool group.
6496 *
6497 * @abstract
6498 * @static
6499 * @inheritable
6500 * @property {string}
6501 */
6502 OO.ui.Tool.static.group = '';
6503
6504 /**
6505 * Tool title.
6506 *
6507 * Title is used as a tooltip when the tool is part of a bar tool group, or a label when the tool
6508 * is part of a list or menu tool group. If a trigger is associated with an action by the same name
6509 * as the tool, a description of its keyboard shortcut for the appropriate platform will be
6510 * appended to the title if the tool is part of a bar tool group.
6511 *
6512 * @abstract
6513 * @static
6514 * @inheritable
6515 * @property {string|Function} Title text or a function that returns text
6516 */
6517 OO.ui.Tool.static.title = '';
6518
6519 /**
6520 * Tool can be automatically added to catch-all groups.
6521 *
6522 * @static
6523 * @inheritable
6524 * @property {boolean}
6525 */
6526 OO.ui.Tool.static.autoAddToCatchall = true;
6527
6528 /**
6529 * Tool can be automatically added to named groups.
6530 *
6531 * @static
6532 * @property {boolean}
6533 * @inheritable
6534 */
6535 OO.ui.Tool.static.autoAddToGroup = true;
6536
6537 /**
6538 * Check if this tool is compatible with given data.
6539 *
6540 * @static
6541 * @inheritable
6542 * @param {Mixed} data Data to check
6543 * @return {boolean} Tool can be used with data
6544 */
6545 OO.ui.Tool.static.isCompatibleWith = function () {
6546 return false;
6547 };
6548
6549 /* Methods */
6550
6551 /**
6552 * Handle the toolbar state being updated.
6553 *
6554 * This is an abstract method that must be overridden in a concrete subclass.
6555 *
6556 * @abstract
6557 */
6558 OO.ui.Tool.prototype.onUpdateState = function () {
6559 throw new Error(
6560 'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor
6561 );
6562 };
6563
6564 /**
6565 * Handle the tool being selected.
6566 *
6567 * This is an abstract method that must be overridden in a concrete subclass.
6568 *
6569 * @abstract
6570 */
6571 OO.ui.Tool.prototype.onSelect = function () {
6572 throw new Error(
6573 'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor
6574 );
6575 };
6576
6577 /**
6578 * Check if the button is active.
6579 *
6580 * @return {boolean} Button is active
6581 */
6582 OO.ui.Tool.prototype.isActive = function () {
6583 return this.active;
6584 };
6585
6586 /**
6587 * Make the button appear active or inactive.
6588 *
6589 * @param {boolean} state Make button appear active
6590 */
6591 OO.ui.Tool.prototype.setActive = function ( state ) {
6592 this.active = !!state;
6593 if ( this.active ) {
6594 this.$element.addClass( 'oo-ui-tool-active' );
6595 } else {
6596 this.$element.removeClass( 'oo-ui-tool-active' );
6597 }
6598 };
6599
6600 /**
6601 * Get the tool title.
6602 *
6603 * @param {string|Function} title Title text or a function that returns text
6604 * @chainable
6605 */
6606 OO.ui.Tool.prototype.setTitle = function ( title ) {
6607 this.title = OO.ui.resolveMsg( title );
6608 this.updateTitle();
6609 return this;
6610 };
6611
6612 /**
6613 * Get the tool title.
6614 *
6615 * @return {string} Title text
6616 */
6617 OO.ui.Tool.prototype.getTitle = function () {
6618 return this.title;
6619 };
6620
6621 /**
6622 * Get the tool's symbolic name.
6623 *
6624 * @return {string} Symbolic name of tool
6625 */
6626 OO.ui.Tool.prototype.getName = function () {
6627 return this.constructor.static.name;
6628 };
6629
6630 /**
6631 * Update the title.
6632 */
6633 OO.ui.Tool.prototype.updateTitle = function () {
6634 var titleTooltips = this.toolGroup.constructor.static.titleTooltips,
6635 accelTooltips = this.toolGroup.constructor.static.accelTooltips,
6636 accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
6637 tooltipParts = [];
6638
6639 this.$title.text( this.title );
6640 this.$accel.text( accel );
6641
6642 if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
6643 tooltipParts.push( this.title );
6644 }
6645 if ( accelTooltips && typeof accel === 'string' && accel.length ) {
6646 tooltipParts.push( accel );
6647 }
6648 if ( tooltipParts.length ) {
6649 this.$link.attr( 'title', tooltipParts.join( ' ' ) );
6650 } else {
6651 this.$link.removeAttr( 'title' );
6652 }
6653 };
6654
6655 /**
6656 * Destroy tool.
6657 */
6658 OO.ui.Tool.prototype.destroy = function () {
6659 this.toolbar.disconnect( this );
6660 this.$element.remove();
6661 };
6662
6663 /**
6664 * Collection of tool groups.
6665 *
6666 * @example
6667 * // Basic OOjs UI toolbar example
6668 *
6669 * // Create the toolbar
6670 * var toolFactory = new OO.ui.ToolFactory();
6671 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
6672 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
6673 *
6674 * // We will be placing status text in this element when tools are used
6675 * var $area = $( '<p>' ).text( 'Toolbar example' );
6676 *
6677 * // Define the tools that we're going to place in our toolbar
6678 *
6679 * // Create a class inheriting from OO.ui.Tool
6680 * function PictureTool() {
6681 * PictureTool.super.apply( this, arguments );
6682 * };
6683 * OO.inheritClass( PictureTool, OO.ui.Tool );
6684 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
6685 * // of 'icon' and 'title' (displayed icon and text).
6686 * PictureTool.static.name = 'picture';
6687 * PictureTool.static.icon = 'picture';
6688 * PictureTool.static.title = 'Insert picture';
6689 * // Defines the action that will happen when this tool is selected (clicked).
6690 * PictureTool.prototype.onSelect = function () {
6691 * $area.text( 'Picture tool clicked!' );
6692 * this.setActive( false );
6693 * };
6694 * // The toolbar can be synchronized with the state of some external stuff, like a text
6695 * // editor's editing area, highlighting the tools (e.g. a 'bold' tool would be shown as active
6696 * // when the text cursor was inside bolded text). Here we simply disable this feature.
6697 * PictureTool.prototype.onUpdateState = function () {
6698 * this.setActive( false );
6699 * };
6700 * // Make this tool available in our toolFactory and thus our toolbar
6701 * toolFactory.register( PictureTool );
6702 *
6703 * // Register two more tools, nothing interesting here
6704 * function SettingsTool() {
6705 * SettingsTool.super.apply( this, arguments );
6706 * };
6707 * OO.inheritClass( SettingsTool, OO.ui.Tool );
6708 * SettingsTool.static.name = 'settings';
6709 * SettingsTool.static.icon = 'settings';
6710 * SettingsTool.static.title = 'Change settings';
6711 * SettingsTool.prototype.onSelect = function () {
6712 * $area.text( 'Settings tool clicked!' );
6713 * this.setActive( false );
6714 * };
6715 * SettingsTool.prototype.onUpdateState = function () {
6716 * this.setActive( false );
6717 * };
6718 * toolFactory.register( SettingsTool );
6719 *
6720 * // Register two more tools, nothing interesting here
6721 * function StuffTool() {
6722 * StuffTool.super.apply( this, arguments );
6723 * };
6724 * OO.inheritClass( StuffTool, OO.ui.Tool );
6725 * StuffTool.static.name = 'stuff';
6726 * StuffTool.static.icon = 'ellipsis';
6727 * StuffTool.static.title = 'More stuff';
6728 * StuffTool.prototype.onSelect = function () {
6729 * $area.text( 'More stuff tool clicked!' );
6730 * this.setActive( false );
6731 * };
6732 * StuffTool.prototype.onUpdateState = function () {
6733 * this.setActive( false );
6734 * };
6735 * toolFactory.register( StuffTool );
6736 *
6737 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
6738 * // little popup window (a PopupWidget). 'onUpdateState' is also already implemented.
6739 * function HelpTool( toolGroup, config ) {
6740 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
6741 * padded: true,
6742 * label: 'Help',
6743 * head: true
6744 * } }, config ) );
6745 * this.popup.$body.append( '<p>I am helpful!</p>' );
6746 * };
6747 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
6748 * HelpTool.static.name = 'help';
6749 * HelpTool.static.icon = 'help';
6750 * HelpTool.static.title = 'Help';
6751 * toolFactory.register( HelpTool );
6752 *
6753 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
6754 * // used once (but not all defined tools must be used).
6755 * toolbar.setup( [
6756 * {
6757 * // 'bar' tool groups display tools' icons only, side-by-side.
6758 * type: 'bar',
6759 * include: [ 'picture', 'help' ]
6760 * },
6761 * {
6762 * // 'list' tool groups display both the titles and icons, in a dropdown list.
6763 * type: 'list',
6764 * indicator: 'down',
6765 * label: 'More',
6766 * include: [ 'settings', 'stuff' ]
6767 * }
6768 * // Note how the tools themselves are toolgroup-agnostic - the same tool can be displayed
6769 * // either in a 'list' or a 'bar'. There is a 'menu' tool group too, not showcased here.
6770 * ] );
6771 *
6772 * // Create some UI around the toolbar and place it in the document
6773 * var frame = new OO.ui.PanelLayout( {
6774 * expanded: false,
6775 * framed: true
6776 * } );
6777 * var contentFrame = new OO.ui.PanelLayout( {
6778 * expanded: false,
6779 * padded: true
6780 * } );
6781 * frame.$element.append(
6782 * toolbar.$element,
6783 * contentFrame.$element.append( $area )
6784 * );
6785 * $( 'body' ).append( frame.$element );
6786 *
6787 * // Here is where the toolbar is actually built. This must be done after inserting it into the
6788 * // document.
6789 * toolbar.initialize();
6790 * toolbar.emit( 'updateState' );
6791 *
6792 * @class
6793 * @extends OO.ui.Element
6794 * @mixins OO.EventEmitter
6795 * @mixins OO.ui.GroupElement
6796 *
6797 * @constructor
6798 * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
6799 * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating tool groups
6800 * @param {Object} [config] Configuration options
6801 * @cfg {boolean} [actions] Add an actions section opposite to the tools
6802 * @cfg {boolean} [shadow] Add a shadow below the toolbar
6803 */
6804 OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
6805 // Allow passing positional parameters inside the config object
6806 if ( OO.isPlainObject( toolFactory ) && config === undefined ) {
6807 config = toolFactory;
6808 toolFactory = config.toolFactory;
6809 toolGroupFactory = config.toolGroupFactory;
6810 }
6811
6812 // Configuration initialization
6813 config = config || {};
6814
6815 // Parent constructor
6816 OO.ui.Toolbar.super.call( this, config );
6817
6818 // Mixin constructors
6819 OO.EventEmitter.call( this );
6820 OO.ui.GroupElement.call( this, config );
6821
6822 // Properties
6823 this.toolFactory = toolFactory;
6824 this.toolGroupFactory = toolGroupFactory;
6825 this.groups = [];
6826 this.tools = {};
6827 this.$bar = $( '<div>' );
6828 this.$actions = $( '<div>' );
6829 this.initialized = false;
6830 this.onWindowResizeHandler = this.onWindowResize.bind( this );
6831
6832 // Events
6833 this.$element
6834 .add( this.$bar ).add( this.$group ).add( this.$actions )
6835 .on( 'mousedown keydown', this.onPointerDown.bind( this ) );
6836
6837 // Initialization
6838 this.$group.addClass( 'oo-ui-toolbar-tools' );
6839 if ( config.actions ) {
6840 this.$bar.append( this.$actions.addClass( 'oo-ui-toolbar-actions' ) );
6841 }
6842 this.$bar
6843 .addClass( 'oo-ui-toolbar-bar' )
6844 .append( this.$group, '<div style="clear:both"></div>' );
6845 if ( config.shadow ) {
6846 this.$bar.append( '<div class="oo-ui-toolbar-shadow"></div>' );
6847 }
6848 this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar );
6849 };
6850
6851 /* Setup */
6852
6853 OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
6854 OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
6855 OO.mixinClass( OO.ui.Toolbar, OO.ui.GroupElement );
6856
6857 /* Methods */
6858
6859 /**
6860 * Get the tool factory.
6861 *
6862 * @return {OO.ui.ToolFactory} Tool factory
6863 */
6864 OO.ui.Toolbar.prototype.getToolFactory = function () {
6865 return this.toolFactory;
6866 };
6867
6868 /**
6869 * Get the tool group factory.
6870 *
6871 * @return {OO.Factory} Tool group factory
6872 */
6873 OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
6874 return this.toolGroupFactory;
6875 };
6876
6877 /**
6878 * Handles mouse down events.
6879 *
6880 * @param {jQuery.Event} e Mouse down event
6881 */
6882 OO.ui.Toolbar.prototype.onPointerDown = function ( e ) {
6883 var $closestWidgetToEvent = $( e.target ).closest( '.oo-ui-widget' ),
6884 $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
6885 if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[ 0 ] === $closestWidgetToToolbar[ 0 ] ) {
6886 return false;
6887 }
6888 };
6889
6890 /**
6891 * Handle window resize event.
6892 *
6893 * @private
6894 * @param {jQuery.Event} e Window resize event
6895 */
6896 OO.ui.Toolbar.prototype.onWindowResize = function () {
6897 this.$element.toggleClass(
6898 'oo-ui-toolbar-narrow',
6899 this.$bar.width() <= this.narrowThreshold
6900 );
6901 };
6902
6903 /**
6904 * Sets up handles and preloads required information for the toolbar to work.
6905 * This must be called after it is attached to a visible document and before doing anything else.
6906 */
6907 OO.ui.Toolbar.prototype.initialize = function () {
6908 this.initialized = true;
6909 this.narrowThreshold = this.$group.width() + this.$actions.width();
6910 $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
6911 this.onWindowResize();
6912 };
6913
6914 /**
6915 * Setup toolbar.
6916 *
6917 * Tools can be specified in the following ways:
6918 *
6919 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
6920 * - All tools in a group: `{ group: 'group-name' }`
6921 * - All tools: `'*'` - Using this will make the group a list with a "More" label by default
6922 *
6923 * @param {Object.<string,Array>} groups List of tool group configurations
6924 * @param {Array|string} [groups.include] Tools to include
6925 * @param {Array|string} [groups.exclude] Tools to exclude
6926 * @param {Array|string} [groups.promote] Tools to promote to the beginning
6927 * @param {Array|string} [groups.demote] Tools to demote to the end
6928 */
6929 OO.ui.Toolbar.prototype.setup = function ( groups ) {
6930 var i, len, type, group,
6931 items = [],
6932 defaultType = 'bar';
6933
6934 // Cleanup previous groups
6935 this.reset();
6936
6937 // Build out new groups
6938 for ( i = 0, len = groups.length; i < len; i++ ) {
6939 group = groups[ i ];
6940 if ( group.include === '*' ) {
6941 // Apply defaults to catch-all groups
6942 if ( group.type === undefined ) {
6943 group.type = 'list';
6944 }
6945 if ( group.label === undefined ) {
6946 group.label = OO.ui.msg( 'ooui-toolbar-more' );
6947 }
6948 }
6949 // Check type has been registered
6950 type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType;
6951 items.push(
6952 this.getToolGroupFactory().create( type, this, group )
6953 );
6954 }
6955 this.addItems( items );
6956 };
6957
6958 /**
6959 * Remove all tools and groups from the toolbar.
6960 */
6961 OO.ui.Toolbar.prototype.reset = function () {
6962 var i, len;
6963
6964 this.groups = [];
6965 this.tools = {};
6966 for ( i = 0, len = this.items.length; i < len; i++ ) {
6967 this.items[ i ].destroy();
6968 }
6969 this.clearItems();
6970 };
6971
6972 /**
6973 * Destroys toolbar, removing event handlers and DOM elements.
6974 *
6975 * Call this whenever you are done using a toolbar.
6976 */
6977 OO.ui.Toolbar.prototype.destroy = function () {
6978 $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
6979 this.reset();
6980 this.$element.remove();
6981 };
6982
6983 /**
6984 * Check if tool has not been used yet.
6985 *
6986 * @param {string} name Symbolic name of tool
6987 * @return {boolean} Tool is available
6988 */
6989 OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
6990 return !this.tools[ name ];
6991 };
6992
6993 /**
6994 * Prevent tool from being used again.
6995 *
6996 * @param {OO.ui.Tool} tool Tool to reserve
6997 */
6998 OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
6999 this.tools[ tool.getName() ] = tool;
7000 };
7001
7002 /**
7003 * Allow tool to be used again.
7004 *
7005 * @param {OO.ui.Tool} tool Tool to release
7006 */
7007 OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
7008 delete this.tools[ tool.getName() ];
7009 };
7010
7011 /**
7012 * Get accelerator label for tool.
7013 *
7014 * This is a stub that should be overridden to provide access to accelerator information.
7015 *
7016 * @param {string} name Symbolic name of tool
7017 * @return {string|undefined} Tool accelerator label if available
7018 */
7019 OO.ui.Toolbar.prototype.getToolAccelerator = function () {
7020 return undefined;
7021 };
7022
7023 /**
7024 * Collection of tools.
7025 *
7026 * Tools can be specified in the following ways:
7027 *
7028 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
7029 * - All tools in a group: `{ group: 'group-name' }`
7030 * - All tools: `'*'`
7031 *
7032 * @abstract
7033 * @class
7034 * @extends OO.ui.Widget
7035 * @mixins OO.ui.GroupElement
7036 *
7037 * @constructor
7038 * @param {OO.ui.Toolbar} toolbar
7039 * @param {Object} [config] Configuration options
7040 * @cfg {Array|string} [include=[]] List of tools to include
7041 * @cfg {Array|string} [exclude=[]] List of tools to exclude
7042 * @cfg {Array|string} [promote=[]] List of tools to promote to the beginning
7043 * @cfg {Array|string} [demote=[]] List of tools to demote to the end
7044 */
7045 OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
7046 // Allow passing positional parameters inside the config object
7047 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
7048 config = toolbar;
7049 toolbar = config.toolbar;
7050 }
7051
7052 // Configuration initialization
7053 config = config || {};
7054
7055 // Parent constructor
7056 OO.ui.ToolGroup.super.call( this, config );
7057
7058 // Mixin constructors
7059 OO.ui.GroupElement.call( this, config );
7060
7061 // Properties
7062 this.toolbar = toolbar;
7063 this.tools = {};
7064 this.pressed = null;
7065 this.autoDisabled = false;
7066 this.include = config.include || [];
7067 this.exclude = config.exclude || [];
7068 this.promote = config.promote || [];
7069 this.demote = config.demote || [];
7070 this.onCapturedMouseKeyUpHandler = this.onCapturedMouseKeyUp.bind( this );
7071
7072 // Events
7073 this.$element.on( {
7074 mousedown: this.onMouseKeyDown.bind( this ),
7075 mouseup: this.onMouseKeyUp.bind( this ),
7076 keydown: this.onMouseKeyDown.bind( this ),
7077 keyup: this.onMouseKeyUp.bind( this ),
7078 focus: this.onMouseOverFocus.bind( this ),
7079 blur: this.onMouseOutBlur.bind( this ),
7080 mouseover: this.onMouseOverFocus.bind( this ),
7081 mouseout: this.onMouseOutBlur.bind( this )
7082 } );
7083 this.toolbar.getToolFactory().connect( this, { register: 'onToolFactoryRegister' } );
7084 this.aggregate( { disable: 'itemDisable' } );
7085 this.connect( this, { itemDisable: 'updateDisabled' } );
7086
7087 // Initialization
7088 this.$group.addClass( 'oo-ui-toolGroup-tools' );
7089 this.$element
7090 .addClass( 'oo-ui-toolGroup' )
7091 .append( this.$group );
7092 this.populate();
7093 };
7094
7095 /* Setup */
7096
7097 OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget );
7098 OO.mixinClass( OO.ui.ToolGroup, OO.ui.GroupElement );
7099
7100 /* Events */
7101
7102 /**
7103 * @event update
7104 */
7105
7106 /* Static Properties */
7107
7108 /**
7109 * Show labels in tooltips.
7110 *
7111 * @static
7112 * @inheritable
7113 * @property {boolean}
7114 */
7115 OO.ui.ToolGroup.static.titleTooltips = false;
7116
7117 /**
7118 * Show acceleration labels in tooltips.
7119 *
7120 * @static
7121 * @inheritable
7122 * @property {boolean}
7123 */
7124 OO.ui.ToolGroup.static.accelTooltips = false;
7125
7126 /**
7127 * Automatically disable the toolgroup when all tools are disabled
7128 *
7129 * @static
7130 * @inheritable
7131 * @property {boolean}
7132 */
7133 OO.ui.ToolGroup.static.autoDisable = true;
7134
7135 /* Methods */
7136
7137 /**
7138 * @inheritdoc
7139 */
7140 OO.ui.ToolGroup.prototype.isDisabled = function () {
7141 return this.autoDisabled || OO.ui.ToolGroup.super.prototype.isDisabled.apply( this, arguments );
7142 };
7143
7144 /**
7145 * @inheritdoc
7146 */
7147 OO.ui.ToolGroup.prototype.updateDisabled = function () {
7148 var i, item, allDisabled = true;
7149
7150 if ( this.constructor.static.autoDisable ) {
7151 for ( i = this.items.length - 1; i >= 0; i-- ) {
7152 item = this.items[ i ];
7153 if ( !item.isDisabled() ) {
7154 allDisabled = false;
7155 break;
7156 }
7157 }
7158 this.autoDisabled = allDisabled;
7159 }
7160 OO.ui.ToolGroup.super.prototype.updateDisabled.apply( this, arguments );
7161 };
7162
7163 /**
7164 * Handle mouse down and key down events.
7165 *
7166 * @param {jQuery.Event} e Mouse down or key down event
7167 */
7168 OO.ui.ToolGroup.prototype.onMouseKeyDown = function ( e ) {
7169 if (
7170 !this.isDisabled() &&
7171 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
7172 ) {
7173 this.pressed = this.getTargetTool( e );
7174 if ( this.pressed ) {
7175 this.pressed.setActive( true );
7176 this.getElementDocument().addEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler, true );
7177 this.getElementDocument().addEventListener( 'keyup', this.onCapturedMouseKeyUpHandler, true );
7178 }
7179 return false;
7180 }
7181 };
7182
7183 /**
7184 * Handle captured mouse up and key up events.
7185 *
7186 * @param {Event} e Mouse up or key up event
7187 */
7188 OO.ui.ToolGroup.prototype.onCapturedMouseKeyUp = function ( e ) {
7189 this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler, true );
7190 this.getElementDocument().removeEventListener( 'keyup', this.onCapturedMouseKeyUpHandler, true );
7191 // onMouseKeyUp may be called a second time, depending on where the mouse is when the button is
7192 // released, but since `this.pressed` will no longer be true, the second call will be ignored.
7193 this.onMouseKeyUp( e );
7194 };
7195
7196 /**
7197 * Handle mouse up and key up events.
7198 *
7199 * @param {jQuery.Event} e Mouse up or key up event
7200 */
7201 OO.ui.ToolGroup.prototype.onMouseKeyUp = function ( e ) {
7202 var tool = this.getTargetTool( e );
7203
7204 if (
7205 !this.isDisabled() && this.pressed && this.pressed === tool &&
7206 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
7207 ) {
7208 this.pressed.onSelect();
7209 this.pressed = null;
7210 return false;
7211 }
7212
7213 this.pressed = null;
7214 };
7215
7216 /**
7217 * Handle mouse over and focus events.
7218 *
7219 * @param {jQuery.Event} e Mouse over or focus event
7220 */
7221 OO.ui.ToolGroup.prototype.onMouseOverFocus = function ( e ) {
7222 var tool = this.getTargetTool( e );
7223
7224 if ( this.pressed && this.pressed === tool ) {
7225 this.pressed.setActive( true );
7226 }
7227 };
7228
7229 /**
7230 * Handle mouse out and blur events.
7231 *
7232 * @param {jQuery.Event} e Mouse out or blur event
7233 */
7234 OO.ui.ToolGroup.prototype.onMouseOutBlur = function ( e ) {
7235 var tool = this.getTargetTool( e );
7236
7237 if ( this.pressed && this.pressed === tool ) {
7238 this.pressed.setActive( false );
7239 }
7240 };
7241
7242 /**
7243 * Get the closest tool to a jQuery.Event.
7244 *
7245 * Only tool links are considered, which prevents other elements in the tool such as popups from
7246 * triggering tool group interactions.
7247 *
7248 * @private
7249 * @param {jQuery.Event} e
7250 * @return {OO.ui.Tool|null} Tool, `null` if none was found
7251 */
7252 OO.ui.ToolGroup.prototype.getTargetTool = function ( e ) {
7253 var tool,
7254 $item = $( e.target ).closest( '.oo-ui-tool-link' );
7255
7256 if ( $item.length ) {
7257 tool = $item.parent().data( 'oo-ui-tool' );
7258 }
7259
7260 return tool && !tool.isDisabled() ? tool : null;
7261 };
7262
7263 /**
7264 * Handle tool registry register events.
7265 *
7266 * If a tool is registered after the group is created, we must repopulate the list to account for:
7267 *
7268 * - a tool being added that may be included
7269 * - a tool already included being overridden
7270 *
7271 * @param {string} name Symbolic name of tool
7272 */
7273 OO.ui.ToolGroup.prototype.onToolFactoryRegister = function () {
7274 this.populate();
7275 };
7276
7277 /**
7278 * Get the toolbar this group is in.
7279 *
7280 * @return {OO.ui.Toolbar} Toolbar of group
7281 */
7282 OO.ui.ToolGroup.prototype.getToolbar = function () {
7283 return this.toolbar;
7284 };
7285
7286 /**
7287 * Add and remove tools based on configuration.
7288 */
7289 OO.ui.ToolGroup.prototype.populate = function () {
7290 var i, len, name, tool,
7291 toolFactory = this.toolbar.getToolFactory(),
7292 names = {},
7293 add = [],
7294 remove = [],
7295 list = this.toolbar.getToolFactory().getTools(
7296 this.include, this.exclude, this.promote, this.demote
7297 );
7298
7299 // Build a list of needed tools
7300 for ( i = 0, len = list.length; i < len; i++ ) {
7301 name = list[ i ];
7302 if (
7303 // Tool exists
7304 toolFactory.lookup( name ) &&
7305 // Tool is available or is already in this group
7306 ( this.toolbar.isToolAvailable( name ) || this.tools[ name ] )
7307 ) {
7308 // Hack to prevent infinite recursion via ToolGroupTool. We need to reserve the tool before
7309 // creating it, but we can't call reserveTool() yet because we haven't created the tool.
7310 this.toolbar.tools[ name ] = true;
7311 tool = this.tools[ name ];
7312 if ( !tool ) {
7313 // Auto-initialize tools on first use
7314 this.tools[ name ] = tool = toolFactory.create( name, this );
7315 tool.updateTitle();
7316 }
7317 this.toolbar.reserveTool( tool );
7318 add.push( tool );
7319 names[ name ] = true;
7320 }
7321 }
7322 // Remove tools that are no longer needed
7323 for ( name in this.tools ) {
7324 if ( !names[ name ] ) {
7325 this.tools[ name ].destroy();
7326 this.toolbar.releaseTool( this.tools[ name ] );
7327 remove.push( this.tools[ name ] );
7328 delete this.tools[ name ];
7329 }
7330 }
7331 if ( remove.length ) {
7332 this.removeItems( remove );
7333 }
7334 // Update emptiness state
7335 if ( add.length ) {
7336 this.$element.removeClass( 'oo-ui-toolGroup-empty' );
7337 } else {
7338 this.$element.addClass( 'oo-ui-toolGroup-empty' );
7339 }
7340 // Re-add tools (moving existing ones to new locations)
7341 this.addItems( add );
7342 // Disabled state may depend on items
7343 this.updateDisabled();
7344 };
7345
7346 /**
7347 * Destroy tool group.
7348 */
7349 OO.ui.ToolGroup.prototype.destroy = function () {
7350 var name;
7351
7352 this.clearItems();
7353 this.toolbar.getToolFactory().disconnect( this );
7354 for ( name in this.tools ) {
7355 this.toolbar.releaseTool( this.tools[ name ] );
7356 this.tools[ name ].disconnect( this ).destroy();
7357 delete this.tools[ name ];
7358 }
7359 this.$element.remove();
7360 };
7361
7362 /**
7363 * MessageDialogs display a confirmation or alert message. By default, the rendered dialog box
7364 * consists of a header that contains the dialog title, a body with the message, and a footer that
7365 * contains any {@link OO.ui.ActionWidget action widgets}. The MessageDialog class is the only type
7366 * of {@link OO.ui.Dialog dialog} that is usually instantiated directly.
7367 *
7368 * There are two basic types of message dialogs, confirmation and alert:
7369 *
7370 * - **confirmation**: the dialog title describes what a progressive action will do and the message provides
7371 * more details about the consequences.
7372 * - **alert**: the dialog title describes which event occurred and the message provides more information
7373 * about why the event occurred.
7374 *
7375 * The MessageDialog class specifies two actions: ‘accept’, the primary
7376 * action (e.g., ‘ok’) and ‘reject,’ the safe action (e.g., ‘cancel’). Both will close the window,
7377 * passing along the selected action.
7378 *
7379 * For more information and examples, please see the [OOjs UI documentation on MediaWiki][1].
7380 *
7381 * @example
7382 * // Example: Creating and opening a message dialog window.
7383 * var messageDialog = new OO.ui.MessageDialog();
7384 *
7385 * // Create and append a window manager.
7386 * var windowManager = new OO.ui.WindowManager();
7387 * $( 'body' ).append( windowManager.$element );
7388 * windowManager.addWindows( [ messageDialog ] );
7389 * // Open the window.
7390 * windowManager.openWindow( messageDialog, {
7391 * title: 'Basic message dialog',
7392 * message: 'This is the message'
7393 * } );
7394 *
7395 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Message_Dialogs
7396 *
7397 * @class
7398 * @extends OO.ui.Dialog
7399 *
7400 * @constructor
7401 * @param {Object} [config] Configuration options
7402 */
7403 OO.ui.MessageDialog = function OoUiMessageDialog( config ) {
7404 // Parent constructor
7405 OO.ui.MessageDialog.super.call( this, config );
7406
7407 // Properties
7408 this.verticalActionLayout = null;
7409
7410 // Initialization
7411 this.$element.addClass( 'oo-ui-messageDialog' );
7412 };
7413
7414 /* Inheritance */
7415
7416 OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog );
7417
7418 /* Static Properties */
7419
7420 OO.ui.MessageDialog.static.name = 'message';
7421
7422 OO.ui.MessageDialog.static.size = 'small';
7423
7424 OO.ui.MessageDialog.static.verbose = false;
7425
7426 /**
7427 * Dialog title.
7428 *
7429 * The title of a confirmation dialog describes what a progressive action will do. The
7430 * title of an alert dialog describes which event occurred.
7431 *
7432 * @static
7433 * @inheritable
7434 * @property {jQuery|string|Function|null}
7435 */
7436 OO.ui.MessageDialog.static.title = null;
7437
7438 /**
7439 * The message displayed in the dialog body.
7440 *
7441 * A confirmation message describes the consequences of a progressive action. An alert
7442 * message describes why an event occurred.
7443 *
7444 * @static
7445 * @inheritable
7446 * @property {jQuery|string|Function|null}
7447 */
7448 OO.ui.MessageDialog.static.message = null;
7449
7450 OO.ui.MessageDialog.static.actions = [
7451 { action: 'accept', label: OO.ui.deferMsg( 'ooui-dialog-message-accept' ), flags: 'primary' },
7452 { action: 'reject', label: OO.ui.deferMsg( 'ooui-dialog-message-reject' ), flags: 'safe' }
7453 ];
7454
7455 /* Methods */
7456
7457 /**
7458 * @inheritdoc
7459 */
7460 OO.ui.MessageDialog.prototype.setManager = function ( manager ) {
7461 OO.ui.MessageDialog.super.prototype.setManager.call( this, manager );
7462
7463 // Events
7464 this.manager.connect( this, {
7465 resize: 'onResize'
7466 } );
7467
7468 return this;
7469 };
7470
7471 /**
7472 * @inheritdoc
7473 */
7474 OO.ui.MessageDialog.prototype.onActionResize = function ( action ) {
7475 this.fitActions();
7476 return OO.ui.MessageDialog.super.prototype.onActionResize.call( this, action );
7477 };
7478
7479 /**
7480 * Handle window resized events.
7481 *
7482 * @private
7483 */
7484 OO.ui.MessageDialog.prototype.onResize = function () {
7485 var dialog = this;
7486 dialog.fitActions();
7487 // Wait for CSS transition to finish and do it again :(
7488 setTimeout( function () {
7489 dialog.fitActions();
7490 }, 300 );
7491 };
7492
7493 /**
7494 * Toggle action layout between vertical and horizontal.
7495 *
7496 *
7497 * @private
7498 * @param {boolean} [value] Layout actions vertically, omit to toggle
7499 * @chainable
7500 */
7501 OO.ui.MessageDialog.prototype.toggleVerticalActionLayout = function ( value ) {
7502 value = value === undefined ? !this.verticalActionLayout : !!value;
7503
7504 if ( value !== this.verticalActionLayout ) {
7505 this.verticalActionLayout = value;
7506 this.$actions
7507 .toggleClass( 'oo-ui-messageDialog-actions-vertical', value )
7508 .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value );
7509 }
7510
7511 return this;
7512 };
7513
7514 /**
7515 * @inheritdoc
7516 */
7517 OO.ui.MessageDialog.prototype.getActionProcess = function ( action ) {
7518 if ( action ) {
7519 return new OO.ui.Process( function () {
7520 this.close( { action: action } );
7521 }, this );
7522 }
7523 return OO.ui.MessageDialog.super.prototype.getActionProcess.call( this, action );
7524 };
7525
7526 /**
7527 * @inheritdoc
7528 *
7529 * @param {Object} [data] Dialog opening data
7530 * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
7531 * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
7532 * @param {boolean} [data.verbose] Message is verbose and should be styled as a long message
7533 * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each
7534 * action item
7535 */
7536 OO.ui.MessageDialog.prototype.getSetupProcess = function ( data ) {
7537 data = data || {};
7538
7539 // Parent method
7540 return OO.ui.MessageDialog.super.prototype.getSetupProcess.call( this, data )
7541 .next( function () {
7542 this.title.setLabel(
7543 data.title !== undefined ? data.title : this.constructor.static.title
7544 );
7545 this.message.setLabel(
7546 data.message !== undefined ? data.message : this.constructor.static.message
7547 );
7548 this.message.$element.toggleClass(
7549 'oo-ui-messageDialog-message-verbose',
7550 data.verbose !== undefined ? data.verbose : this.constructor.static.verbose
7551 );
7552 }, this );
7553 };
7554
7555 /**
7556 * @inheritdoc
7557 */
7558 OO.ui.MessageDialog.prototype.getBodyHeight = function () {
7559 var bodyHeight, oldOverflow,
7560 $scrollable = this.container.$element;
7561
7562 oldOverflow = $scrollable[ 0 ].style.overflow;
7563 $scrollable[ 0 ].style.overflow = 'hidden';
7564
7565 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
7566
7567 bodyHeight = this.text.$element.outerHeight( true );
7568 $scrollable[ 0 ].style.overflow = oldOverflow;
7569
7570 return bodyHeight;
7571 };
7572
7573 /**
7574 * @inheritdoc
7575 */
7576 OO.ui.MessageDialog.prototype.setDimensions = function ( dim ) {
7577 var $scrollable = this.container.$element;
7578 OO.ui.MessageDialog.super.prototype.setDimensions.call( this, dim );
7579
7580 // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
7581 // Need to do it after transition completes (250ms), add 50ms just in case.
7582 setTimeout( function () {
7583 var oldOverflow = $scrollable[ 0 ].style.overflow;
7584 $scrollable[ 0 ].style.overflow = 'hidden';
7585
7586 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
7587
7588 $scrollable[ 0 ].style.overflow = oldOverflow;
7589 }, 300 );
7590
7591 return this;
7592 };
7593
7594 /**
7595 * @inheritdoc
7596 */
7597 OO.ui.MessageDialog.prototype.initialize = function () {
7598 // Parent method
7599 OO.ui.MessageDialog.super.prototype.initialize.call( this );
7600
7601 // Properties
7602 this.$actions = $( '<div>' );
7603 this.container = new OO.ui.PanelLayout( {
7604 scrollable: true, classes: [ 'oo-ui-messageDialog-container' ]
7605 } );
7606 this.text = new OO.ui.PanelLayout( {
7607 padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ]
7608 } );
7609 this.message = new OO.ui.LabelWidget( {
7610 classes: [ 'oo-ui-messageDialog-message' ]
7611 } );
7612
7613 // Initialization
7614 this.title.$element.addClass( 'oo-ui-messageDialog-title' );
7615 this.$content.addClass( 'oo-ui-messageDialog-content' );
7616 this.container.$element.append( this.text.$element );
7617 this.text.$element.append( this.title.$element, this.message.$element );
7618 this.$body.append( this.container.$element );
7619 this.$actions.addClass( 'oo-ui-messageDialog-actions' );
7620 this.$foot.append( this.$actions );
7621 };
7622
7623 /**
7624 * @inheritdoc
7625 */
7626 OO.ui.MessageDialog.prototype.attachActions = function () {
7627 var i, len, other, special, others;
7628
7629 // Parent method
7630 OO.ui.MessageDialog.super.prototype.attachActions.call( this );
7631
7632 special = this.actions.getSpecial();
7633 others = this.actions.getOthers();
7634 if ( special.safe ) {
7635 this.$actions.append( special.safe.$element );
7636 special.safe.toggleFramed( false );
7637 }
7638 if ( others.length ) {
7639 for ( i = 0, len = others.length; i < len; i++ ) {
7640 other = others[ i ];
7641 this.$actions.append( other.$element );
7642 other.toggleFramed( false );
7643 }
7644 }
7645 if ( special.primary ) {
7646 this.$actions.append( special.primary.$element );
7647 special.primary.toggleFramed( false );
7648 }
7649
7650 if ( !this.isOpening() ) {
7651 // If the dialog is currently opening, this will be called automatically soon.
7652 // This also calls #fitActions.
7653 this.updateSize();
7654 }
7655 };
7656
7657 /**
7658 * Fit action actions into columns or rows.
7659 *
7660 * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
7661 *
7662 * @private
7663 */
7664 OO.ui.MessageDialog.prototype.fitActions = function () {
7665 var i, len, action,
7666 previous = this.verticalActionLayout,
7667 actions = this.actions.get();
7668
7669 // Detect clipping
7670 this.toggleVerticalActionLayout( false );
7671 for ( i = 0, len = actions.length; i < len; i++ ) {
7672 action = actions[ i ];
7673 if ( action.$element.innerWidth() < action.$label.outerWidth( true ) ) {
7674 this.toggleVerticalActionLayout( true );
7675 break;
7676 }
7677 }
7678
7679 // Move the body out of the way of the foot
7680 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
7681
7682 if ( this.verticalActionLayout !== previous ) {
7683 // We changed the layout, window height might need to be updated.
7684 this.updateSize();
7685 }
7686 };
7687
7688 /**
7689 * ProcessDialog windows encapsulate a {@link OO.ui.Process process} and all of the code necessary
7690 * to complete it. If the process terminates with an error, a customizable {@link OO.ui.Error error
7691 * interface} alerts users to the trouble, permitting the user to dismiss the error and try again when
7692 * relevant. The ProcessDialog class is always extended and customized with the actions and content
7693 * required for each process.
7694 *
7695 * The process dialog box consists of a header that visually represents the ‘working’ state of long
7696 * processes with an animation. The header contains the dialog title as well as
7697 * two {@link OO.ui.ActionWidget action widgets}: a ‘safe’ action on the left (e.g., ‘Cancel’) and
7698 * a ‘primary’ action on the right (e.g., ‘Done’).
7699 *
7700 * Like other windows, the process dialog is managed by a {@link OO.ui.WindowManager window manager}.
7701 * Please see the [OOjs UI documentation on MediaWiki][1] for more information and examples.
7702 *
7703 * @example
7704 * // Example: Creating and opening a process dialog window.
7705 * function MyProcessDialog( config ) {
7706 * MyProcessDialog.super.call( this, config );
7707 * }
7708 * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
7709 *
7710 * MyProcessDialog.static.title = 'Process dialog';
7711 * MyProcessDialog.static.actions = [
7712 * { action: 'save', label: 'Done', flags: 'primary' },
7713 * { label: 'Cancel', flags: 'safe' }
7714 * ];
7715 *
7716 * MyProcessDialog.prototype.initialize = function () {
7717 * MyProcessDialog.super.prototype.initialize.apply( this, arguments );
7718 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
7719 * this.content.$element.append( '<p>This is a process dialog window. The header contains the title and two buttons: \'Cancel\' (a safe action) on the left and \'Done\' (a primary action) on the right.</p>' );
7720 * this.$body.append( this.content.$element );
7721 * };
7722 * MyProcessDialog.prototype.getActionProcess = function ( action ) {
7723 * var dialog = this;
7724 * if ( action ) {
7725 * return new OO.ui.Process( function () {
7726 * dialog.close( { action: action } );
7727 * } );
7728 * }
7729 * return MyProcessDialog.super.prototype.getActionProcess.call( this, action );
7730 * };
7731 *
7732 * var windowManager = new OO.ui.WindowManager();
7733 * $( 'body' ).append( windowManager.$element );
7734 *
7735 * var dialog = new MyProcessDialog();
7736 * windowManager.addWindows( [ dialog ] );
7737 * windowManager.openWindow( dialog );
7738 *
7739 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
7740 *
7741 * @abstract
7742 * @class
7743 * @extends OO.ui.Dialog
7744 *
7745 * @constructor
7746 * @param {Object} [config] Configuration options
7747 */
7748 OO.ui.ProcessDialog = function OoUiProcessDialog( config ) {
7749 // Parent constructor
7750 OO.ui.ProcessDialog.super.call( this, config );
7751
7752 // Initialization
7753 this.$element.addClass( 'oo-ui-processDialog' );
7754 };
7755
7756 /* Setup */
7757
7758 OO.inheritClass( OO.ui.ProcessDialog, OO.ui.Dialog );
7759
7760 /* Methods */
7761
7762 /**
7763 * Handle dismiss button click events.
7764 *
7765 * Hides errors.
7766 *
7767 * @private
7768 */
7769 OO.ui.ProcessDialog.prototype.onDismissErrorButtonClick = function () {
7770 this.hideErrors();
7771 };
7772
7773 /**
7774 * Handle retry button click events.
7775 *
7776 * Hides errors and then tries again.
7777 *
7778 * @private
7779 */
7780 OO.ui.ProcessDialog.prototype.onRetryButtonClick = function () {
7781 this.hideErrors();
7782 this.executeAction( this.currentAction );
7783 };
7784
7785 /**
7786 * @inheritdoc
7787 */
7788 OO.ui.ProcessDialog.prototype.onActionResize = function ( action ) {
7789 if ( this.actions.isSpecial( action ) ) {
7790 this.fitLabel();
7791 }
7792 return OO.ui.ProcessDialog.super.prototype.onActionResize.call( this, action );
7793 };
7794
7795 /**
7796 * @inheritdoc
7797 */
7798 OO.ui.ProcessDialog.prototype.initialize = function () {
7799 // Parent method
7800 OO.ui.ProcessDialog.super.prototype.initialize.call( this );
7801
7802 // Properties
7803 this.$navigation = $( '<div>' );
7804 this.$location = $( '<div>' );
7805 this.$safeActions = $( '<div>' );
7806 this.$primaryActions = $( '<div>' );
7807 this.$otherActions = $( '<div>' );
7808 this.dismissButton = new OO.ui.ButtonWidget( {
7809 label: OO.ui.msg( 'ooui-dialog-process-dismiss' )
7810 } );
7811 this.retryButton = new OO.ui.ButtonWidget();
7812 this.$errors = $( '<div>' );
7813 this.$errorsTitle = $( '<div>' );
7814
7815 // Events
7816 this.dismissButton.connect( this, { click: 'onDismissErrorButtonClick' } );
7817 this.retryButton.connect( this, { click: 'onRetryButtonClick' } );
7818
7819 // Initialization
7820 this.title.$element.addClass( 'oo-ui-processDialog-title' );
7821 this.$location
7822 .append( this.title.$element )
7823 .addClass( 'oo-ui-processDialog-location' );
7824 this.$safeActions.addClass( 'oo-ui-processDialog-actions-safe' );
7825 this.$primaryActions.addClass( 'oo-ui-processDialog-actions-primary' );
7826 this.$otherActions.addClass( 'oo-ui-processDialog-actions-other' );
7827 this.$errorsTitle
7828 .addClass( 'oo-ui-processDialog-errors-title' )
7829 .text( OO.ui.msg( 'ooui-dialog-process-error' ) );
7830 this.$errors
7831 .addClass( 'oo-ui-processDialog-errors oo-ui-element-hidden' )
7832 .append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element );
7833 this.$content
7834 .addClass( 'oo-ui-processDialog-content' )
7835 .append( this.$errors );
7836 this.$navigation
7837 .addClass( 'oo-ui-processDialog-navigation' )
7838 .append( this.$safeActions, this.$location, this.$primaryActions );
7839 this.$head.append( this.$navigation );
7840 this.$foot.append( this.$otherActions );
7841 };
7842
7843 /**
7844 * @inheritdoc
7845 */
7846 OO.ui.ProcessDialog.prototype.getActionWidgets = function ( actions ) {
7847 var i, len, widgets = [];
7848 for ( i = 0, len = actions.length; i < len; i++ ) {
7849 widgets.push(
7850 new OO.ui.ActionWidget( $.extend( { framed: true }, actions[ i ] ) )
7851 );
7852 }
7853 return widgets;
7854 };
7855
7856 /**
7857 * @inheritdoc
7858 */
7859 OO.ui.ProcessDialog.prototype.attachActions = function () {
7860 var i, len, other, special, others;
7861
7862 // Parent method
7863 OO.ui.ProcessDialog.super.prototype.attachActions.call( this );
7864
7865 special = this.actions.getSpecial();
7866 others = this.actions.getOthers();
7867 if ( special.primary ) {
7868 this.$primaryActions.append( special.primary.$element );
7869 }
7870 for ( i = 0, len = others.length; i < len; i++ ) {
7871 other = others[ i ];
7872 this.$otherActions.append( other.$element );
7873 }
7874 if ( special.safe ) {
7875 this.$safeActions.append( special.safe.$element );
7876 }
7877
7878 this.fitLabel();
7879 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
7880 };
7881
7882 /**
7883 * @inheritdoc
7884 */
7885 OO.ui.ProcessDialog.prototype.executeAction = function ( action ) {
7886 var process = this;
7887 return OO.ui.ProcessDialog.super.prototype.executeAction.call( this, action )
7888 .fail( function ( errors ) {
7889 process.showErrors( errors || [] );
7890 } );
7891 };
7892
7893 /**
7894 * Fit label between actions.
7895 *
7896 * @private
7897 * @chainable
7898 */
7899 OO.ui.ProcessDialog.prototype.fitLabel = function () {
7900 var width = Math.max(
7901 this.$safeActions.is( ':visible' ) ? this.$safeActions.width() : 0,
7902 this.$primaryActions.is( ':visible' ) ? this.$primaryActions.width() : 0
7903 );
7904 this.$location.css( { paddingLeft: width, paddingRight: width } );
7905
7906 return this;
7907 };
7908
7909 /**
7910 * Handle errors that occurred during accept or reject processes.
7911 *
7912 * @private
7913 * @param {OO.ui.Error[]|OO.ui.Error} errors Errors to be handled
7914 */
7915 OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
7916 var i, len, $item, actions,
7917 items = [],
7918 abilities = {},
7919 recoverable = true,
7920 warning = false;
7921
7922 if ( errors instanceof OO.ui.Error ) {
7923 errors = [ errors ];
7924 }
7925
7926 for ( i = 0, len = errors.length; i < len; i++ ) {
7927 if ( !errors[ i ].isRecoverable() ) {
7928 recoverable = false;
7929 }
7930 if ( errors[ i ].isWarning() ) {
7931 warning = true;
7932 }
7933 $item = $( '<div>' )
7934 .addClass( 'oo-ui-processDialog-error' )
7935 .append( errors[ i ].getMessage() );
7936 items.push( $item[ 0 ] );
7937 }
7938 this.$errorItems = $( items );
7939 if ( recoverable ) {
7940 abilities[this.currentAction] = true;
7941 // Copy the flags from the first matching action
7942 actions = this.actions.get( { actions: this.currentAction } );
7943 if ( actions.length ) {
7944 this.retryButton.clearFlags().setFlags( actions[0].getFlags() );
7945 }
7946 } else {
7947 abilities[this.currentAction] = false;
7948 this.actions.setAbilities( abilities );
7949 }
7950 if ( warning ) {
7951 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-continue' ) );
7952 } else {
7953 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-retry' ) );
7954 }
7955 this.retryButton.toggle( recoverable );
7956 this.$errorsTitle.after( this.$errorItems );
7957 this.$errors.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 );
7958 };
7959
7960 /**
7961 * Hide errors.
7962 *
7963 * @private
7964 */
7965 OO.ui.ProcessDialog.prototype.hideErrors = function () {
7966 this.$errors.addClass( 'oo-ui-element-hidden' );
7967 if ( this.$errorItems ) {
7968 this.$errorItems.remove();
7969 this.$errorItems = null;
7970 }
7971 };
7972
7973 /**
7974 * @inheritdoc
7975 */
7976 OO.ui.ProcessDialog.prototype.getTeardownProcess = function ( data ) {
7977 // Parent method
7978 return OO.ui.ProcessDialog.super.prototype.getTeardownProcess.call( this, data )
7979 .first( function () {
7980 // Make sure to hide errors
7981 this.hideErrors();
7982 }, this );
7983 };
7984
7985 /**
7986 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
7987 * which is a widget that is specified by reference before any optional configuration settings.
7988 *
7989 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
7990 *
7991 * - **left**: The label is placed before the field-widget and aligned with the left margin.
7992 * A left-alignment is used for forms with many fields.
7993 * - **right**: The label is placed before the field-widget and aligned to the right margin.
7994 * A right-alignment is used for long but familiar forms which users tab through,
7995 * verifying the current field with a quick glance at the label.
7996 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
7997 * that users fill out from top to bottom.
7998 * - **inline**: The label is placed after the field-widget and aligned to the left.
7999 * An inline-alignment is best used with checkboxes or radio buttons.
8000 *
8001 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
8002 * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
8003 *
8004 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
8005 * @class
8006 * @extends OO.ui.Layout
8007 * @mixins OO.ui.LabelElement
8008 *
8009 * @constructor
8010 * @param {OO.ui.Widget} fieldWidget Field widget
8011 * @param {Object} [config] Configuration options
8012 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
8013 * @cfg {string} [help] Help text. When help text is specified, a help icon will appear
8014 * in the upper-right corner of the rendered field.
8015 */
8016 OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
8017 // Allow passing positional parameters inside the config object
8018 if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
8019 config = fieldWidget;
8020 fieldWidget = config.fieldWidget;
8021 }
8022
8023 var hasInputWidget = fieldWidget instanceof OO.ui.InputWidget;
8024
8025 // Configuration initialization
8026 config = $.extend( { align: 'left' }, config );
8027
8028 // Parent constructor
8029 OO.ui.FieldLayout.super.call( this, config );
8030
8031 // Mixin constructors
8032 OO.ui.LabelElement.call( this, config );
8033
8034 // Properties
8035 this.fieldWidget = fieldWidget;
8036 this.$field = $( '<div>' );
8037 this.$body = $( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
8038 this.align = null;
8039 if ( config.help ) {
8040 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
8041 classes: [ 'oo-ui-fieldLayout-help' ],
8042 framed: false,
8043 icon: 'info'
8044 } );
8045
8046 this.popupButtonWidget.getPopup().$body.append(
8047 $( '<div>' )
8048 .text( config.help )
8049 .addClass( 'oo-ui-fieldLayout-help-content' )
8050 );
8051 this.$help = this.popupButtonWidget.$element;
8052 } else {
8053 this.$help = $( [] );
8054 }
8055
8056 // Events
8057 if ( hasInputWidget ) {
8058 this.$label.on( 'click', this.onLabelClick.bind( this ) );
8059 }
8060 this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
8061
8062 // Initialization
8063 this.$element
8064 .addClass( 'oo-ui-fieldLayout' )
8065 .append( this.$help, this.$body );
8066 this.$body.addClass( 'oo-ui-fieldLayout-body' );
8067 this.$field
8068 .addClass( 'oo-ui-fieldLayout-field' )
8069 .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget.isDisabled() )
8070 .append( this.fieldWidget.$element );
8071
8072 this.setAlignment( config.align );
8073 };
8074
8075 /* Setup */
8076
8077 OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
8078 OO.mixinClass( OO.ui.FieldLayout, OO.ui.LabelElement );
8079
8080 /* Methods */
8081
8082 /**
8083 * Handle field disable events.
8084 *
8085 * @private
8086 * @param {boolean} value Field is disabled
8087 */
8088 OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
8089 this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
8090 };
8091
8092 /**
8093 * Handle label mouse click events.
8094 *
8095 * @private
8096 * @param {jQuery.Event} e Mouse click event
8097 */
8098 OO.ui.FieldLayout.prototype.onLabelClick = function () {
8099 this.fieldWidget.simulateLabelClick();
8100 return false;
8101 };
8102
8103 /**
8104 * Get the widget contained by the field.
8105 *
8106 * @return {OO.ui.Widget} Field widget
8107 */
8108 OO.ui.FieldLayout.prototype.getField = function () {
8109 return this.fieldWidget;
8110 };
8111
8112 /**
8113 * Set the field alignment mode.
8114 *
8115 * @private
8116 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
8117 * @chainable
8118 */
8119 OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
8120 if ( value !== this.align ) {
8121 // Default to 'left'
8122 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) {
8123 value = 'left';
8124 }
8125 // Reorder elements
8126 if ( value === 'inline' ) {
8127 this.$body.append( this.$field, this.$label );
8128 } else {
8129 this.$body.append( this.$label, this.$field );
8130 }
8131 // Set classes. The following classes can be used here:
8132 // * oo-ui-fieldLayout-align-left
8133 // * oo-ui-fieldLayout-align-right
8134 // * oo-ui-fieldLayout-align-top
8135 // * oo-ui-fieldLayout-align-inline
8136 if ( this.align ) {
8137 this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
8138 }
8139 this.$element.addClass( 'oo-ui-fieldLayout-align-' + value );
8140 this.align = value;
8141 }
8142
8143 return this;
8144 };
8145
8146 /**
8147 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
8148 * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
8149 * is required and is specified before any optional configuration settings.
8150 *
8151 * Labels can be aligned in one of four ways:
8152 *
8153 * - **left**: The label is placed before the field-widget and aligned with the left margin.
8154 * A left-alignment is used for forms with many fields.
8155 * - **right**: The label is placed before the field-widget and aligned to the right margin.
8156 * A right-alignment is used for long but familiar forms which users tab through,
8157 * verifying the current field with a quick glance at the label.
8158 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
8159 * that users fill out from top to bottom.
8160 * - **inline**: The label is placed after the field-widget and aligned to the left.
8161 * An inline-alignment is best used with checkboxes or radio buttons.
8162 *
8163 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
8164 * text is specified.
8165 *
8166 * @example
8167 * // Example of an ActionFieldLayout
8168 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
8169 * new OO.ui.TextInputWidget( {
8170 * placeholder: 'Field widget'
8171 * } ),
8172 * new OO.ui.ButtonWidget( {
8173 * label: 'Button'
8174 * } ),
8175 * {
8176 * label: 'An ActionFieldLayout. This label is aligned top',
8177 * align: 'top',
8178 * help: 'This is help text'
8179 * }
8180 * );
8181 *
8182 * $( 'body' ).append( actionFieldLayout.$element );
8183 *
8184 *
8185 * @class
8186 * @extends OO.ui.FieldLayout
8187 *
8188 * @constructor
8189 * @param {OO.ui.Widget} fieldWidget Field widget
8190 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
8191 * @param {Object} [config] Configuration options
8192 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
8193 * @cfg {string} [help] Help text. When help text is specified, a help icon will appear in the
8194 * upper-right corner of the rendered field.
8195 */
8196 OO.ui.ActionFieldLayout = function OoUiActionFieldLayout( fieldWidget, buttonWidget, config ) {
8197 // Allow passing positional parameters inside the config object
8198 if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
8199 config = fieldWidget;
8200 fieldWidget = config.fieldWidget;
8201 buttonWidget = config.buttonWidget;
8202 }
8203
8204 // Configuration initialization
8205 config = $.extend( { align: 'left' }, config );
8206
8207 // Parent constructor
8208 OO.ui.ActionFieldLayout.super.call( this, fieldWidget, config );
8209
8210 // Properties
8211 this.fieldWidget = fieldWidget;
8212 this.buttonWidget = buttonWidget;
8213 this.$button = $( '<div>' )
8214 .addClass( 'oo-ui-actionFieldLayout-button' )
8215 .append( this.buttonWidget.$element );
8216 this.$input = $( '<div>' )
8217 .addClass( 'oo-ui-actionFieldLayout-input' )
8218 .append( this.fieldWidget.$element );
8219 this.$field
8220 .addClass( 'oo-ui-actionFieldLayout' )
8221 .append( this.$input, this.$button );
8222 };
8223
8224 /* Setup */
8225
8226 OO.inheritClass( OO.ui.ActionFieldLayout, OO.ui.FieldLayout );
8227
8228 /**
8229 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
8230 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
8231 * configured with a label as well. For more information and examples,
8232 * please see the [OOjs UI documentation on MediaWiki][1].
8233 *
8234 * @example
8235 * // Example of a fieldset layout
8236 * var input1 = new OO.ui.TextInputWidget( {
8237 * placeholder: 'A text input field'
8238 * } );
8239 *
8240 * var input2 = new OO.ui.TextInputWidget( {
8241 * placeholder: 'A text input field'
8242 * } );
8243 *
8244 * var fieldset = new OO.ui.FieldsetLayout( {
8245 * label: 'Example of a fieldset layout'
8246 * } );
8247 *
8248 * fieldset.addItems( [
8249 * new OO.ui.FieldLayout( input1, {
8250 * label: 'Field One'
8251 * } ),
8252 * new OO.ui.FieldLayout( input2, {
8253 * label: 'Field Two'
8254 * } )
8255 * ] );
8256 * $( 'body' ).append( fieldset.$element );
8257 *
8258 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
8259 *
8260 * @class
8261 * @extends OO.ui.Layout
8262 * @mixins OO.ui.IconElement
8263 * @mixins OO.ui.LabelElement
8264 * @mixins OO.ui.GroupElement
8265 *
8266 * @constructor
8267 * @param {Object} [config] Configuration options
8268 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
8269 */
8270 OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
8271 // Configuration initialization
8272 config = config || {};
8273
8274 // Parent constructor
8275 OO.ui.FieldsetLayout.super.call( this, config );
8276
8277 // Mixin constructors
8278 OO.ui.IconElement.call( this, config );
8279 OO.ui.LabelElement.call( this, config );
8280 OO.ui.GroupElement.call( this, config );
8281
8282 if ( config.help ) {
8283 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
8284 classes: [ 'oo-ui-fieldsetLayout-help' ],
8285 framed: false,
8286 icon: 'info'
8287 } );
8288
8289 this.popupButtonWidget.getPopup().$body.append(
8290 $( '<div>' )
8291 .text( config.help )
8292 .addClass( 'oo-ui-fieldsetLayout-help-content' )
8293 );
8294 this.$help = this.popupButtonWidget.$element;
8295 } else {
8296 this.$help = $( [] );
8297 }
8298
8299 // Initialization
8300 this.$element
8301 .addClass( 'oo-ui-fieldsetLayout' )
8302 .prepend( this.$help, this.$icon, this.$label, this.$group );
8303 if ( Array.isArray( config.items ) ) {
8304 this.addItems( config.items );
8305 }
8306 };
8307
8308 /* Setup */
8309
8310 OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
8311 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.IconElement );
8312 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.LabelElement );
8313 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.GroupElement );
8314
8315 /**
8316 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
8317 * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
8318 * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
8319 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
8320 *
8321 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
8322 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
8323 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
8324 * some fancier controls. Some controls have both regular and InputWidget variants, for example
8325 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
8326 * often have simplified APIs to match the capabilities of HTML forms.
8327 * See the [OOjs UI Inputs documentation on MediaWiki] [2] for more information about InputWidgets.
8328 *
8329 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Forms
8330 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8331 *
8332 * @example
8333 * // Example of a form layout that wraps a fieldset layout
8334 * var input1 = new OO.ui.TextInputWidget( {
8335 * placeholder: 'Username'
8336 * } );
8337 * var input2 = new OO.ui.TextInputWidget( {
8338 * placeholder: 'Password',
8339 * type: 'password'
8340 * } );
8341 * var submit = new OO.ui.ButtonInputWidget( {
8342 * label: 'Submit'
8343 * } );
8344 *
8345 * var fieldset = new OO.ui.FieldsetLayout( {
8346 * label: 'A form layout'
8347 * } );
8348 * fieldset.addItems( [
8349 * new OO.ui.FieldLayout( input1, {
8350 * label: 'Username',
8351 * align: 'top'
8352 * } ),
8353 * new OO.ui.FieldLayout( input2, {
8354 * label: 'Password',
8355 * align: 'top'
8356 * } ),
8357 * new OO.ui.FieldLayout( submit )
8358 * ] );
8359 * var form = new OO.ui.FormLayout( {
8360 * items: [ fieldset ],
8361 * action: '/api/formhandler',
8362 * method: 'get'
8363 * } )
8364 * $( 'body' ).append( form.$element );
8365 *
8366 * @class
8367 * @extends OO.ui.Layout
8368 * @mixins OO.ui.GroupElement
8369 *
8370 * @constructor
8371 * @param {Object} [config] Configuration options
8372 * @cfg {string} [method] HTML form `method` attribute
8373 * @cfg {string} [action] HTML form `action` attribute
8374 * @cfg {string} [enctype] HTML form `enctype` attribute
8375 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
8376 */
8377 OO.ui.FormLayout = function OoUiFormLayout( config ) {
8378 // Configuration initialization
8379 config = config || {};
8380
8381 // Parent constructor
8382 OO.ui.FormLayout.super.call( this, config );
8383
8384 // Mixin constructors
8385 OO.ui.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
8386
8387 // Events
8388 this.$element.on( 'submit', this.onFormSubmit.bind( this ) );
8389
8390 // Initialization
8391 this.$element
8392 .addClass( 'oo-ui-formLayout' )
8393 .attr( {
8394 method: config.method,
8395 action: config.action,
8396 enctype: config.enctype
8397 } );
8398 if ( Array.isArray( config.items ) ) {
8399 this.addItems( config.items );
8400 }
8401 };
8402
8403 /* Setup */
8404
8405 OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout );
8406 OO.mixinClass( OO.ui.FormLayout, OO.ui.GroupElement );
8407
8408 /* Events */
8409
8410 /**
8411 * A 'submit' event is emitted when the form is submitted.
8412 *
8413 * @event submit
8414 */
8415
8416 /* Static Properties */
8417
8418 OO.ui.FormLayout.static.tagName = 'form';
8419
8420 /* Methods */
8421
8422 /**
8423 * Handle form submit events.
8424 *
8425 * @private
8426 * @param {jQuery.Event} e Submit event
8427 * @fires submit
8428 */
8429 OO.ui.FormLayout.prototype.onFormSubmit = function () {
8430 this.emit( 'submit' );
8431 return false;
8432 };
8433
8434 /**
8435 * MenuLayouts combine a menu and a content {@link OO.ui.PanelLayout panel}. The menu is positioned relative to the content (after, before, top, or bottom)
8436 * and its size is customized with the #menuSize config. The content area will fill all remaining space.
8437 *
8438 * @example
8439 * var menuLayout = new OO.ui.MenuLayout( {
8440 * position: 'top'
8441 * } ),
8442 * menuPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
8443 * contentPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
8444 * select = new OO.ui.SelectWidget( {
8445 * items: [
8446 * new OO.ui.OptionWidget( {
8447 * data: 'before',
8448 * label: 'Before',
8449 * } ),
8450 * new OO.ui.OptionWidget( {
8451 * data: 'after',
8452 * label: 'After',
8453 * } ),
8454 * new OO.ui.OptionWidget( {
8455 * data: 'top',
8456 * label: 'Top',
8457 * } ),
8458 * new OO.ui.OptionWidget( {
8459 * data: 'bottom',
8460 * label: 'Bottom',
8461 * } )
8462 * ]
8463 * } ).on( 'select', function ( item ) {
8464 * menuLayout.setMenuPosition( item.getData() );
8465 * } );
8466 *
8467 * menuLayout.$menu.append(
8468 * menuPanel.$element.append( '<b>Menu panel</b>', select.$element )
8469 * );
8470 * menuLayout.$content.append(
8471 * contentPanel.$element.append( '<b>Content panel</b>', '<p>Note that the menu is positioned relative to the content panel: top, bottom, after, before.</p>')
8472 * );
8473 * $( 'body' ).append( menuLayout.$element );
8474 *
8475 * If menu size needs to be overridden, it can be accomplished using CSS similar to the snippet
8476 * below. MenuLayout's CSS will override the appropriate values with 'auto' or '0' to display the
8477 * menu correctly. If `menuPosition` is known beforehand, CSS rules corresponding to other positions
8478 * may be omitted.
8479 *
8480 * .oo-ui-menuLayout-menu {
8481 * height: 200px;
8482 * width: 200px;
8483 * }
8484 * .oo-ui-menuLayout-content {
8485 * top: 200px;
8486 * left: 200px;
8487 * right: 200px;
8488 * bottom: 200px;
8489 * }
8490 *
8491 * @class
8492 * @extends OO.ui.Layout
8493 *
8494 * @constructor
8495 * @param {Object} [config] Configuration options
8496 * @cfg {boolean} [showMenu=true] Show menu
8497 * @cfg {string} [menuPosition='before'] Position of menu: `top`, `after`, `bottom` or `before`
8498 */
8499 OO.ui.MenuLayout = function OoUiMenuLayout( config ) {
8500 // Configuration initialization
8501 config = $.extend( {
8502 showMenu: true,
8503 menuPosition: 'before'
8504 }, config );
8505
8506 // Parent constructor
8507 OO.ui.MenuLayout.super.call( this, config );
8508
8509 /**
8510 * Menu DOM node
8511 *
8512 * @property {jQuery}
8513 */
8514 this.$menu = $( '<div>' );
8515 /**
8516 * Content DOM node
8517 *
8518 * @property {jQuery}
8519 */
8520 this.$content = $( '<div>' );
8521
8522 // Initialization
8523 this.$menu
8524 .addClass( 'oo-ui-menuLayout-menu' );
8525 this.$content.addClass( 'oo-ui-menuLayout-content' );
8526 this.$element
8527 .addClass( 'oo-ui-menuLayout' )
8528 .append( this.$content, this.$menu );
8529 this.setMenuPosition( config.menuPosition );
8530 this.toggleMenu( config.showMenu );
8531 };
8532
8533 /* Setup */
8534
8535 OO.inheritClass( OO.ui.MenuLayout, OO.ui.Layout );
8536
8537 /* Methods */
8538
8539 /**
8540 * Toggle menu.
8541 *
8542 * @param {boolean} showMenu Show menu, omit to toggle
8543 * @chainable
8544 */
8545 OO.ui.MenuLayout.prototype.toggleMenu = function ( showMenu ) {
8546 showMenu = showMenu === undefined ? !this.showMenu : !!showMenu;
8547
8548 if ( this.showMenu !== showMenu ) {
8549 this.showMenu = showMenu;
8550 this.$element
8551 .toggleClass( 'oo-ui-menuLayout-showMenu', this.showMenu )
8552 .toggleClass( 'oo-ui-menuLayout-hideMenu', !this.showMenu );
8553 }
8554
8555 return this;
8556 };
8557
8558 /**
8559 * Check if menu is visible
8560 *
8561 * @return {boolean} Menu is visible
8562 */
8563 OO.ui.MenuLayout.prototype.isMenuVisible = function () {
8564 return this.showMenu;
8565 };
8566
8567 /**
8568 * Set menu position.
8569 *
8570 * @param {string} position Position of menu, either `top`, `after`, `bottom` or `before`
8571 * @throws {Error} If position value is not supported
8572 * @chainable
8573 */
8574 OO.ui.MenuLayout.prototype.setMenuPosition = function ( position ) {
8575 this.$element.removeClass( 'oo-ui-menuLayout-' + this.menuPosition );
8576 this.menuPosition = position;
8577 this.$element.addClass( 'oo-ui-menuLayout-' + position );
8578
8579 return this;
8580 };
8581
8582 /**
8583 * Get menu position.
8584 *
8585 * @return {string} Menu position
8586 */
8587 OO.ui.MenuLayout.prototype.getMenuPosition = function () {
8588 return this.menuPosition;
8589 };
8590
8591 /**
8592 * BookletLayouts contain {@link OO.ui.PageLayout page layouts} as well as
8593 * an {@link OO.ui.OutlineSelectWidget outline} that allows users to easily navigate
8594 * through the pages and select which one to display. By default, only one page is
8595 * displayed at a time and the outline is hidden. When a user navigates to a new page,
8596 * the booklet layout automatically focuses on the first focusable element, unless the
8597 * default setting is changed. Optionally, booklets can be configured to show
8598 * {@link OO.ui.OutlineControlsWidget controls} for adding, moving, and removing items.
8599 *
8600 * @example
8601 * // Example of a BookletLayout that contains two PageLayouts.
8602 *
8603 * function PageOneLayout( name, config ) {
8604 * PageOneLayout.super.call( this, name, config );
8605 * this.$element.append( '<p>First page</p><p>(This booklet has an outline, displayed on the left)</p>' );
8606 * }
8607 * OO.inheritClass( PageOneLayout, OO.ui.PageLayout );
8608 * PageOneLayout.prototype.setupOutlineItem = function () {
8609 * this.outlineItem.setLabel( 'Page One' );
8610 * };
8611 *
8612 * function PageTwoLayout( name, config ) {
8613 * PageTwoLayout.super.call( this, name, config );
8614 * this.$element.append( '<p>Second page</p>' );
8615 * }
8616 * OO.inheritClass( PageTwoLayout, OO.ui.PageLayout );
8617 * PageTwoLayout.prototype.setupOutlineItem = function () {
8618 * this.outlineItem.setLabel( 'Page Two' );
8619 * };
8620 *
8621 * var page1 = new PageOneLayout( 'one' ),
8622 * page2 = new PageTwoLayout( 'two' );
8623 *
8624 * var booklet = new OO.ui.BookletLayout( {
8625 * outlined: true
8626 * } );
8627 *
8628 * booklet.addPages ( [ page1, page2 ] );
8629 * $( 'body' ).append( booklet.$element );
8630 *
8631 * @class
8632 * @extends OO.ui.MenuLayout
8633 *
8634 * @constructor
8635 * @param {Object} [config] Configuration options
8636 * @cfg {boolean} [continuous=false] Show all pages, one after another
8637 * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new page is displayed.
8638 * @cfg {boolean} [outlined=false] Show the outline. The outline is used to navigate through the pages of the booklet.
8639 * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
8640 */
8641 OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
8642 // Configuration initialization
8643 config = config || {};
8644
8645 // Parent constructor
8646 OO.ui.BookletLayout.super.call( this, config );
8647
8648 // Properties
8649 this.currentPageName = null;
8650 this.pages = {};
8651 this.ignoreFocus = false;
8652 this.stackLayout = new OO.ui.StackLayout( { continuous: !!config.continuous } );
8653 this.$content.append( this.stackLayout.$element );
8654 this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
8655 this.outlineVisible = false;
8656 this.outlined = !!config.outlined;
8657 if ( this.outlined ) {
8658 this.editable = !!config.editable;
8659 this.outlineControlsWidget = null;
8660 this.outlineSelectWidget = new OO.ui.OutlineSelectWidget();
8661 this.outlinePanel = new OO.ui.PanelLayout( { scrollable: true } );
8662 this.$menu.append( this.outlinePanel.$element );
8663 this.outlineVisible = true;
8664 if ( this.editable ) {
8665 this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
8666 this.outlineSelectWidget
8667 );
8668 }
8669 }
8670 this.toggleMenu( this.outlined );
8671
8672 // Events
8673 this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
8674 if ( this.outlined ) {
8675 this.outlineSelectWidget.connect( this, { select: 'onOutlineSelectWidgetSelect' } );
8676 }
8677 if ( this.autoFocus ) {
8678 // Event 'focus' does not bubble, but 'focusin' does
8679 this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
8680 }
8681
8682 // Initialization
8683 this.$element.addClass( 'oo-ui-bookletLayout' );
8684 this.stackLayout.$element.addClass( 'oo-ui-bookletLayout-stackLayout' );
8685 if ( this.outlined ) {
8686 this.outlinePanel.$element
8687 .addClass( 'oo-ui-bookletLayout-outlinePanel' )
8688 .append( this.outlineSelectWidget.$element );
8689 if ( this.editable ) {
8690 this.outlinePanel.$element
8691 .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
8692 .append( this.outlineControlsWidget.$element );
8693 }
8694 }
8695 };
8696
8697 /* Setup */
8698
8699 OO.inheritClass( OO.ui.BookletLayout, OO.ui.MenuLayout );
8700
8701 /* Events */
8702
8703 /**
8704 * A 'set' event is emitted when a page is {@link #setPage set} to be displayed by the booklet layout.
8705 * @event set
8706 * @param {OO.ui.PageLayout} page Current page
8707 */
8708
8709 /**
8710 * An 'add' event is emitted when pages are {@link #addPages added} to the booklet layout.
8711 *
8712 * @event add
8713 * @param {OO.ui.PageLayout[]} page Added pages
8714 * @param {number} index Index pages were added at
8715 */
8716
8717 /**
8718 * A 'remove' event is emitted when pages are {@link #clearPages cleared} or
8719 * {@link #removePages removed} from the booklet.
8720 *
8721 * @event remove
8722 * @param {OO.ui.PageLayout[]} pages Removed pages
8723 */
8724
8725 /* Methods */
8726
8727 /**
8728 * Handle stack layout focus.
8729 *
8730 * @private
8731 * @param {jQuery.Event} e Focusin event
8732 */
8733 OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) {
8734 var name, $target;
8735
8736 // Find the page that an element was focused within
8737 $target = $( e.target ).closest( '.oo-ui-pageLayout' );
8738 for ( name in this.pages ) {
8739 // Check for page match, exclude current page to find only page changes
8740 if ( this.pages[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentPageName ) {
8741 this.setPage( name );
8742 break;
8743 }
8744 }
8745 };
8746
8747 /**
8748 * Handle stack layout set events.
8749 *
8750 * @private
8751 * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
8752 */
8753 OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
8754 var layout = this;
8755 if ( page ) {
8756 page.scrollElementIntoView( { complete: function () {
8757 if ( layout.autoFocus ) {
8758 layout.focus();
8759 }
8760 } } );
8761 }
8762 };
8763
8764 /**
8765 * Focus the first input in the current page.
8766 *
8767 * If no page is selected, the first selectable page will be selected.
8768 * If the focus is already in an element on the current page, nothing will happen.
8769 * @param {number} [itemIndex] A specific item to focus on
8770 */
8771 OO.ui.BookletLayout.prototype.focus = function ( itemIndex ) {
8772 var $input, page,
8773 items = this.stackLayout.getItems();
8774
8775 if ( itemIndex !== undefined && items[ itemIndex ] ) {
8776 page = items[ itemIndex ];
8777 } else {
8778 page = this.stackLayout.getCurrentItem();
8779 }
8780
8781 if ( !page && this.outlined ) {
8782 this.selectFirstSelectablePage();
8783 page = this.stackLayout.getCurrentItem();
8784 }
8785 if ( !page ) {
8786 return;
8787 }
8788 // Only change the focus if is not already in the current page
8789 if ( !page.$element.find( ':focus' ).length ) {
8790 $input = page.$element.find( ':input:first' );
8791 if ( $input.length ) {
8792 $input[ 0 ].focus();
8793 }
8794 }
8795 };
8796
8797 /**
8798 * Find the first focusable input in the booklet layout and focus
8799 * on it.
8800 */
8801 OO.ui.BookletLayout.prototype.focusFirstFocusable = function () {
8802 var i, len,
8803 found = false,
8804 items = this.stackLayout.getItems(),
8805 checkAndFocus = function () {
8806 if ( OO.ui.isFocusableElement( $( this ) ) ) {
8807 $( this ).focus();
8808 found = true;
8809 return false;
8810 }
8811 };
8812
8813 for ( i = 0, len = items.length; i < len; i++ ) {
8814 if ( found ) {
8815 break;
8816 }
8817 // Find all potentially focusable elements in the item
8818 // and check if they are focusable
8819 items[i].$element
8820 .find( 'input, select, textarea, button, object' )
8821 /* jshint loopfunc:true */
8822 .each( checkAndFocus );
8823 }
8824 };
8825
8826 /**
8827 * Handle outline widget select events.
8828 *
8829 * @private
8830 * @param {OO.ui.OptionWidget|null} item Selected item
8831 */
8832 OO.ui.BookletLayout.prototype.onOutlineSelectWidgetSelect = function ( item ) {
8833 if ( item ) {
8834 this.setPage( item.getData() );
8835 }
8836 };
8837
8838 /**
8839 * Check if booklet has an outline.
8840 *
8841 * @return {boolean} Booklet has an outline
8842 */
8843 OO.ui.BookletLayout.prototype.isOutlined = function () {
8844 return this.outlined;
8845 };
8846
8847 /**
8848 * Check if booklet has editing controls.
8849 *
8850 * @return {boolean} Booklet is editable
8851 */
8852 OO.ui.BookletLayout.prototype.isEditable = function () {
8853 return this.editable;
8854 };
8855
8856 /**
8857 * Check if booklet has a visible outline.
8858 *
8859 * @return {boolean} Outline is visible
8860 */
8861 OO.ui.BookletLayout.prototype.isOutlineVisible = function () {
8862 return this.outlined && this.outlineVisible;
8863 };
8864
8865 /**
8866 * Hide or show the outline.
8867 *
8868 * @param {boolean} [show] Show outline, omit to invert current state
8869 * @chainable
8870 */
8871 OO.ui.BookletLayout.prototype.toggleOutline = function ( show ) {
8872 if ( this.outlined ) {
8873 show = show === undefined ? !this.outlineVisible : !!show;
8874 this.outlineVisible = show;
8875 this.toggleMenu( show );
8876 }
8877
8878 return this;
8879 };
8880
8881 /**
8882 * Get the page closest to the specified page.
8883 *
8884 * @param {OO.ui.PageLayout} page Page to use as a reference point
8885 * @return {OO.ui.PageLayout|null} Page closest to the specified page
8886 */
8887 OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) {
8888 var next, prev, level,
8889 pages = this.stackLayout.getItems(),
8890 index = $.inArray( page, pages );
8891
8892 if ( index !== -1 ) {
8893 next = pages[ index + 1 ];
8894 prev = pages[ index - 1 ];
8895 // Prefer adjacent pages at the same level
8896 if ( this.outlined ) {
8897 level = this.outlineSelectWidget.getItemFromData( page.getName() ).getLevel();
8898 if (
8899 prev &&
8900 level === this.outlineSelectWidget.getItemFromData( prev.getName() ).getLevel()
8901 ) {
8902 return prev;
8903 }
8904 if (
8905 next &&
8906 level === this.outlineSelectWidget.getItemFromData( next.getName() ).getLevel()
8907 ) {
8908 return next;
8909 }
8910 }
8911 }
8912 return prev || next || null;
8913 };
8914
8915 /**
8916 * Get the outline widget.
8917 *
8918 * If the booklet is not outlined, the method will return `null`.
8919 *
8920 * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if the booklet is not outlined
8921 */
8922 OO.ui.BookletLayout.prototype.getOutline = function () {
8923 return this.outlineSelectWidget;
8924 };
8925
8926 /**
8927 * Get the outline controls widget.
8928 *
8929 * If the outline is not editable, the method will return `null`.
8930 *
8931 * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
8932 */
8933 OO.ui.BookletLayout.prototype.getOutlineControls = function () {
8934 return this.outlineControlsWidget;
8935 };
8936
8937 /**
8938 * Get a page by its symbolic name.
8939 *
8940 * @param {string} name Symbolic name of page
8941 * @return {OO.ui.PageLayout|undefined} Page, if found
8942 */
8943 OO.ui.BookletLayout.prototype.getPage = function ( name ) {
8944 return this.pages[ name ];
8945 };
8946
8947 /**
8948 * Get the current page.
8949 *
8950 * @return {OO.ui.PageLayout|undefined} Current page, if found
8951 */
8952 OO.ui.BookletLayout.prototype.getCurrentPage = function () {
8953 var name = this.getCurrentPageName();
8954 return name ? this.getPage( name ) : undefined;
8955 };
8956
8957 /**
8958 * Get the symbolic name of the current page.
8959 *
8960 * @return {string|null} Symbolic name of the current page
8961 */
8962 OO.ui.BookletLayout.prototype.getCurrentPageName = function () {
8963 return this.currentPageName;
8964 };
8965
8966 /**
8967 * Add pages to the booklet layout
8968 *
8969 * When pages are added with the same names as existing pages, the existing pages will be
8970 * automatically removed before the new pages are added.
8971 *
8972 * @param {OO.ui.PageLayout[]} pages Pages to add
8973 * @param {number} index Index of the insertion point
8974 * @fires add
8975 * @chainable
8976 */
8977 OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) {
8978 var i, len, name, page, item, currentIndex,
8979 stackLayoutPages = this.stackLayout.getItems(),
8980 remove = [],
8981 items = [];
8982
8983 // Remove pages with same names
8984 for ( i = 0, len = pages.length; i < len; i++ ) {
8985 page = pages[ i ];
8986 name = page.getName();
8987
8988 if ( Object.prototype.hasOwnProperty.call( this.pages, name ) ) {
8989 // Correct the insertion index
8990 currentIndex = $.inArray( this.pages[ name ], stackLayoutPages );
8991 if ( currentIndex !== -1 && currentIndex + 1 < index ) {
8992 index--;
8993 }
8994 remove.push( this.pages[ name ] );
8995 }
8996 }
8997 if ( remove.length ) {
8998 this.removePages( remove );
8999 }
9000
9001 // Add new pages
9002 for ( i = 0, len = pages.length; i < len; i++ ) {
9003 page = pages[ i ];
9004 name = page.getName();
9005 this.pages[ page.getName() ] = page;
9006 if ( this.outlined ) {
9007 item = new OO.ui.OutlineOptionWidget( { data: name } );
9008 page.setOutlineItem( item );
9009 items.push( item );
9010 }
9011 }
9012
9013 if ( this.outlined && items.length ) {
9014 this.outlineSelectWidget.addItems( items, index );
9015 this.selectFirstSelectablePage();
9016 }
9017 this.stackLayout.addItems( pages, index );
9018 this.emit( 'add', pages, index );
9019
9020 return this;
9021 };
9022
9023 /**
9024 * Remove the specified pages from the booklet layout.
9025 *
9026 * To remove all pages from the booklet, you may wish to use the #clearPages method instead.
9027 *
9028 * @param {OO.ui.PageLayout[]} pages An array of pages to remove
9029 * @fires remove
9030 * @chainable
9031 */
9032 OO.ui.BookletLayout.prototype.removePages = function ( pages ) {
9033 var i, len, name, page,
9034 items = [];
9035
9036 for ( i = 0, len = pages.length; i < len; i++ ) {
9037 page = pages[ i ];
9038 name = page.getName();
9039 delete this.pages[ name ];
9040 if ( this.outlined ) {
9041 items.push( this.outlineSelectWidget.getItemFromData( name ) );
9042 page.setOutlineItem( null );
9043 }
9044 }
9045 if ( this.outlined && items.length ) {
9046 this.outlineSelectWidget.removeItems( items );
9047 this.selectFirstSelectablePage();
9048 }
9049 this.stackLayout.removeItems( pages );
9050 this.emit( 'remove', pages );
9051
9052 return this;
9053 };
9054
9055 /**
9056 * Clear all pages from the booklet layout.
9057 *
9058 * To remove only a subset of pages from the booklet, use the #removePages method.
9059 *
9060 * @fires remove
9061 * @chainable
9062 */
9063 OO.ui.BookletLayout.prototype.clearPages = function () {
9064 var i, len,
9065 pages = this.stackLayout.getItems();
9066
9067 this.pages = {};
9068 this.currentPageName = null;
9069 if ( this.outlined ) {
9070 this.outlineSelectWidget.clearItems();
9071 for ( i = 0, len = pages.length; i < len; i++ ) {
9072 pages[ i ].setOutlineItem( null );
9073 }
9074 }
9075 this.stackLayout.clearItems();
9076
9077 this.emit( 'remove', pages );
9078
9079 return this;
9080 };
9081
9082 /**
9083 * Set the current page by symbolic name.
9084 *
9085 * @fires set
9086 * @param {string} name Symbolic name of page
9087 */
9088 OO.ui.BookletLayout.prototype.setPage = function ( name ) {
9089 var selectedItem,
9090 $focused,
9091 page = this.pages[ name ];
9092
9093 if ( name !== this.currentPageName ) {
9094 if ( this.outlined ) {
9095 selectedItem = this.outlineSelectWidget.getSelectedItem();
9096 if ( selectedItem && selectedItem.getData() !== name ) {
9097 this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getItemFromData( name ) );
9098 }
9099 }
9100 if ( page ) {
9101 if ( this.currentPageName && this.pages[ this.currentPageName ] ) {
9102 this.pages[ this.currentPageName ].setActive( false );
9103 // Blur anything focused if the next page doesn't have anything focusable - this
9104 // is not needed if the next page has something focusable because once it is focused
9105 // this blur happens automatically
9106 if ( this.autoFocus && !page.$element.find( ':input' ).length ) {
9107 $focused = this.pages[ this.currentPageName ].$element.find( ':focus' );
9108 if ( $focused.length ) {
9109 $focused[ 0 ].blur();
9110 }
9111 }
9112 }
9113 this.currentPageName = name;
9114 this.stackLayout.setItem( page );
9115 page.setActive( true );
9116 this.emit( 'set', page );
9117 }
9118 }
9119 };
9120
9121 /**
9122 * Select the first selectable page.
9123 *
9124 * @chainable
9125 */
9126 OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () {
9127 if ( !this.outlineSelectWidget.getSelectedItem() ) {
9128 this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getFirstSelectableItem() );
9129 }
9130
9131 return this;
9132 };
9133
9134 /**
9135 * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
9136 * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
9137 *
9138 * @example
9139 * // Example of a panel layout
9140 * var panel = new OO.ui.PanelLayout( {
9141 * expanded: false,
9142 * framed: true,
9143 * padded: true,
9144 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
9145 * } );
9146 * $( 'body' ).append( panel.$element );
9147 *
9148 * @class
9149 * @extends OO.ui.Layout
9150 *
9151 * @constructor
9152 * @param {Object} [config] Configuration options
9153 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
9154 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
9155 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
9156 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
9157 */
9158 OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
9159 // Configuration initialization
9160 config = $.extend( {
9161 scrollable: false,
9162 padded: false,
9163 expanded: true,
9164 framed: false
9165 }, config );
9166
9167 // Parent constructor
9168 OO.ui.PanelLayout.super.call( this, config );
9169
9170 // Initialization
9171 this.$element.addClass( 'oo-ui-panelLayout' );
9172 if ( config.scrollable ) {
9173 this.$element.addClass( 'oo-ui-panelLayout-scrollable' );
9174 }
9175 if ( config.padded ) {
9176 this.$element.addClass( 'oo-ui-panelLayout-padded' );
9177 }
9178 if ( config.expanded ) {
9179 this.$element.addClass( 'oo-ui-panelLayout-expanded' );
9180 }
9181 if ( config.framed ) {
9182 this.$element.addClass( 'oo-ui-panelLayout-framed' );
9183 }
9184 };
9185
9186 /* Setup */
9187
9188 OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
9189
9190 /**
9191 * PageLayouts are used within {@link OO.ui.BookletLayout booklet layouts} to create pages that users can select and display
9192 * from the booklet's optional {@link OO.ui.OutlineSelectWidget outline} navigation. Pages are usually not instantiated directly,
9193 * rather extended to include the required content and functionality.
9194 *
9195 * Each page must have a unique symbolic name, which is passed to the constructor. In addition, the page's outline
9196 * item is customized (with a label, outline level, etc.) using the #setupOutlineItem method. See
9197 * {@link OO.ui.BookletLayout BookletLayout} for an example.
9198 *
9199 * @class
9200 * @extends OO.ui.PanelLayout
9201 *
9202 * @constructor
9203 * @param {string} name Unique symbolic name of page
9204 * @param {Object} [config] Configuration options
9205 */
9206 OO.ui.PageLayout = function OoUiPageLayout( name, config ) {
9207 // Allow passing positional parameters inside the config object
9208 if ( OO.isPlainObject( name ) && config === undefined ) {
9209 config = name;
9210 name = config.name;
9211 }
9212
9213 // Configuration initialization
9214 config = $.extend( { scrollable: true }, config );
9215
9216 // Parent constructor
9217 OO.ui.PageLayout.super.call( this, config );
9218
9219 // Properties
9220 this.name = name;
9221 this.outlineItem = null;
9222 this.active = false;
9223
9224 // Initialization
9225 this.$element.addClass( 'oo-ui-pageLayout' );
9226 };
9227
9228 /* Setup */
9229
9230 OO.inheritClass( OO.ui.PageLayout, OO.ui.PanelLayout );
9231
9232 /* Events */
9233
9234 /**
9235 * An 'active' event is emitted when the page becomes active. Pages become active when they are
9236 * shown in a booklet layout that is configured to display only one page at a time.
9237 *
9238 * @event active
9239 * @param {boolean} active Page is active
9240 */
9241
9242 /* Methods */
9243
9244 /**
9245 * Get the symbolic name of the page.
9246 *
9247 * @return {string} Symbolic name of page
9248 */
9249 OO.ui.PageLayout.prototype.getName = function () {
9250 return this.name;
9251 };
9252
9253 /**
9254 * Check if page is active.
9255 *
9256 * Pages become active when they are shown in a {@link OO.ui.BookletLayout booklet layout} that is configured to display
9257 * only one page at a time. Additional CSS is applied to the page's outline item to reflect the active state.
9258 *
9259 * @return {boolean} Page is active
9260 */
9261 OO.ui.PageLayout.prototype.isActive = function () {
9262 return this.active;
9263 };
9264
9265 /**
9266 * Get outline item.
9267 *
9268 * The outline item allows users to access the page from the booklet's outline
9269 * navigation. The outline item itself can be customized (with a label, level, etc.) using the #setupOutlineItem method.
9270 *
9271 * @return {OO.ui.OutlineOptionWidget|null} Outline option widget
9272 */
9273 OO.ui.PageLayout.prototype.getOutlineItem = function () {
9274 return this.outlineItem;
9275 };
9276
9277 /**
9278 * Set or unset the outline item.
9279 *
9280 * Specify an {@link OO.ui.OutlineOptionWidget outline option} to set it,
9281 * or `null` to clear the outline item. To customize the outline item itself (e.g., to set a label or outline
9282 * level), use #setupOutlineItem instead of this method.
9283 *
9284 * @param {OO.ui.OutlineOptionWidget|null} outlineItem Outline option widget, null to clear
9285 * @chainable
9286 */
9287 OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) {
9288 this.outlineItem = outlineItem || null;
9289 if ( outlineItem ) {
9290 this.setupOutlineItem();
9291 }
9292 return this;
9293 };
9294
9295 /**
9296 * Set up the outline item.
9297 *
9298 * Use this method to customize the outline item (e.g., to add a label or outline level). To set or unset
9299 * the outline item itself (with an {@link OO.ui.OutlineOptionWidget outline option} or `null`), use
9300 * the #setOutlineItem method instead.
9301 *
9302 * @param {OO.ui.OutlineOptionWidget} outlineItem Outline option widget to set up
9303 * @chainable
9304 */
9305 OO.ui.PageLayout.prototype.setupOutlineItem = function () {
9306 return this;
9307 };
9308
9309 /**
9310 * Set the page to its 'active' state.
9311 *
9312 * Pages become active when they are shown in a booklet layout that is configured to display only one page at a time. Additional
9313 * CSS is applied to the outline item to reflect the page's active state. Outside of the booklet
9314 * context, setting the active state on a page does nothing.
9315 *
9316 * @param {boolean} value Page is active
9317 * @fires active
9318 */
9319 OO.ui.PageLayout.prototype.setActive = function ( active ) {
9320 active = !!active;
9321
9322 if ( active !== this.active ) {
9323 this.active = active;
9324 this.$element.toggleClass( 'oo-ui-pageLayout-active', active );
9325 this.emit( 'active', this.active );
9326 }
9327 };
9328
9329 /**
9330 * StackLayouts contain a series of {@link OO.ui.PanelLayout panel layouts}. By default, only one panel is displayed
9331 * at a time, though the stack layout can also be configured to show all contained panels, one after another,
9332 * by setting the #continuous option to 'true'.
9333 *
9334 * @example
9335 * // A stack layout with two panels, configured to be displayed continously
9336 * var myStack = new OO.ui.StackLayout( {
9337 * items: [
9338 * new OO.ui.PanelLayout( {
9339 * $content: $( '<p>Panel One</p>' ),
9340 * padded: true,
9341 * framed: true
9342 * } ),
9343 * new OO.ui.PanelLayout( {
9344 * $content: $( '<p>Panel Two</p>' ),
9345 * padded: true,
9346 * framed: true
9347 * } )
9348 * ],
9349 * continuous: true
9350 * } );
9351 * $( 'body' ).append( myStack.$element );
9352 *
9353 * @class
9354 * @extends OO.ui.PanelLayout
9355 * @mixins OO.ui.GroupElement
9356 *
9357 * @constructor
9358 * @param {Object} [config] Configuration options
9359 * @cfg {boolean} [continuous=false] Show all panels, one after another. By default, only one panel is displayed at a time.
9360 * @cfg {OO.ui.Layout[]} [items] Panel layouts to add to the stack layout.
9361 */
9362 OO.ui.StackLayout = function OoUiStackLayout( config ) {
9363 // Configuration initialization
9364 config = $.extend( { scrollable: true }, config );
9365
9366 // Parent constructor
9367 OO.ui.StackLayout.super.call( this, config );
9368
9369 // Mixin constructors
9370 OO.ui.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
9371
9372 // Properties
9373 this.currentItem = null;
9374 this.continuous = !!config.continuous;
9375
9376 // Initialization
9377 this.$element.addClass( 'oo-ui-stackLayout' );
9378 if ( this.continuous ) {
9379 this.$element.addClass( 'oo-ui-stackLayout-continuous' );
9380 }
9381 if ( Array.isArray( config.items ) ) {
9382 this.addItems( config.items );
9383 }
9384 };
9385
9386 /* Setup */
9387
9388 OO.inheritClass( OO.ui.StackLayout, OO.ui.PanelLayout );
9389 OO.mixinClass( OO.ui.StackLayout, OO.ui.GroupElement );
9390
9391 /* Events */
9392
9393 /**
9394 * A 'set' event is emitted when panels are {@link #addItems added}, {@link #removeItems removed},
9395 * {@link #clearItems cleared} or {@link #setItem displayed}.
9396 *
9397 * @event set
9398 * @param {OO.ui.Layout|null} item Current panel or `null` if no panel is shown
9399 */
9400
9401 /* Methods */
9402
9403 /**
9404 * Get the current panel.
9405 *
9406 * @return {OO.ui.Layout|null}
9407 */
9408 OO.ui.StackLayout.prototype.getCurrentItem = function () {
9409 return this.currentItem;
9410 };
9411
9412 /**
9413 * Unset the current item.
9414 *
9415 * @private
9416 * @param {OO.ui.StackLayout} layout
9417 * @fires set
9418 */
9419 OO.ui.StackLayout.prototype.unsetCurrentItem = function () {
9420 var prevItem = this.currentItem;
9421 if ( prevItem === null ) {
9422 return;
9423 }
9424
9425 this.currentItem = null;
9426 this.emit( 'set', null );
9427 };
9428
9429 /**
9430 * Add panel layouts to the stack layout.
9431 *
9432 * Panels will be added to the end of the stack layout array unless the optional index parameter specifies a different
9433 * insertion point. Adding a panel that is already in the stack will move it to the end of the array or the point specified
9434 * by the index.
9435 *
9436 * @param {OO.ui.Layout[]} items Panels to add
9437 * @param {number} [index] Index of the insertion point
9438 * @chainable
9439 */
9440 OO.ui.StackLayout.prototype.addItems = function ( items, index ) {
9441 // Update the visibility
9442 this.updateHiddenState( items, this.currentItem );
9443
9444 // Mixin method
9445 OO.ui.GroupElement.prototype.addItems.call( this, items, index );
9446
9447 if ( !this.currentItem && items.length ) {
9448 this.setItem( items[ 0 ] );
9449 }
9450
9451 return this;
9452 };
9453
9454 /**
9455 * Remove the specified panels from the stack layout.
9456 *
9457 * Removed panels are detached from the DOM, not removed, so that they may be reused. To remove all panels,
9458 * you may wish to use the #clearItems method instead.
9459 *
9460 * @param {OO.ui.Layout[]} items Panels to remove
9461 * @chainable
9462 * @fires set
9463 */
9464 OO.ui.StackLayout.prototype.removeItems = function ( items ) {
9465 // Mixin method
9466 OO.ui.GroupElement.prototype.removeItems.call( this, items );
9467
9468 if ( $.inArray( this.currentItem, items ) !== -1 ) {
9469 if ( this.items.length ) {
9470 this.setItem( this.items[ 0 ] );
9471 } else {
9472 this.unsetCurrentItem();
9473 }
9474 }
9475
9476 return this;
9477 };
9478
9479 /**
9480 * Clear all panels from the stack layout.
9481 *
9482 * Cleared panels are detached from the DOM, not removed, so that they may be reused. To remove only
9483 * a subset of panels, use the #removeItems method.
9484 *
9485 * @chainable
9486 * @fires set
9487 */
9488 OO.ui.StackLayout.prototype.clearItems = function () {
9489 this.unsetCurrentItem();
9490 OO.ui.GroupElement.prototype.clearItems.call( this );
9491
9492 return this;
9493 };
9494
9495 /**
9496 * Show the specified panel.
9497 *
9498 * If another panel is currently displayed, it will be hidden.
9499 *
9500 * @param {OO.ui.Layout} item Panel to show
9501 * @chainable
9502 * @fires set
9503 */
9504 OO.ui.StackLayout.prototype.setItem = function ( item ) {
9505 if ( item !== this.currentItem ) {
9506 this.updateHiddenState( this.items, item );
9507
9508 if ( $.inArray( item, this.items ) !== -1 ) {
9509 this.currentItem = item;
9510 this.emit( 'set', item );
9511 } else {
9512 this.unsetCurrentItem();
9513 }
9514 }
9515
9516 return this;
9517 };
9518
9519 /**
9520 * Update the visibility of all items in case of non-continuous view.
9521 *
9522 * Ensure all items are hidden except for the selected one.
9523 * This method does nothing when the stack is continuous.
9524 *
9525 * @private
9526 * @param {OO.ui.Layout[]} items Item list iterate over
9527 * @param {OO.ui.Layout} [selectedItem] Selected item to show
9528 */
9529 OO.ui.StackLayout.prototype.updateHiddenState = function ( items, selectedItem ) {
9530 var i, len;
9531
9532 if ( !this.continuous ) {
9533 for ( i = 0, len = items.length; i < len; i++ ) {
9534 if ( !selectedItem || selectedItem !== items[ i ] ) {
9535 items[ i ].$element.addClass( 'oo-ui-element-hidden' );
9536 }
9537 }
9538 if ( selectedItem ) {
9539 selectedItem.$element.removeClass( 'oo-ui-element-hidden' );
9540 }
9541 }
9542 };
9543
9544 /**
9545 * Horizontal bar layout of tools as icon buttons.
9546 *
9547 * @class
9548 * @extends OO.ui.ToolGroup
9549 *
9550 * @constructor
9551 * @param {OO.ui.Toolbar} toolbar
9552 * @param {Object} [config] Configuration options
9553 */
9554 OO.ui.BarToolGroup = function OoUiBarToolGroup( toolbar, config ) {
9555 // Allow passing positional parameters inside the config object
9556 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
9557 config = toolbar;
9558 toolbar = config.toolbar;
9559 }
9560
9561 // Parent constructor
9562 OO.ui.BarToolGroup.super.call( this, toolbar, config );
9563
9564 // Initialization
9565 this.$element.addClass( 'oo-ui-barToolGroup' );
9566 };
9567
9568 /* Setup */
9569
9570 OO.inheritClass( OO.ui.BarToolGroup, OO.ui.ToolGroup );
9571
9572 /* Static Properties */
9573
9574 OO.ui.BarToolGroup.static.titleTooltips = true;
9575
9576 OO.ui.BarToolGroup.static.accelTooltips = true;
9577
9578 OO.ui.BarToolGroup.static.name = 'bar';
9579
9580 /**
9581 * Popup list of tools with an icon and optional label.
9582 *
9583 * @abstract
9584 * @class
9585 * @extends OO.ui.ToolGroup
9586 * @mixins OO.ui.IconElement
9587 * @mixins OO.ui.IndicatorElement
9588 * @mixins OO.ui.LabelElement
9589 * @mixins OO.ui.TitledElement
9590 * @mixins OO.ui.ClippableElement
9591 * @mixins OO.ui.TabIndexedElement
9592 *
9593 * @constructor
9594 * @param {OO.ui.Toolbar} toolbar
9595 * @param {Object} [config] Configuration options
9596 * @cfg {string} [header] Text to display at the top of the pop-up
9597 */
9598 OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
9599 // Allow passing positional parameters inside the config object
9600 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
9601 config = toolbar;
9602 toolbar = config.toolbar;
9603 }
9604
9605 // Configuration initialization
9606 config = config || {};
9607
9608 // Parent constructor
9609 OO.ui.PopupToolGroup.super.call( this, toolbar, config );
9610
9611 // Properties
9612 this.active = false;
9613 this.dragging = false;
9614 this.onBlurHandler = this.onBlur.bind( this );
9615 this.$handle = $( '<span>' );
9616
9617 // Mixin constructors
9618 OO.ui.IconElement.call( this, config );
9619 OO.ui.IndicatorElement.call( this, config );
9620 OO.ui.LabelElement.call( this, config );
9621 OO.ui.TitledElement.call( this, config );
9622 OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
9623 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
9624
9625 // Events
9626 this.$handle.on( {
9627 keydown: this.onHandleMouseKeyDown.bind( this ),
9628 keyup: this.onHandleMouseKeyUp.bind( this ),
9629 mousedown: this.onHandleMouseKeyDown.bind( this ),
9630 mouseup: this.onHandleMouseKeyUp.bind( this )
9631 } );
9632
9633 // Initialization
9634 this.$handle
9635 .addClass( 'oo-ui-popupToolGroup-handle' )
9636 .append( this.$icon, this.$label, this.$indicator );
9637 // If the pop-up should have a header, add it to the top of the toolGroup.
9638 // Note: If this feature is useful for other widgets, we could abstract it into an
9639 // OO.ui.HeaderedElement mixin constructor.
9640 if ( config.header !== undefined ) {
9641 this.$group
9642 .prepend( $( '<span>' )
9643 .addClass( 'oo-ui-popupToolGroup-header' )
9644 .text( config.header )
9645 );
9646 }
9647 this.$element
9648 .addClass( 'oo-ui-popupToolGroup' )
9649 .prepend( this.$handle );
9650 };
9651
9652 /* Setup */
9653
9654 OO.inheritClass( OO.ui.PopupToolGroup, OO.ui.ToolGroup );
9655 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IconElement );
9656 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IndicatorElement );
9657 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.LabelElement );
9658 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.TitledElement );
9659 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.ClippableElement );
9660 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.TabIndexedElement );
9661
9662 /* Static Properties */
9663
9664 /* Methods */
9665
9666 /**
9667 * @inheritdoc
9668 */
9669 OO.ui.PopupToolGroup.prototype.setDisabled = function () {
9670 // Parent method
9671 OO.ui.PopupToolGroup.super.prototype.setDisabled.apply( this, arguments );
9672
9673 if ( this.isDisabled() && this.isElementAttached() ) {
9674 this.setActive( false );
9675 }
9676 };
9677
9678 /**
9679 * Handle focus being lost.
9680 *
9681 * The event is actually generated from a mouseup/keyup, so it is not a normal blur event object.
9682 *
9683 * @param {jQuery.Event} e Mouse up or key up event
9684 */
9685 OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) {
9686 // Only deactivate when clicking outside the dropdown element
9687 if ( $( e.target ).closest( '.oo-ui-popupToolGroup' )[ 0 ] !== this.$element[ 0 ] ) {
9688 this.setActive( false );
9689 }
9690 };
9691
9692 /**
9693 * @inheritdoc
9694 */
9695 OO.ui.PopupToolGroup.prototype.onMouseKeyUp = function ( e ) {
9696 // Only close toolgroup when a tool was actually selected
9697 if (
9698 !this.isDisabled() && this.pressed && this.pressed === this.getTargetTool( e ) &&
9699 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
9700 ) {
9701 this.setActive( false );
9702 }
9703 return OO.ui.PopupToolGroup.super.prototype.onMouseKeyUp.call( this, e );
9704 };
9705
9706 /**
9707 * Handle mouse up and key up events.
9708 *
9709 * @param {jQuery.Event} e Mouse up or key up event
9710 */
9711 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyUp = function ( e ) {
9712 if (
9713 !this.isDisabled() &&
9714 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
9715 ) {
9716 return false;
9717 }
9718 };
9719
9720 /**
9721 * Handle mouse down and key down events.
9722 *
9723 * @param {jQuery.Event} e Mouse down or key down event
9724 */
9725 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyDown = function ( e ) {
9726 if (
9727 !this.isDisabled() &&
9728 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
9729 ) {
9730 this.setActive( !this.active );
9731 return false;
9732 }
9733 };
9734
9735 /**
9736 * Switch into active mode.
9737 *
9738 * When active, mouseup events anywhere in the document will trigger deactivation.
9739 */
9740 OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
9741 value = !!value;
9742 if ( this.active !== value ) {
9743 this.active = value;
9744 if ( value ) {
9745 this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler, true );
9746 this.getElementDocument().addEventListener( 'keyup', this.onBlurHandler, true );
9747
9748 // Try anchoring the popup to the left first
9749 this.$element.addClass( 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left' );
9750 this.toggleClipping( true );
9751 if ( this.isClippedHorizontally() ) {
9752 // Anchoring to the left caused the popup to clip, so anchor it to the right instead
9753 this.toggleClipping( false );
9754 this.$element
9755 .removeClass( 'oo-ui-popupToolGroup-left' )
9756 .addClass( 'oo-ui-popupToolGroup-right' );
9757 this.toggleClipping( true );
9758 }
9759 } else {
9760 this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler, true );
9761 this.getElementDocument().removeEventListener( 'keyup', this.onBlurHandler, true );
9762 this.$element.removeClass(
9763 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left oo-ui-popupToolGroup-right'
9764 );
9765 this.toggleClipping( false );
9766 }
9767 }
9768 };
9769
9770 /**
9771 * Drop down list layout of tools as labeled icon buttons.
9772 *
9773 * This layout allows some tools to be collapsible, controlled by a "More" / "Fewer" option at the
9774 * bottom of the main list. These are not automatically positioned at the bottom of the list; you
9775 * may want to use the 'promote' and 'demote' configuration options to achieve this.
9776 *
9777 * @class
9778 * @extends OO.ui.PopupToolGroup
9779 *
9780 * @constructor
9781 * @param {OO.ui.Toolbar} toolbar
9782 * @param {Object} [config] Configuration options
9783 * @cfg {Array} [allowCollapse] List of tools that can be collapsed. Remaining tools will be always
9784 * shown.
9785 * @cfg {Array} [forceExpand] List of tools that *may not* be collapsed. All remaining tools will be
9786 * allowed to be collapsed.
9787 * @cfg {boolean} [expanded=false] Whether the collapsible tools are expanded by default
9788 */
9789 OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
9790 // Allow passing positional parameters inside the config object
9791 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
9792 config = toolbar;
9793 toolbar = config.toolbar;
9794 }
9795
9796 // Configuration initialization
9797 config = config || {};
9798
9799 // Properties (must be set before parent constructor, which calls #populate)
9800 this.allowCollapse = config.allowCollapse;
9801 this.forceExpand = config.forceExpand;
9802 this.expanded = config.expanded !== undefined ? config.expanded : false;
9803 this.collapsibleTools = [];
9804
9805 // Parent constructor
9806 OO.ui.ListToolGroup.super.call( this, toolbar, config );
9807
9808 // Initialization
9809 this.$element.addClass( 'oo-ui-listToolGroup' );
9810 };
9811
9812 /* Setup */
9813
9814 OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup );
9815
9816 /* Static Properties */
9817
9818 OO.ui.ListToolGroup.static.accelTooltips = true;
9819
9820 OO.ui.ListToolGroup.static.name = 'list';
9821
9822 /* Methods */
9823
9824 /**
9825 * @inheritdoc
9826 */
9827 OO.ui.ListToolGroup.prototype.populate = function () {
9828 var i, len, allowCollapse = [];
9829
9830 OO.ui.ListToolGroup.super.prototype.populate.call( this );
9831
9832 // Update the list of collapsible tools
9833 if ( this.allowCollapse !== undefined ) {
9834 allowCollapse = this.allowCollapse;
9835 } else if ( this.forceExpand !== undefined ) {
9836 allowCollapse = OO.simpleArrayDifference( Object.keys( this.tools ), this.forceExpand );
9837 }
9838
9839 this.collapsibleTools = [];
9840 for ( i = 0, len = allowCollapse.length; i < len; i++ ) {
9841 if ( this.tools[ allowCollapse[ i ] ] !== undefined ) {
9842 this.collapsibleTools.push( this.tools[ allowCollapse[ i ] ] );
9843 }
9844 }
9845
9846 // Keep at the end, even when tools are added
9847 this.$group.append( this.getExpandCollapseTool().$element );
9848
9849 this.getExpandCollapseTool().toggle( this.collapsibleTools.length !== 0 );
9850 this.updateCollapsibleState();
9851 };
9852
9853 OO.ui.ListToolGroup.prototype.getExpandCollapseTool = function () {
9854 if ( this.expandCollapseTool === undefined ) {
9855 var ExpandCollapseTool = function () {
9856 ExpandCollapseTool.super.apply( this, arguments );
9857 };
9858
9859 OO.inheritClass( ExpandCollapseTool, OO.ui.Tool );
9860
9861 ExpandCollapseTool.prototype.onSelect = function () {
9862 this.toolGroup.expanded = !this.toolGroup.expanded;
9863 this.toolGroup.updateCollapsibleState();
9864 this.setActive( false );
9865 };
9866 ExpandCollapseTool.prototype.onUpdateState = function () {
9867 // Do nothing. Tool interface requires an implementation of this function.
9868 };
9869
9870 ExpandCollapseTool.static.name = 'more-fewer';
9871
9872 this.expandCollapseTool = new ExpandCollapseTool( this );
9873 }
9874 return this.expandCollapseTool;
9875 };
9876
9877 /**
9878 * @inheritdoc
9879 */
9880 OO.ui.ListToolGroup.prototype.onMouseKeyUp = function ( e ) {
9881 // Do not close the popup when the user wants to show more/fewer tools
9882 if (
9883 $( e.target ).closest( '.oo-ui-tool-name-more-fewer' ).length &&
9884 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
9885 ) {
9886 // HACK: Prevent the popup list from being hidden. Skip the PopupToolGroup implementation (which
9887 // hides the popup list when a tool is selected) and call ToolGroup's implementation directly.
9888 return OO.ui.ListToolGroup.super.super.prototype.onMouseKeyUp.call( this, e );
9889 } else {
9890 return OO.ui.ListToolGroup.super.prototype.onMouseKeyUp.call( this, e );
9891 }
9892 };
9893
9894 OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () {
9895 var i, len;
9896
9897 this.getExpandCollapseTool()
9898 .setIcon( this.expanded ? 'collapse' : 'expand' )
9899 .setTitle( OO.ui.msg( this.expanded ? 'ooui-toolgroup-collapse' : 'ooui-toolgroup-expand' ) );
9900
9901 for ( i = 0, len = this.collapsibleTools.length; i < len; i++ ) {
9902 this.collapsibleTools[ i ].toggle( this.expanded );
9903 }
9904 };
9905
9906 /**
9907 * Drop down menu layout of tools as selectable menu items.
9908 *
9909 * @class
9910 * @extends OO.ui.PopupToolGroup
9911 *
9912 * @constructor
9913 * @param {OO.ui.Toolbar} toolbar
9914 * @param {Object} [config] Configuration options
9915 */
9916 OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
9917 // Allow passing positional parameters inside the config object
9918 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
9919 config = toolbar;
9920 toolbar = config.toolbar;
9921 }
9922
9923 // Configuration initialization
9924 config = config || {};
9925
9926 // Parent constructor
9927 OO.ui.MenuToolGroup.super.call( this, toolbar, config );
9928
9929 // Events
9930 this.toolbar.connect( this, { updateState: 'onUpdateState' } );
9931
9932 // Initialization
9933 this.$element.addClass( 'oo-ui-menuToolGroup' );
9934 };
9935
9936 /* Setup */
9937
9938 OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup );
9939
9940 /* Static Properties */
9941
9942 OO.ui.MenuToolGroup.static.accelTooltips = true;
9943
9944 OO.ui.MenuToolGroup.static.name = 'menu';
9945
9946 /* Methods */
9947
9948 /**
9949 * Handle the toolbar state being updated.
9950 *
9951 * When the state changes, the title of each active item in the menu will be joined together and
9952 * used as a label for the group. The label will be empty if none of the items are active.
9953 */
9954 OO.ui.MenuToolGroup.prototype.onUpdateState = function () {
9955 var name,
9956 labelTexts = [];
9957
9958 for ( name in this.tools ) {
9959 if ( this.tools[ name ].isActive() ) {
9960 labelTexts.push( this.tools[ name ].getTitle() );
9961 }
9962 }
9963
9964 this.setLabel( labelTexts.join( ', ' ) || ' ' );
9965 };
9966
9967 /**
9968 * Tool that shows a popup when selected.
9969 *
9970 * @abstract
9971 * @class
9972 * @extends OO.ui.Tool
9973 * @mixins OO.ui.PopupElement
9974 *
9975 * @constructor
9976 * @param {OO.ui.ToolGroup} toolGroup
9977 * @param {Object} [config] Configuration options
9978 */
9979 OO.ui.PopupTool = function OoUiPopupTool( toolGroup, config ) {
9980 // Allow passing positional parameters inside the config object
9981 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
9982 config = toolGroup;
9983 toolGroup = config.toolGroup;
9984 }
9985
9986 // Parent constructor
9987 OO.ui.PopupTool.super.call( this, toolGroup, config );
9988
9989 // Mixin constructors
9990 OO.ui.PopupElement.call( this, config );
9991
9992 // Initialization
9993 this.$element
9994 .addClass( 'oo-ui-popupTool' )
9995 .append( this.popup.$element );
9996 };
9997
9998 /* Setup */
9999
10000 OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool );
10001 OO.mixinClass( OO.ui.PopupTool, OO.ui.PopupElement );
10002
10003 /* Methods */
10004
10005 /**
10006 * Handle the tool being selected.
10007 *
10008 * @inheritdoc
10009 */
10010 OO.ui.PopupTool.prototype.onSelect = function () {
10011 if ( !this.isDisabled() ) {
10012 this.popup.toggle();
10013 }
10014 this.setActive( false );
10015 return false;
10016 };
10017
10018 /**
10019 * Handle the toolbar state being updated.
10020 *
10021 * @inheritdoc
10022 */
10023 OO.ui.PopupTool.prototype.onUpdateState = function () {
10024 this.setActive( false );
10025 };
10026
10027 /**
10028 * Tool that has a tool group inside. This is a bad workaround for the lack of proper hierarchical
10029 * menus in toolbars (T74159).
10030 *
10031 * @abstract
10032 * @class
10033 * @extends OO.ui.Tool
10034 *
10035 * @constructor
10036 * @param {OO.ui.ToolGroup} toolGroup
10037 * @param {Object} [config] Configuration options
10038 */
10039 OO.ui.ToolGroupTool = function OoUiToolGroupTool( toolGroup, config ) {
10040 // Allow passing positional parameters inside the config object
10041 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
10042 config = toolGroup;
10043 toolGroup = config.toolGroup;
10044 }
10045
10046 // Parent constructor
10047 OO.ui.ToolGroupTool.super.call( this, toolGroup, config );
10048
10049 // Properties
10050 this.innerToolGroup = this.createGroup( this.constructor.static.groupConfig );
10051
10052 // Initialization
10053 this.$link.remove();
10054 this.$element
10055 .addClass( 'oo-ui-toolGroupTool' )
10056 .append( this.innerToolGroup.$element );
10057 };
10058
10059 /* Setup */
10060
10061 OO.inheritClass( OO.ui.ToolGroupTool, OO.ui.Tool );
10062
10063 /* Static Properties */
10064
10065 /**
10066 * Tool group configuration. See OO.ui.Toolbar#setup for the accepted values.
10067 *
10068 * @property {Object.<string,Array>}
10069 */
10070 OO.ui.ToolGroupTool.static.groupConfig = {};
10071
10072 /* Methods */
10073
10074 /**
10075 * Handle the tool being selected.
10076 *
10077 * @inheritdoc
10078 */
10079 OO.ui.ToolGroupTool.prototype.onSelect = function () {
10080 this.innerToolGroup.setActive( !this.innerToolGroup.active );
10081 return false;
10082 };
10083
10084 /**
10085 * Handle the toolbar state being updated.
10086 *
10087 * @inheritdoc
10088 */
10089 OO.ui.ToolGroupTool.prototype.onUpdateState = function () {
10090 this.setActive( false );
10091 };
10092
10093 /**
10094 * Build a OO.ui.ToolGroup from the configuration.
10095 *
10096 * @param {Object.<string,Array>} group Tool group configuration. See OO.ui.Toolbar#setup for the
10097 * accepted values.
10098 * @return {OO.ui.ListToolGroup}
10099 */
10100 OO.ui.ToolGroupTool.prototype.createGroup = function ( group ) {
10101 if ( group.include === '*' ) {
10102 // Apply defaults to catch-all groups
10103 if ( group.label === undefined ) {
10104 group.label = OO.ui.msg( 'ooui-toolbar-more' );
10105 }
10106 }
10107
10108 return this.toolbar.getToolGroupFactory().create( 'list', this.toolbar, group );
10109 };
10110
10111 /**
10112 * Mixin for OO.ui.Widget subclasses to provide OO.ui.GroupElement.
10113 *
10114 * Use together with OO.ui.ItemWidget to make disabled state inheritable.
10115 *
10116 * @private
10117 * @abstract
10118 * @class
10119 * @extends OO.ui.GroupElement
10120 *
10121 * @constructor
10122 * @param {Object} [config] Configuration options
10123 */
10124 OO.ui.GroupWidget = function OoUiGroupWidget( config ) {
10125 // Parent constructor
10126 OO.ui.GroupWidget.super.call( this, config );
10127 };
10128
10129 /* Setup */
10130
10131 OO.inheritClass( OO.ui.GroupWidget, OO.ui.GroupElement );
10132
10133 /* Methods */
10134
10135 /**
10136 * Set the disabled state of the widget.
10137 *
10138 * This will also update the disabled state of child widgets.
10139 *
10140 * @param {boolean} disabled Disable widget
10141 * @chainable
10142 */
10143 OO.ui.GroupWidget.prototype.setDisabled = function ( disabled ) {
10144 var i, len;
10145
10146 // Parent method
10147 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
10148 OO.ui.Widget.prototype.setDisabled.call( this, disabled );
10149
10150 // During construction, #setDisabled is called before the OO.ui.GroupElement constructor
10151 if ( this.items ) {
10152 for ( i = 0, len = this.items.length; i < len; i++ ) {
10153 this.items[ i ].updateDisabled();
10154 }
10155 }
10156
10157 return this;
10158 };
10159
10160 /**
10161 * Mixin for widgets used as items in widgets that inherit OO.ui.GroupWidget.
10162 *
10163 * Item widgets have a reference to a OO.ui.GroupWidget while they are attached to the group. This
10164 * allows bidirectional communication.
10165 *
10166 * Use together with OO.ui.GroupWidget to make disabled state inheritable.
10167 *
10168 * @private
10169 * @abstract
10170 * @class
10171 *
10172 * @constructor
10173 */
10174 OO.ui.ItemWidget = function OoUiItemWidget() {
10175 //
10176 };
10177
10178 /* Methods */
10179
10180 /**
10181 * Check if widget is disabled.
10182 *
10183 * Checks parent if present, making disabled state inheritable.
10184 *
10185 * @return {boolean} Widget is disabled
10186 */
10187 OO.ui.ItemWidget.prototype.isDisabled = function () {
10188 return this.disabled ||
10189 ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() );
10190 };
10191
10192 /**
10193 * Set group element is in.
10194 *
10195 * @param {OO.ui.GroupElement|null} group Group element, null if none
10196 * @chainable
10197 */
10198 OO.ui.ItemWidget.prototype.setElementGroup = function ( group ) {
10199 // Parent method
10200 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
10201 OO.ui.Element.prototype.setElementGroup.call( this, group );
10202
10203 // Initialize item disabled states
10204 this.updateDisabled();
10205
10206 return this;
10207 };
10208
10209 /**
10210 * OutlineControlsWidget is a set of controls for an {@link OO.ui.OutlineSelectWidget outline select widget}.
10211 * Controls include moving items up and down, removing items, and adding different kinds of items.
10212 * ####Currently, this class is only used by {@link OO.ui.BookletLayout BookletLayouts}.####
10213 *
10214 * @class
10215 * @extends OO.ui.Widget
10216 * @mixins OO.ui.GroupElement
10217 * @mixins OO.ui.IconElement
10218 *
10219 * @constructor
10220 * @param {OO.ui.OutlineSelectWidget} outline Outline to control
10221 * @param {Object} [config] Configuration options
10222 * @cfg {Object} [abilities] List of abilties
10223 * @cfg {boolean} [abilities.move=true] Allow moving movable items
10224 * @cfg {boolean} [abilities.remove=true] Allow removing removable items
10225 */
10226 OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
10227 // Allow passing positional parameters inside the config object
10228 if ( OO.isPlainObject( outline ) && config === undefined ) {
10229 config = outline;
10230 outline = config.outline;
10231 }
10232
10233 // Configuration initialization
10234 config = $.extend( { icon: 'add' }, config );
10235
10236 // Parent constructor
10237 OO.ui.OutlineControlsWidget.super.call( this, config );
10238
10239 // Mixin constructors
10240 OO.ui.GroupElement.call( this, config );
10241 OO.ui.IconElement.call( this, config );
10242
10243 // Properties
10244 this.outline = outline;
10245 this.$movers = $( '<div>' );
10246 this.upButton = new OO.ui.ButtonWidget( {
10247 framed: false,
10248 icon: 'collapse',
10249 title: OO.ui.msg( 'ooui-outline-control-move-up' )
10250 } );
10251 this.downButton = new OO.ui.ButtonWidget( {
10252 framed: false,
10253 icon: 'expand',
10254 title: OO.ui.msg( 'ooui-outline-control-move-down' )
10255 } );
10256 this.removeButton = new OO.ui.ButtonWidget( {
10257 framed: false,
10258 icon: 'remove',
10259 title: OO.ui.msg( 'ooui-outline-control-remove' )
10260 } );
10261 this.abilities = { move: true, remove: true };
10262
10263 // Events
10264 outline.connect( this, {
10265 select: 'onOutlineChange',
10266 add: 'onOutlineChange',
10267 remove: 'onOutlineChange'
10268 } );
10269 this.upButton.connect( this, { click: [ 'emit', 'move', -1 ] } );
10270 this.downButton.connect( this, { click: [ 'emit', 'move', 1 ] } );
10271 this.removeButton.connect( this, { click: [ 'emit', 'remove' ] } );
10272
10273 // Initialization
10274 this.$element.addClass( 'oo-ui-outlineControlsWidget' );
10275 this.$group.addClass( 'oo-ui-outlineControlsWidget-items' );
10276 this.$movers
10277 .addClass( 'oo-ui-outlineControlsWidget-movers' )
10278 .append( this.removeButton.$element, this.upButton.$element, this.downButton.$element );
10279 this.$element.append( this.$icon, this.$group, this.$movers );
10280 this.setAbilities( config.abilities || {} );
10281 };
10282
10283 /* Setup */
10284
10285 OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget );
10286 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.GroupElement );
10287 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.IconElement );
10288
10289 /* Events */
10290
10291 /**
10292 * @event move
10293 * @param {number} places Number of places to move
10294 */
10295
10296 /**
10297 * @event remove
10298 */
10299
10300 /* Methods */
10301
10302 /**
10303 * Set abilities.
10304 *
10305 * @param {Object} abilities List of abilties
10306 * @param {boolean} [abilities.move] Allow moving movable items
10307 * @param {boolean} [abilities.remove] Allow removing removable items
10308 */
10309 OO.ui.OutlineControlsWidget.prototype.setAbilities = function ( abilities ) {
10310 var ability;
10311
10312 for ( ability in this.abilities ) {
10313 if ( abilities[ability] !== undefined ) {
10314 this.abilities[ability] = !!abilities[ability];
10315 }
10316 }
10317
10318 this.onOutlineChange();
10319 };
10320
10321 /**
10322 *
10323 * @private
10324 * Handle outline change events.
10325 */
10326 OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () {
10327 var i, len, firstMovable, lastMovable,
10328 items = this.outline.getItems(),
10329 selectedItem = this.outline.getSelectedItem(),
10330 movable = this.abilities.move && selectedItem && selectedItem.isMovable(),
10331 removable = this.abilities.remove && selectedItem && selectedItem.isRemovable();
10332
10333 if ( movable ) {
10334 i = -1;
10335 len = items.length;
10336 while ( ++i < len ) {
10337 if ( items[ i ].isMovable() ) {
10338 firstMovable = items[ i ];
10339 break;
10340 }
10341 }
10342 i = len;
10343 while ( i-- ) {
10344 if ( items[ i ].isMovable() ) {
10345 lastMovable = items[ i ];
10346 break;
10347 }
10348 }
10349 }
10350 this.upButton.setDisabled( !movable || selectedItem === firstMovable );
10351 this.downButton.setDisabled( !movable || selectedItem === lastMovable );
10352 this.removeButton.setDisabled( !removable );
10353 };
10354
10355 /**
10356 * ToggleWidget implements basic behavior of widgets with an on/off state.
10357 * Please see OO.ui.ToggleButtonWidget and OO.ui.ToggleSwitchWidget for examples.
10358 *
10359 * @abstract
10360 * @class
10361 * @extends OO.ui.Widget
10362 *
10363 * @constructor
10364 * @param {Object} [config] Configuration options
10365 * @cfg {boolean} [value=false] The toggle’s initial on/off state.
10366 * By default, the toggle is in the 'off' state.
10367 */
10368 OO.ui.ToggleWidget = function OoUiToggleWidget( config ) {
10369 // Configuration initialization
10370 config = config || {};
10371
10372 // Parent constructor
10373 OO.ui.ToggleWidget.super.call( this, config );
10374
10375 // Properties
10376 this.value = null;
10377
10378 // Initialization
10379 this.$element.addClass( 'oo-ui-toggleWidget' );
10380 this.setValue( !!config.value );
10381 };
10382
10383 /* Setup */
10384
10385 OO.inheritClass( OO.ui.ToggleWidget, OO.ui.Widget );
10386
10387 /* Events */
10388
10389 /**
10390 * @event change
10391 *
10392 * A change event is emitted when the on/off state of the toggle changes.
10393 *
10394 * @param {boolean} value Value representing the new state of the toggle
10395 */
10396
10397 /* Methods */
10398
10399 /**
10400 * Get the value representing the toggle’s state.
10401 *
10402 * @return {boolean} The on/off state of the toggle
10403 */
10404 OO.ui.ToggleWidget.prototype.getValue = function () {
10405 return this.value;
10406 };
10407
10408 /**
10409 * Set the state of the toggle: `true` for 'on', `false' for 'off'.
10410 *
10411 * @param {boolean} value The state of the toggle
10412 * @fires change
10413 * @chainable
10414 */
10415 OO.ui.ToggleWidget.prototype.setValue = function ( value ) {
10416 value = !!value;
10417 if ( this.value !== value ) {
10418 this.value = value;
10419 this.emit( 'change', value );
10420 this.$element.toggleClass( 'oo-ui-toggleWidget-on', value );
10421 this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value );
10422 this.$element.attr( 'aria-checked', value.toString() );
10423 }
10424 return this;
10425 };
10426
10427 /**
10428 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
10429 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
10430 * removed, and cleared from the group.
10431 *
10432 * @example
10433 * // Example: A ButtonGroupWidget with two buttons
10434 * var button1 = new OO.ui.PopupButtonWidget( {
10435 * label: 'Select a category',
10436 * icon: 'menu',
10437 * popup: {
10438 * $content: $( '<p>List of categories...</p>' ),
10439 * padded: true,
10440 * align: 'left'
10441 * }
10442 * } );
10443 * var button2 = new OO.ui.ButtonWidget( {
10444 * label: 'Add item'
10445 * });
10446 * var buttonGroup = new OO.ui.ButtonGroupWidget( {
10447 * items: [button1, button2]
10448 * } );
10449 * $( 'body' ).append( buttonGroup.$element );
10450 *
10451 * @class
10452 * @extends OO.ui.Widget
10453 * @mixins OO.ui.GroupElement
10454 *
10455 * @constructor
10456 * @param {Object} [config] Configuration options
10457 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
10458 */
10459 OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
10460 // Configuration initialization
10461 config = config || {};
10462
10463 // Parent constructor
10464 OO.ui.ButtonGroupWidget.super.call( this, config );
10465
10466 // Mixin constructors
10467 OO.ui.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
10468
10469 // Initialization
10470 this.$element.addClass( 'oo-ui-buttonGroupWidget' );
10471 if ( Array.isArray( config.items ) ) {
10472 this.addItems( config.items );
10473 }
10474 };
10475
10476 /* Setup */
10477
10478 OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget );
10479 OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.GroupElement );
10480
10481 /**
10482 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
10483 * feels, and functionality can be customized via the class’s configuration options
10484 * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
10485 * and examples.
10486 *
10487 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
10488 *
10489 * @example
10490 * // A button widget
10491 * var button = new OO.ui.ButtonWidget( {
10492 * label: 'Button with Icon',
10493 * icon: 'remove',
10494 * iconTitle: 'Remove'
10495 * } );
10496 * $( 'body' ).append( button.$element );
10497 *
10498 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
10499 *
10500 * @class
10501 * @extends OO.ui.Widget
10502 * @mixins OO.ui.ButtonElement
10503 * @mixins OO.ui.IconElement
10504 * @mixins OO.ui.IndicatorElement
10505 * @mixins OO.ui.LabelElement
10506 * @mixins OO.ui.TitledElement
10507 * @mixins OO.ui.FlaggedElement
10508 * @mixins OO.ui.TabIndexedElement
10509 *
10510 * @constructor
10511 * @param {Object} [config] Configuration options
10512 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
10513 * @cfg {string} [target] The frame or window in which to open the hyperlink.
10514 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
10515 */
10516 OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
10517 // Configuration initialization
10518 config = config || {};
10519
10520 // Parent constructor
10521 OO.ui.ButtonWidget.super.call( this, config );
10522
10523 // Mixin constructors
10524 OO.ui.ButtonElement.call( this, config );
10525 OO.ui.IconElement.call( this, config );
10526 OO.ui.IndicatorElement.call( this, config );
10527 OO.ui.LabelElement.call( this, config );
10528 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
10529 OO.ui.FlaggedElement.call( this, config );
10530 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
10531
10532 // Properties
10533 this.href = null;
10534 this.target = null;
10535 this.noFollow = false;
10536
10537 // Events
10538 this.connect( this, { disable: 'onDisable' } );
10539
10540 // Initialization
10541 this.$button.append( this.$icon, this.$label, this.$indicator );
10542 this.$element
10543 .addClass( 'oo-ui-buttonWidget' )
10544 .append( this.$button );
10545 this.setHref( config.href );
10546 this.setTarget( config.target );
10547 this.setNoFollow( config.noFollow );
10548 };
10549
10550 /* Setup */
10551
10552 OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget );
10553 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.ButtonElement );
10554 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IconElement );
10555 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IndicatorElement );
10556 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.LabelElement );
10557 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TitledElement );
10558 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.FlaggedElement );
10559 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TabIndexedElement );
10560
10561 /* Methods */
10562
10563 /**
10564 * @inheritdoc
10565 */
10566 OO.ui.ButtonWidget.prototype.onMouseDown = function ( e ) {
10567 if ( !this.isDisabled() ) {
10568 // Remove the tab-index while the button is down to prevent the button from stealing focus
10569 this.$button.removeAttr( 'tabindex' );
10570 }
10571
10572 return OO.ui.ButtonElement.prototype.onMouseDown.call( this, e );
10573 };
10574
10575 /**
10576 * @inheritdoc
10577 */
10578 OO.ui.ButtonWidget.prototype.onMouseUp = function ( e ) {
10579 if ( !this.isDisabled() ) {
10580 // Restore the tab-index after the button is up to restore the button's accessibility
10581 this.$button.attr( 'tabindex', this.tabIndex );
10582 }
10583
10584 return OO.ui.ButtonElement.prototype.onMouseUp.call( this, e );
10585 };
10586
10587 /**
10588 * @inheritdoc
10589 */
10590 OO.ui.ButtonWidget.prototype.onClick = function ( e ) {
10591 var ret = OO.ui.ButtonElement.prototype.onClick.call( this, e );
10592 if ( this.href ) {
10593 return true;
10594 }
10595 return ret;
10596 };
10597
10598 /**
10599 * @inheritdoc
10600 */
10601 OO.ui.ButtonWidget.prototype.onKeyPress = function ( e ) {
10602 var ret = OO.ui.ButtonElement.prototype.onKeyPress.call( this, e );
10603 if ( this.href ) {
10604 return true;
10605 }
10606 return ret;
10607 };
10608
10609 /**
10610 * Get hyperlink location.
10611 *
10612 * @return {string} Hyperlink location
10613 */
10614 OO.ui.ButtonWidget.prototype.getHref = function () {
10615 return this.href;
10616 };
10617
10618 /**
10619 * Get hyperlink target.
10620 *
10621 * @return {string} Hyperlink target
10622 */
10623 OO.ui.ButtonWidget.prototype.getTarget = function () {
10624 return this.target;
10625 };
10626
10627 /**
10628 * Get search engine traversal hint.
10629 *
10630 * @return {boolean} Whether search engines should avoid traversing this hyperlink
10631 */
10632 OO.ui.ButtonWidget.prototype.getNoFollow = function () {
10633 return this.noFollow;
10634 };
10635
10636 /**
10637 * Set hyperlink location.
10638 *
10639 * @param {string|null} href Hyperlink location, null to remove
10640 */
10641 OO.ui.ButtonWidget.prototype.setHref = function ( href ) {
10642 href = typeof href === 'string' ? href : null;
10643
10644 if ( href !== this.href ) {
10645 this.href = href;
10646 this.updateHref();
10647 }
10648
10649 return this;
10650 };
10651
10652 /**
10653 * Update the `href` attribute, in case of changes to href or
10654 * disabled state.
10655 *
10656 * @private
10657 * @chainable
10658 */
10659 OO.ui.ButtonWidget.prototype.updateHref = function () {
10660 if ( this.href !== null && !this.isDisabled() ) {
10661 this.$button.attr( 'href', this.href );
10662 } else {
10663 this.$button.removeAttr( 'href' );
10664 }
10665
10666 return this;
10667 };
10668
10669 /**
10670 * Handle disable events.
10671 *
10672 * @private
10673 * @param {boolean} disabled Element is disabled
10674 */
10675 OO.ui.ButtonWidget.prototype.onDisable = function () {
10676 this.updateHref();
10677 };
10678
10679 /**
10680 * Set hyperlink target.
10681 *
10682 * @param {string|null} target Hyperlink target, null to remove
10683 */
10684 OO.ui.ButtonWidget.prototype.setTarget = function ( target ) {
10685 target = typeof target === 'string' ? target : null;
10686
10687 if ( target !== this.target ) {
10688 this.target = target;
10689 if ( target !== null ) {
10690 this.$button.attr( 'target', target );
10691 } else {
10692 this.$button.removeAttr( 'target' );
10693 }
10694 }
10695
10696 return this;
10697 };
10698
10699 /**
10700 * Set search engine traversal hint.
10701 *
10702 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
10703 */
10704 OO.ui.ButtonWidget.prototype.setNoFollow = function ( noFollow ) {
10705 noFollow = typeof noFollow === 'boolean' ? noFollow : true;
10706
10707 if ( noFollow !== this.noFollow ) {
10708 this.noFollow = noFollow;
10709 if ( noFollow ) {
10710 this.$button.attr( 'rel', 'nofollow' );
10711 } else {
10712 this.$button.removeAttr( 'rel' );
10713 }
10714 }
10715
10716 return this;
10717 };
10718
10719 /**
10720 * An ActionWidget is a {@link OO.ui.ButtonWidget button widget} that executes an action.
10721 * Action widgets are used with OO.ui.ActionSet, which manages the behavior and availability
10722 * of the actions.
10723 *
10724 * Both actions and action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
10725 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information
10726 * and examples.
10727 *
10728 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
10729 *
10730 * @class
10731 * @extends OO.ui.ButtonWidget
10732 * @mixins OO.ui.PendingElement
10733 *
10734 * @constructor
10735 * @param {Object} [config] Configuration options
10736 * @cfg {string} [action] Symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
10737 * @cfg {string[]} [modes] Symbolic names of the modes (e.g., ‘edit’ or ‘read’) in which the action
10738 * should be made available. See the action set's {@link OO.ui.ActionSet#setMode setMode} method
10739 * for more information about setting modes.
10740 * @cfg {boolean} [framed=false] Render the action button with a frame
10741 */
10742 OO.ui.ActionWidget = function OoUiActionWidget( config ) {
10743 // Configuration initialization
10744 config = $.extend( { framed: false }, config );
10745
10746 // Parent constructor
10747 OO.ui.ActionWidget.super.call( this, config );
10748
10749 // Mixin constructors
10750 OO.ui.PendingElement.call( this, config );
10751
10752 // Properties
10753 this.action = config.action || '';
10754 this.modes = config.modes || [];
10755 this.width = 0;
10756 this.height = 0;
10757
10758 // Initialization
10759 this.$element.addClass( 'oo-ui-actionWidget' );
10760 };
10761
10762 /* Setup */
10763
10764 OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget );
10765 OO.mixinClass( OO.ui.ActionWidget, OO.ui.PendingElement );
10766
10767 /* Events */
10768
10769 /**
10770 * A resize event is emitted when the size of the widget changes.
10771 *
10772 * @event resize
10773 */
10774
10775 /* Methods */
10776
10777 /**
10778 * Check if the action is configured to be available in the specified `mode`.
10779 *
10780 * @param {string} mode Name of mode
10781 * @return {boolean} The action is configured with the mode
10782 */
10783 OO.ui.ActionWidget.prototype.hasMode = function ( mode ) {
10784 return this.modes.indexOf( mode ) !== -1;
10785 };
10786
10787 /**
10788 * Get the symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
10789 *
10790 * @return {string}
10791 */
10792 OO.ui.ActionWidget.prototype.getAction = function () {
10793 return this.action;
10794 };
10795
10796 /**
10797 * Get the symbolic name of the mode or modes for which the action is configured to be available.
10798 *
10799 * The current mode is set with the action set's {@link OO.ui.ActionSet#setMode setMode} method.
10800 * Only actions that are configured to be avaiable in the current mode will be visible. All other actions
10801 * are hidden.
10802 *
10803 * @return {string[]}
10804 */
10805 OO.ui.ActionWidget.prototype.getModes = function () {
10806 return this.modes.slice();
10807 };
10808
10809 /**
10810 * Emit a resize event if the size has changed.
10811 *
10812 * @private
10813 * @chainable
10814 */
10815 OO.ui.ActionWidget.prototype.propagateResize = function () {
10816 var width, height;
10817
10818 if ( this.isElementAttached() ) {
10819 width = this.$element.width();
10820 height = this.$element.height();
10821
10822 if ( width !== this.width || height !== this.height ) {
10823 this.width = width;
10824 this.height = height;
10825 this.emit( 'resize' );
10826 }
10827 }
10828
10829 return this;
10830 };
10831
10832 /**
10833 * @inheritdoc
10834 */
10835 OO.ui.ActionWidget.prototype.setIcon = function () {
10836 // Mixin method
10837 OO.ui.IconElement.prototype.setIcon.apply( this, arguments );
10838 this.propagateResize();
10839
10840 return this;
10841 };
10842
10843 /**
10844 * @inheritdoc
10845 */
10846 OO.ui.ActionWidget.prototype.setLabel = function () {
10847 // Mixin method
10848 OO.ui.LabelElement.prototype.setLabel.apply( this, arguments );
10849 this.propagateResize();
10850
10851 return this;
10852 };
10853
10854 /**
10855 * @inheritdoc
10856 */
10857 OO.ui.ActionWidget.prototype.setFlags = function () {
10858 // Mixin method
10859 OO.ui.FlaggedElement.prototype.setFlags.apply( this, arguments );
10860 this.propagateResize();
10861
10862 return this;
10863 };
10864
10865 /**
10866 * @inheritdoc
10867 */
10868 OO.ui.ActionWidget.prototype.clearFlags = function () {
10869 // Mixin method
10870 OO.ui.FlaggedElement.prototype.clearFlags.apply( this, arguments );
10871 this.propagateResize();
10872
10873 return this;
10874 };
10875
10876 /**
10877 * Toggle the visibility of the action button.
10878 *
10879 * @param {boolean} [show] Show button, omit to toggle visibility
10880 * @chainable
10881 */
10882 OO.ui.ActionWidget.prototype.toggle = function () {
10883 // Parent method
10884 OO.ui.ActionWidget.super.prototype.toggle.apply( this, arguments );
10885 this.propagateResize();
10886
10887 return this;
10888 };
10889
10890 /**
10891 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
10892 * which is used to display additional information or options.
10893 *
10894 * @example
10895 * // Example of a popup button.
10896 * var popupButton = new OO.ui.PopupButtonWidget( {
10897 * label: 'Popup button with options',
10898 * icon: 'menu',
10899 * popup: {
10900 * $content: $( '<p>Additional options here.</p>' ),
10901 * padded: true,
10902 * align: 'left'
10903 * }
10904 * } );
10905 * // Append the button to the DOM.
10906 * $( 'body' ).append( popupButton.$element );
10907 *
10908 * @class
10909 * @extends OO.ui.ButtonWidget
10910 * @mixins OO.ui.PopupElement
10911 *
10912 * @constructor
10913 * @param {Object} [config] Configuration options
10914 */
10915 OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
10916 // Parent constructor
10917 OO.ui.PopupButtonWidget.super.call( this, config );
10918
10919 // Mixin constructors
10920 OO.ui.PopupElement.call( this, config );
10921
10922 // Events
10923 this.connect( this, { click: 'onAction' } );
10924
10925 // Initialization
10926 this.$element
10927 .addClass( 'oo-ui-popupButtonWidget' )
10928 .attr( 'aria-haspopup', 'true' )
10929 .append( this.popup.$element );
10930 };
10931
10932 /* Setup */
10933
10934 OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
10935 OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.PopupElement );
10936
10937 /* Methods */
10938
10939 /**
10940 * Handle the button action being triggered.
10941 *
10942 * @private
10943 */
10944 OO.ui.PopupButtonWidget.prototype.onAction = function () {
10945 this.popup.toggle();
10946 };
10947
10948 /**
10949 * ToggleButtons are buttons that have a state (‘on’ or ‘off’) that is represented by a
10950 * Boolean value. Like other {@link OO.ui.ButtonWidget buttons}, toggle buttons can be
10951 * configured with {@link OO.ui.IconElement icons}, {@link OO.ui.IndicatorElement indicators},
10952 * {@link OO.ui.TitledElement titles}, {@link OO.ui.FlaggedElement styling flags},
10953 * and {@link OO.ui.LabelElement labels}. Please see
10954 * the [OOjs UI documentation][1] on MediaWiki for more information.
10955 *
10956 * @example
10957 * // Toggle buttons in the 'off' and 'on' state.
10958 * var toggleButton1 = new OO.ui.ToggleButtonWidget( {
10959 * label: 'Toggle Button off'
10960 * } );
10961 * var toggleButton2 = new OO.ui.ToggleButtonWidget( {
10962 * label: 'Toggle Button on',
10963 * value: true
10964 * } );
10965 * // Append the buttons to the DOM.
10966 * $( 'body' ).append( toggleButton1.$element, toggleButton2.$element );
10967 *
10968 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Toggle_buttons
10969 *
10970 * @class
10971 * @extends OO.ui.ToggleWidget
10972 * @mixins OO.ui.ButtonElement
10973 * @mixins OO.ui.IconElement
10974 * @mixins OO.ui.IndicatorElement
10975 * @mixins OO.ui.LabelElement
10976 * @mixins OO.ui.TitledElement
10977 * @mixins OO.ui.FlaggedElement
10978 * @mixins OO.ui.TabIndexedElement
10979 *
10980 * @constructor
10981 * @param {Object} [config] Configuration options
10982 * @cfg {boolean} [value=false] The toggle button’s initial on/off
10983 * state. By default, the button is in the 'off' state.
10984 */
10985 OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) {
10986 // Configuration initialization
10987 config = config || {};
10988
10989 // Parent constructor
10990 OO.ui.ToggleButtonWidget.super.call( this, config );
10991
10992 // Mixin constructors
10993 OO.ui.ButtonElement.call( this, config );
10994 OO.ui.IconElement.call( this, config );
10995 OO.ui.IndicatorElement.call( this, config );
10996 OO.ui.LabelElement.call( this, config );
10997 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
10998 OO.ui.FlaggedElement.call( this, config );
10999 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
11000
11001 // Events
11002 this.connect( this, { click: 'onAction' } );
11003
11004 // Initialization
11005 this.$button.append( this.$icon, this.$label, this.$indicator );
11006 this.$element
11007 .addClass( 'oo-ui-toggleButtonWidget' )
11008 .append( this.$button );
11009 };
11010
11011 /* Setup */
11012
11013 OO.inheritClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget );
11014 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.ButtonElement );
11015 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.IconElement );
11016 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.IndicatorElement );
11017 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.LabelElement );
11018 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.TitledElement );
11019 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.FlaggedElement );
11020 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.TabIndexedElement );
11021
11022 /* Methods */
11023
11024 /**
11025 * Handle the button action being triggered.
11026 *
11027 * @private
11028 */
11029 OO.ui.ToggleButtonWidget.prototype.onAction = function () {
11030 this.setValue( !this.value );
11031 };
11032
11033 /**
11034 * @inheritdoc
11035 */
11036 OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
11037 value = !!value;
11038 if ( value !== this.value ) {
11039 // Might be called from parent constructor before ButtonElement constructor
11040 if ( this.$button ) {
11041 this.$button.attr( 'aria-pressed', value.toString() );
11042 }
11043 this.setActive( value );
11044 }
11045
11046 // Parent method
11047 OO.ui.ToggleButtonWidget.super.prototype.setValue.call( this, value );
11048
11049 return this;
11050 };
11051
11052 /**
11053 * @inheritdoc
11054 */
11055 OO.ui.ToggleButtonWidget.prototype.setButtonElement = function ( $button ) {
11056 if ( this.$button ) {
11057 this.$button.removeAttr( 'aria-pressed' );
11058 }
11059 OO.ui.ButtonElement.prototype.setButtonElement.call( this, $button );
11060 this.$button.attr( 'aria-pressed', this.value.toString() );
11061 };
11062
11063 /**
11064 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
11065 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
11066 * users can interact with it.
11067 *
11068 * @example
11069 * // Example: A DropdownWidget with a menu that contains three options
11070 * var dropDown = new OO.ui.DropdownWidget( {
11071 * label: 'Dropdown menu: Select a menu option',
11072 * menu: {
11073 * items: [
11074 * new OO.ui.MenuOptionWidget( {
11075 * data: 'a',
11076 * label: 'First'
11077 * } ),
11078 * new OO.ui.MenuOptionWidget( {
11079 * data: 'b',
11080 * label: 'Second'
11081 * } ),
11082 * new OO.ui.MenuOptionWidget( {
11083 * data: 'c',
11084 * label: 'Third'
11085 * } )
11086 * ]
11087 * }
11088 * } );
11089 *
11090 * $( 'body' ).append( dropDown.$element );
11091 *
11092 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
11093 *
11094 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
11095 *
11096 * @class
11097 * @extends OO.ui.Widget
11098 * @mixins OO.ui.IconElement
11099 * @mixins OO.ui.IndicatorElement
11100 * @mixins OO.ui.LabelElement
11101 * @mixins OO.ui.TitledElement
11102 * @mixins OO.ui.TabIndexedElement
11103 *
11104 * @constructor
11105 * @param {Object} [config] Configuration options
11106 * @cfg {Object} [menu] Configuration options to pass to menu widget
11107 */
11108 OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
11109 // Configuration initialization
11110 config = $.extend( { indicator: 'down' }, config );
11111
11112 // Parent constructor
11113 OO.ui.DropdownWidget.super.call( this, config );
11114
11115 // Properties (must be set before TabIndexedElement constructor call)
11116 this.$handle = this.$( '<span>' );
11117
11118 // Mixin constructors
11119 OO.ui.IconElement.call( this, config );
11120 OO.ui.IndicatorElement.call( this, config );
11121 OO.ui.LabelElement.call( this, config );
11122 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
11123 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
11124
11125 // Properties
11126 this.menu = new OO.ui.MenuSelectWidget( $.extend( { widget: this }, config.menu ) );
11127
11128 // Events
11129 this.$handle.on( {
11130 click: this.onClick.bind( this ),
11131 keypress: this.onKeyPress.bind( this )
11132 } );
11133 this.menu.connect( this, { select: 'onMenuSelect' } );
11134
11135 // Initialization
11136 this.$handle
11137 .addClass( 'oo-ui-dropdownWidget-handle' )
11138 .append( this.$icon, this.$label, this.$indicator );
11139 this.$element
11140 .addClass( 'oo-ui-dropdownWidget' )
11141 .append( this.$handle, this.menu.$element );
11142 };
11143
11144 /* Setup */
11145
11146 OO.inheritClass( OO.ui.DropdownWidget, OO.ui.Widget );
11147 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.IconElement );
11148 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.IndicatorElement );
11149 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.LabelElement );
11150 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.TitledElement );
11151 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.TabIndexedElement );
11152
11153 /* Methods */
11154
11155 /**
11156 * Get the menu.
11157 *
11158 * @return {OO.ui.MenuSelectWidget} Menu of widget
11159 */
11160 OO.ui.DropdownWidget.prototype.getMenu = function () {
11161 return this.menu;
11162 };
11163
11164 /**
11165 * Handles menu select events.
11166 *
11167 * @private
11168 * @param {OO.ui.MenuOptionWidget} item Selected menu item
11169 */
11170 OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
11171 var selectedLabel;
11172
11173 if ( !item ) {
11174 return;
11175 }
11176
11177 selectedLabel = item.getLabel();
11178
11179 // If the label is a DOM element, clone it, because setLabel will append() it
11180 if ( selectedLabel instanceof jQuery ) {
11181 selectedLabel = selectedLabel.clone();
11182 }
11183
11184 this.setLabel( selectedLabel );
11185 };
11186
11187 /**
11188 * Handle mouse click events.
11189 *
11190 * @private
11191 * @param {jQuery.Event} e Mouse click event
11192 */
11193 OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
11194 if ( !this.isDisabled() && e.which === 1 ) {
11195 this.menu.toggle();
11196 }
11197 return false;
11198 };
11199
11200 /**
11201 * Handle key press events.
11202 *
11203 * @private
11204 * @param {jQuery.Event} e Key press event
11205 */
11206 OO.ui.DropdownWidget.prototype.onKeyPress = function ( e ) {
11207 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
11208 this.menu.toggle();
11209 return false;
11210 }
11211 };
11212
11213 /**
11214 * IconWidget is a generic widget for {@link OO.ui.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
11215 * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
11216 * for a list of icons included in the library.
11217 *
11218 * @example
11219 * // An icon widget with a label
11220 * var myIcon = new OO.ui.IconWidget( {
11221 * icon: 'help',
11222 * iconTitle: 'Help'
11223 * } );
11224 * // Create a label.
11225 * var iconLabel = new OO.ui.LabelWidget( {
11226 * label: 'Help'
11227 * } );
11228 * $( 'body' ).append( myIcon.$element, iconLabel.$element );
11229 *
11230 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
11231 *
11232 * @class
11233 * @extends OO.ui.Widget
11234 * @mixins OO.ui.IconElement
11235 * @mixins OO.ui.TitledElement
11236 * @mixins OO.ui.FlaggedElement
11237 *
11238 * @constructor
11239 * @param {Object} [config] Configuration options
11240 */
11241 OO.ui.IconWidget = function OoUiIconWidget( config ) {
11242 // Configuration initialization
11243 config = config || {};
11244
11245 // Parent constructor
11246 OO.ui.IconWidget.super.call( this, config );
11247
11248 // Mixin constructors
11249 OO.ui.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) );
11250 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
11251 OO.ui.FlaggedElement.call( this, $.extend( {}, config, { $flagged: this.$element } ) );
11252
11253 // Initialization
11254 this.$element.addClass( 'oo-ui-iconWidget' );
11255 };
11256
11257 /* Setup */
11258
11259 OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
11260 OO.mixinClass( OO.ui.IconWidget, OO.ui.IconElement );
11261 OO.mixinClass( OO.ui.IconWidget, OO.ui.TitledElement );
11262 OO.mixinClass( OO.ui.IconWidget, OO.ui.FlaggedElement );
11263
11264 /* Static Properties */
11265
11266 OO.ui.IconWidget.static.tagName = 'span';
11267
11268 /**
11269 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
11270 * attention to the status of an item or to clarify the function of a control. For a list of
11271 * indicators included in the library, please see the [OOjs UI documentation on MediaWiki][1].
11272 *
11273 * @example
11274 * // Example of an indicator widget
11275 * var indicator1 = new OO.ui.IndicatorWidget( {
11276 * indicator: 'alert'
11277 * } );
11278 *
11279 * // Create a fieldset layout to add a label
11280 * var fieldset = new OO.ui.FieldsetLayout();
11281 * fieldset.addItems( [
11282 * new OO.ui.FieldLayout( indicator1, { label: 'An alert indicator:' } )
11283 * ] );
11284 * $( 'body' ).append( fieldset.$element );
11285 *
11286 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
11287 *
11288 * @class
11289 * @extends OO.ui.Widget
11290 * @mixins OO.ui.IndicatorElement
11291 * @mixins OO.ui.TitledElement
11292 *
11293 * @constructor
11294 * @param {Object} [config] Configuration options
11295 */
11296 OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
11297 // Configuration initialization
11298 config = config || {};
11299
11300 // Parent constructor
11301 OO.ui.IndicatorWidget.super.call( this, config );
11302
11303 // Mixin constructors
11304 OO.ui.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) );
11305 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
11306
11307 // Initialization
11308 this.$element.addClass( 'oo-ui-indicatorWidget' );
11309 };
11310
11311 /* Setup */
11312
11313 OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
11314 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.IndicatorElement );
11315 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.TitledElement );
11316
11317 /* Static Properties */
11318
11319 OO.ui.IndicatorWidget.static.tagName = 'span';
11320
11321 /**
11322 * InputWidget is the base class for all input widgets, which
11323 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
11324 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
11325 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
11326 *
11327 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
11328 *
11329 * @abstract
11330 * @class
11331 * @extends OO.ui.Widget
11332 * @mixins OO.ui.FlaggedElement
11333 * @mixins OO.ui.TabIndexedElement
11334 *
11335 * @constructor
11336 * @param {Object} [config] Configuration options
11337 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
11338 * @cfg {string} [value=''] The value of the input.
11339 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
11340 * before it is accepted.
11341 */
11342 OO.ui.InputWidget = function OoUiInputWidget( config ) {
11343 // Configuration initialization
11344 config = config || {};
11345
11346 // Parent constructor
11347 OO.ui.InputWidget.super.call( this, config );
11348
11349 // Properties
11350 this.$input = this.getInputElement( config );
11351 this.value = '';
11352 this.inputFilter = config.inputFilter;
11353
11354 // Mixin constructors
11355 OO.ui.FlaggedElement.call( this, config );
11356 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$input } ) );
11357
11358 // Events
11359 this.$input.on( 'keydown mouseup cut paste change input select', this.onEdit.bind( this ) );
11360
11361 // Initialization
11362 this.$input
11363 .attr( 'name', config.name )
11364 .prop( 'disabled', this.isDisabled() );
11365 this.$element.addClass( 'oo-ui-inputWidget' ).append( this.$input, $( '<span>' ) );
11366 this.setValue( config.value );
11367 };
11368
11369 /* Setup */
11370
11371 OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
11372 OO.mixinClass( OO.ui.InputWidget, OO.ui.FlaggedElement );
11373 OO.mixinClass( OO.ui.InputWidget, OO.ui.TabIndexedElement );
11374
11375 /* Events */
11376
11377 /**
11378 * @event change
11379 *
11380 * A change event is emitted when the value of the input changes.
11381 *
11382 * @param {string} value
11383 */
11384
11385 /* Methods */
11386
11387 /**
11388 * Get input element.
11389 *
11390 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
11391 * different circumstances. The element must have a `value` property (like form elements).
11392 *
11393 * @private
11394 * @param {Object} config Configuration options
11395 * @return {jQuery} Input element
11396 */
11397 OO.ui.InputWidget.prototype.getInputElement = function () {
11398 return $( '<input>' );
11399 };
11400
11401 /**
11402 * Handle potentially value-changing events.
11403 *
11404 * @private
11405 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
11406 */
11407 OO.ui.InputWidget.prototype.onEdit = function () {
11408 var widget = this;
11409 if ( !this.isDisabled() ) {
11410 // Allow the stack to clear so the value will be updated
11411 setTimeout( function () {
11412 widget.setValue( widget.$input.val() );
11413 } );
11414 }
11415 };
11416
11417 /**
11418 * Get the value of the input.
11419 *
11420 * @return {string} Input value
11421 */
11422 OO.ui.InputWidget.prototype.getValue = function () {
11423 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
11424 // it, and we won't know unless they're kind enough to trigger a 'change' event.
11425 var value = this.$input.val();
11426 if ( this.value !== value ) {
11427 this.setValue( value );
11428 }
11429 return this.value;
11430 };
11431
11432 /**
11433 * Set the direction of the input, either RTL (right-to-left) or LTR (left-to-right).
11434 *
11435 * @param {boolean} isRTL
11436 * Direction is right-to-left
11437 */
11438 OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
11439 this.$input.prop( 'dir', isRTL ? 'rtl' : 'ltr' );
11440 };
11441
11442 /**
11443 * Set the value of the input.
11444 *
11445 * @param {string} value New value
11446 * @fires change
11447 * @chainable
11448 */
11449 OO.ui.InputWidget.prototype.setValue = function ( value ) {
11450 value = this.cleanUpValue( value );
11451 // Update the DOM if it has changed. Note that with cleanUpValue, it
11452 // is possible for the DOM value to change without this.value changing.
11453 if ( this.$input.val() !== value ) {
11454 this.$input.val( value );
11455 }
11456 if ( this.value !== value ) {
11457 this.value = value;
11458 this.emit( 'change', this.value );
11459 }
11460 return this;
11461 };
11462
11463 /**
11464 * Clean up incoming value.
11465 *
11466 * Ensures value is a string, and converts undefined and null to empty string.
11467 *
11468 * @private
11469 * @param {string} value Original value
11470 * @return {string} Cleaned up value
11471 */
11472 OO.ui.InputWidget.prototype.cleanUpValue = function ( value ) {
11473 if ( value === undefined || value === null ) {
11474 return '';
11475 } else if ( this.inputFilter ) {
11476 return this.inputFilter( String( value ) );
11477 } else {
11478 return String( value );
11479 }
11480 };
11481
11482 /**
11483 * Simulate the behavior of clicking on a label bound to this input. This method is only called by
11484 * {@link OO.ui.LabelWidget LabelWidget} and {@link OO.ui.FieldLayout FieldLayout}. It should not be
11485 * called directly.
11486 */
11487 OO.ui.InputWidget.prototype.simulateLabelClick = function () {
11488 if ( !this.isDisabled() ) {
11489 if ( this.$input.is( ':checkbox, :radio' ) ) {
11490 this.$input.click();
11491 }
11492 if ( this.$input.is( ':input' ) ) {
11493 this.$input[ 0 ].focus();
11494 }
11495 }
11496 };
11497
11498 /**
11499 * @inheritdoc
11500 */
11501 OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
11502 OO.ui.InputWidget.super.prototype.setDisabled.call( this, state );
11503 if ( this.$input ) {
11504 this.$input.prop( 'disabled', this.isDisabled() );
11505 }
11506 return this;
11507 };
11508
11509 /**
11510 * Focus the input.
11511 *
11512 * @chainable
11513 */
11514 OO.ui.InputWidget.prototype.focus = function () {
11515 this.$input[ 0 ].focus();
11516 return this;
11517 };
11518
11519 /**
11520 * Blur the input.
11521 *
11522 * @chainable
11523 */
11524 OO.ui.InputWidget.prototype.blur = function () {
11525 this.$input[ 0 ].blur();
11526 return this;
11527 };
11528
11529 /**
11530 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
11531 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
11532 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
11533 * HTML `<button/>` (the default) or an HTML `<input/>` tags. See the
11534 * [OOjs UI documentation on MediaWiki] [1] for more information.
11535 *
11536 * @example
11537 * // A ButtonInputWidget rendered as an HTML button, the default.
11538 * var button = new OO.ui.ButtonInputWidget( {
11539 * label: 'Input button',
11540 * icon: 'check',
11541 * value: 'check'
11542 * } );
11543 * $( 'body' ).append( button.$element );
11544 *
11545 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
11546 *
11547 * @class
11548 * @extends OO.ui.InputWidget
11549 * @mixins OO.ui.ButtonElement
11550 * @mixins OO.ui.IconElement
11551 * @mixins OO.ui.IndicatorElement
11552 * @mixins OO.ui.LabelElement
11553 * @mixins OO.ui.TitledElement
11554 *
11555 * @constructor
11556 * @param {Object} [config] Configuration options
11557 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
11558 * @cfg {boolean} [useInputTag=false] Use an `<input/>` tag instead of a `<button/>` tag, the default.
11559 * Widgets configured to be an `<input/>` do not support {@link #icon icons} and {@link #indicator indicators},
11560 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
11561 * be set to `true` when there’s need to support IE6 in a form with multiple buttons.
11562 */
11563 OO.ui.ButtonInputWidget = function OoUiButtonInputWidget( config ) {
11564 // Configuration initialization
11565 config = $.extend( { type: 'button', useInputTag: false }, config );
11566
11567 // Properties (must be set before parent constructor, which calls #setValue)
11568 this.useInputTag = config.useInputTag;
11569
11570 // Parent constructor
11571 OO.ui.ButtonInputWidget.super.call( this, config );
11572
11573 // Mixin constructors
11574 OO.ui.ButtonElement.call( this, $.extend( {}, config, { $button: this.$input } ) );
11575 OO.ui.IconElement.call( this, config );
11576 OO.ui.IndicatorElement.call( this, config );
11577 OO.ui.LabelElement.call( this, config );
11578 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
11579
11580 // Initialization
11581 if ( !config.useInputTag ) {
11582 this.$input.append( this.$icon, this.$label, this.$indicator );
11583 }
11584 this.$element.addClass( 'oo-ui-buttonInputWidget' );
11585 };
11586
11587 /* Setup */
11588
11589 OO.inheritClass( OO.ui.ButtonInputWidget, OO.ui.InputWidget );
11590 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.ButtonElement );
11591 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.IconElement );
11592 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.IndicatorElement );
11593 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.LabelElement );
11594 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.TitledElement );
11595
11596 /* Methods */
11597
11598 /**
11599 * @inheritdoc
11600 * @private
11601 */
11602 OO.ui.ButtonInputWidget.prototype.getInputElement = function ( config ) {
11603 var html = '<' + ( config.useInputTag ? 'input' : 'button' ) + ' type="' + config.type + '">';
11604 return $( html );
11605 };
11606
11607 /**
11608 * Set label value.
11609 *
11610 * If #useInputTag is `true`, the label is set as the `value` of the `<input/>` tag.
11611 *
11612 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
11613 * text, or `null` for no label
11614 * @chainable
11615 */
11616 OO.ui.ButtonInputWidget.prototype.setLabel = function ( label ) {
11617 OO.ui.LabelElement.prototype.setLabel.call( this, label );
11618
11619 if ( this.useInputTag ) {
11620 if ( typeof label === 'function' ) {
11621 label = OO.ui.resolveMsg( label );
11622 }
11623 if ( label instanceof jQuery ) {
11624 label = label.text();
11625 }
11626 if ( !label ) {
11627 label = '';
11628 }
11629 this.$input.val( label );
11630 }
11631
11632 return this;
11633 };
11634
11635 /**
11636 * Set the value of the input.
11637 *
11638 * This method is disabled for button inputs configured as {@link #useInputTag <input/> tags}, as
11639 * they do not support {@link #value values}.
11640 *
11641 * @param {string} value New value
11642 * @chainable
11643 */
11644 OO.ui.ButtonInputWidget.prototype.setValue = function ( value ) {
11645 if ( !this.useInputTag ) {
11646 OO.ui.ButtonInputWidget.super.prototype.setValue.call( this, value );
11647 }
11648 return this;
11649 };
11650
11651 /**
11652 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
11653 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
11654 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
11655 * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
11656 *
11657 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
11658 *
11659 * @example
11660 * // An example of selected, unselected, and disabled checkbox inputs
11661 * var checkbox1=new OO.ui.CheckboxInputWidget( {
11662 * value: 'a',
11663 * selected: true
11664 * } );
11665 * var checkbox2=new OO.ui.CheckboxInputWidget( {
11666 * value: 'b'
11667 * } );
11668 * var checkbox3=new OO.ui.CheckboxInputWidget( {
11669 * value:'c',
11670 * disabled: true
11671 * } );
11672 * // Create a fieldset layout with fields for each checkbox.
11673 * var fieldset = new OO.ui.FieldsetLayout( {
11674 * label: 'Checkboxes'
11675 * } );
11676 * fieldset.addItems( [
11677 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
11678 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
11679 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
11680 * ] );
11681 * $( 'body' ).append( fieldset.$element );
11682 *
11683 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
11684 *
11685 * @class
11686 * @extends OO.ui.InputWidget
11687 *
11688 * @constructor
11689 * @param {Object} [config] Configuration options
11690 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
11691 */
11692 OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
11693 // Configuration initialization
11694 config = config || {};
11695
11696 // Parent constructor
11697 OO.ui.CheckboxInputWidget.super.call( this, config );
11698
11699 // Initialization
11700 this.$element.addClass( 'oo-ui-checkboxInputWidget' );
11701 this.setSelected( config.selected !== undefined ? config.selected : false );
11702 };
11703
11704 /* Setup */
11705
11706 OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
11707
11708 /* Methods */
11709
11710 /**
11711 * @inheritdoc
11712 * @private
11713 */
11714 OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
11715 return $( '<input type="checkbox" />' );
11716 };
11717
11718 /**
11719 * @inheritdoc
11720 */
11721 OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
11722 var widget = this;
11723 if ( !this.isDisabled() ) {
11724 // Allow the stack to clear so the value will be updated
11725 setTimeout( function () {
11726 widget.setSelected( widget.$input.prop( 'checked' ) );
11727 } );
11728 }
11729 };
11730
11731 /**
11732 * Set selection state of this checkbox.
11733 *
11734 * @param {boolean} state `true` for selected
11735 * @chainable
11736 */
11737 OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state ) {
11738 state = !!state;
11739 if ( this.selected !== state ) {
11740 this.selected = state;
11741 this.$input.prop( 'checked', this.selected );
11742 this.emit( 'change', this.selected );
11743 }
11744 return this;
11745 };
11746
11747 /**
11748 * Check if this checkbox is selected.
11749 *
11750 * @return {boolean} Checkbox is selected
11751 */
11752 OO.ui.CheckboxInputWidget.prototype.isSelected = function () {
11753 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
11754 // it, and we won't know unless they're kind enough to trigger a 'change' event.
11755 var selected = this.$input.prop( 'checked' );
11756 if ( this.selected !== selected ) {
11757 this.setSelected( selected );
11758 }
11759 return this.selected;
11760 };
11761
11762 /**
11763 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
11764 * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
11765 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
11766 * more information about input widgets.
11767 *
11768 * @example
11769 * // Example: A DropdownInputWidget with three options
11770 * var dropDown = new OO.ui.DropdownInputWidget( {
11771 * label: 'Dropdown menu: Select a menu option',
11772 * options: [
11773 * { data: 'a', label: 'First' } ,
11774 * { data: 'b', label: 'Second'} ,
11775 * { data: 'c', label: 'Third' }
11776 * ]
11777 * } );
11778 * $( 'body' ).append( dropDown.$element );
11779 *
11780 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
11781 *
11782 * @class
11783 * @extends OO.ui.InputWidget
11784 *
11785 * @constructor
11786 * @param {Object} [config] Configuration options
11787 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
11788 */
11789 OO.ui.DropdownInputWidget = function OoUiDropdownInputWidget( config ) {
11790 // Configuration initialization
11791 config = config || {};
11792
11793 // Properties (must be done before parent constructor which calls #setDisabled)
11794 this.dropdownWidget = new OO.ui.DropdownWidget();
11795
11796 // Parent constructor
11797 OO.ui.DropdownInputWidget.super.call( this, config );
11798
11799 // Events
11800 this.dropdownWidget.getMenu().connect( this, { select: 'onMenuSelect' } );
11801
11802 // Initialization
11803 this.setOptions( config.options || [] );
11804 this.$element
11805 .addClass( 'oo-ui-dropdownInputWidget' )
11806 .append( this.dropdownWidget.$element );
11807 };
11808
11809 /* Setup */
11810
11811 OO.inheritClass( OO.ui.DropdownInputWidget, OO.ui.InputWidget );
11812
11813 /* Methods */
11814
11815 /**
11816 * @inheritdoc
11817 * @private
11818 */
11819 OO.ui.DropdownInputWidget.prototype.getInputElement = function () {
11820 return $( '<input type="hidden">' );
11821 };
11822
11823 /**
11824 * Handles menu select events.
11825 *
11826 * @private
11827 * @param {OO.ui.MenuOptionWidget} item Selected menu item
11828 */
11829 OO.ui.DropdownInputWidget.prototype.onMenuSelect = function ( item ) {
11830 this.setValue( item.getData() );
11831 };
11832
11833 /**
11834 * @inheritdoc
11835 */
11836 OO.ui.DropdownInputWidget.prototype.setValue = function ( value ) {
11837 var item = this.dropdownWidget.getMenu().getItemFromData( value );
11838 if ( item ) {
11839 this.dropdownWidget.getMenu().selectItem( item );
11840 }
11841 OO.ui.DropdownInputWidget.super.prototype.setValue.call( this, value );
11842 return this;
11843 };
11844
11845 /**
11846 * @inheritdoc
11847 */
11848 OO.ui.DropdownInputWidget.prototype.setDisabled = function ( state ) {
11849 this.dropdownWidget.setDisabled( state );
11850 OO.ui.DropdownInputWidget.super.prototype.setDisabled.call( this, state );
11851 return this;
11852 };
11853
11854 /**
11855 * Set the options available for this input.
11856 *
11857 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
11858 * @chainable
11859 */
11860 OO.ui.DropdownInputWidget.prototype.setOptions = function ( options ) {
11861 var value = this.getValue();
11862
11863 // Rebuild the dropdown menu
11864 this.dropdownWidget.getMenu()
11865 .clearItems()
11866 .addItems( options.map( function ( opt ) {
11867 return new OO.ui.MenuOptionWidget( {
11868 data: opt.data,
11869 label: opt.label !== undefined ? opt.label : opt.data
11870 } );
11871 } ) );
11872
11873 // Restore the previous value, or reset to something sensible
11874 if ( this.dropdownWidget.getMenu().getItemFromData( value ) ) {
11875 // Previous value is still available, ensure consistency with the dropdown
11876 this.setValue( value );
11877 } else {
11878 // No longer valid, reset
11879 if ( options.length ) {
11880 this.setValue( options[ 0 ].data );
11881 }
11882 }
11883
11884 return this;
11885 };
11886
11887 /**
11888 * @inheritdoc
11889 */
11890 OO.ui.DropdownInputWidget.prototype.focus = function () {
11891 this.dropdownWidget.getMenu().toggle( true );
11892 return this;
11893 };
11894
11895 /**
11896 * @inheritdoc
11897 */
11898 OO.ui.DropdownInputWidget.prototype.blur = function () {
11899 this.dropdownWidget.getMenu().toggle( false );
11900 return this;
11901 };
11902
11903 /**
11904 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
11905 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
11906 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
11907 * please see the [OOjs UI documentation on MediaWiki][1].
11908 *
11909 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
11910 *
11911 * @example
11912 * // An example of selected, unselected, and disabled radio inputs
11913 * var radio1 = new OO.ui.RadioInputWidget( {
11914 * value: 'a',
11915 * selected: true
11916 * } );
11917 * var radio2 = new OO.ui.RadioInputWidget( {
11918 * value: 'b'
11919 * } );
11920 * var radio3 = new OO.ui.RadioInputWidget( {
11921 * value: 'c',
11922 * disabled: true
11923 * } );
11924 * // Create a fieldset layout with fields for each radio button.
11925 * var fieldset = new OO.ui.FieldsetLayout( {
11926 * label: 'Radio inputs'
11927 * } );
11928 * fieldset.addItems( [
11929 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
11930 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
11931 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
11932 * ] );
11933 * $( 'body' ).append( fieldset.$element );
11934 *
11935 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
11936 *
11937 * @class
11938 * @extends OO.ui.InputWidget
11939 *
11940 * @constructor
11941 * @param {Object} [config] Configuration options
11942 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
11943 */
11944 OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) {
11945 // Configuration initialization
11946 config = config || {};
11947
11948 // Parent constructor
11949 OO.ui.RadioInputWidget.super.call( this, config );
11950
11951 // Initialization
11952 this.$element.addClass( 'oo-ui-radioInputWidget' );
11953 this.setSelected( config.selected !== undefined ? config.selected : false );
11954 };
11955
11956 /* Setup */
11957
11958 OO.inheritClass( OO.ui.RadioInputWidget, OO.ui.InputWidget );
11959
11960 /* Methods */
11961
11962 /**
11963 * @inheritdoc
11964 * @private
11965 */
11966 OO.ui.RadioInputWidget.prototype.getInputElement = function () {
11967 return $( '<input type="radio" />' );
11968 };
11969
11970 /**
11971 * @inheritdoc
11972 */
11973 OO.ui.RadioInputWidget.prototype.onEdit = function () {
11974 // RadioInputWidget doesn't track its state.
11975 };
11976
11977 /**
11978 * Set selection state of this radio button.
11979 *
11980 * @param {boolean} state `true` for selected
11981 * @chainable
11982 */
11983 OO.ui.RadioInputWidget.prototype.setSelected = function ( state ) {
11984 // RadioInputWidget doesn't track its state.
11985 this.$input.prop( 'checked', state );
11986 return this;
11987 };
11988
11989 /**
11990 * Check if this radio button is selected.
11991 *
11992 * @return {boolean} Radio is selected
11993 */
11994 OO.ui.RadioInputWidget.prototype.isSelected = function () {
11995 return this.$input.prop( 'checked' );
11996 };
11997
11998 /**
11999 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
12000 * size of the field as well as its presentation. In addition, these widgets can be configured
12001 * with {@link OO.ui.IconElement icons}, {@link OO.ui.IndicatorElement indicators}, an optional
12002 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
12003 * which modifies incoming values rather than validating them.
12004 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
12005 *
12006 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
12007 *
12008 * @example
12009 * // Example of a text input widget
12010 * var textInput = new OO.ui.TextInputWidget( {
12011 * value: 'Text input'
12012 * } )
12013 * $( 'body' ).append( textInput.$element );
12014 *
12015 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
12016 *
12017 * @class
12018 * @extends OO.ui.InputWidget
12019 * @mixins OO.ui.IconElement
12020 * @mixins OO.ui.IndicatorElement
12021 * @mixins OO.ui.PendingElement
12022 * @mixins OO.ui.LabelElement
12023 *
12024 * @constructor
12025 * @param {Object} [config] Configuration options
12026 * @cfg {string} [type='text'] The value of the HTML `type` attribute
12027 * @cfg {string} [placeholder] Placeholder text
12028 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
12029 * instruct the browser to focus this widget.
12030 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
12031 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
12032 * @cfg {boolean} [multiline=false] Allow multiple lines of text
12033 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
12034 * Use the #maxRows config to specify a maximum number of displayed rows.
12035 * @cfg {boolean} [maxRows=10] Maximum number of rows to display when #autosize is set to true.
12036 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
12037 * the value or placeholder text: `'before'` or `'after'`
12038 * @cfg {boolean} [required=false] Mark the field as required
12039 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
12040 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
12041 * (the value must contain only numbers); when RegExp, a regular expression that must match the
12042 * value for it to be considered valid; when Function, a function receiving the value as parameter
12043 * that must return true, or promise resolving to true, for it to be considered valid.
12044 */
12045 OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
12046 // Configuration initialization
12047 config = $.extend( {
12048 type: 'text',
12049 labelPosition: 'after',
12050 maxRows: 10
12051 }, config );
12052
12053 // Parent constructor
12054 OO.ui.TextInputWidget.super.call( this, config );
12055
12056 // Mixin constructors
12057 OO.ui.IconElement.call( this, config );
12058 OO.ui.IndicatorElement.call( this, config );
12059 OO.ui.PendingElement.call( this, config );
12060 OO.ui.LabelElement.call( this, config );
12061
12062 // Properties
12063 this.readOnly = false;
12064 this.multiline = !!config.multiline;
12065 this.autosize = !!config.autosize;
12066 this.maxRows = config.maxRows;
12067 this.validate = null;
12068
12069 // Clone for resizing
12070 if ( this.autosize ) {
12071 this.$clone = this.$input
12072 .clone()
12073 .insertAfter( this.$input )
12074 .attr( 'aria-hidden', 'true' )
12075 .addClass( 'oo-ui-element-hidden' );
12076 }
12077
12078 this.setValidation( config.validate );
12079 this.setLabelPosition( config.labelPosition );
12080
12081 // Events
12082 this.$input.on( {
12083 keypress: this.onKeyPress.bind( this ),
12084 blur: this.setValidityFlag.bind( this )
12085 } );
12086 this.$input.one( {
12087 focus: this.onElementAttach.bind( this )
12088 } );
12089 this.$icon.on( 'mousedown', this.onIconMouseDown.bind( this ) );
12090 this.$indicator.on( 'mousedown', this.onIndicatorMouseDown.bind( this ) );
12091 this.on( 'labelChange', this.updatePosition.bind( this ) );
12092
12093 // Initialization
12094 this.$element
12095 .addClass( 'oo-ui-textInputWidget' )
12096 .append( this.$icon, this.$indicator );
12097 this.setReadOnly( !!config.readOnly );
12098 if ( config.placeholder ) {
12099 this.$input.attr( 'placeholder', config.placeholder );
12100 }
12101 if ( config.maxLength !== undefined ) {
12102 this.$input.attr( 'maxlength', config.maxLength );
12103 }
12104 if ( config.autofocus ) {
12105 this.$input.attr( 'autofocus', 'autofocus' );
12106 }
12107 if ( config.required ) {
12108 this.$input.attr( 'required', 'true' );
12109 }
12110 if ( this.label || config.autosize ) {
12111 this.installParentChangeDetector();
12112 }
12113 };
12114
12115 /* Setup */
12116
12117 OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
12118 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IconElement );
12119 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IndicatorElement );
12120 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.PendingElement );
12121 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.LabelElement );
12122
12123 /* Static properties */
12124
12125 OO.ui.TextInputWidget.static.validationPatterns = {
12126 'non-empty': /.+/,
12127 integer: /^\d+$/
12128 };
12129
12130 /* Events */
12131
12132 /**
12133 * An `enter` event is emitted when the user presses 'enter' inside the text box.
12134 *
12135 * Not emitted if the input is multiline.
12136 *
12137 * @event enter
12138 */
12139
12140 /* Methods */
12141
12142 /**
12143 * Handle icon mouse down events.
12144 *
12145 * @private
12146 * @param {jQuery.Event} e Mouse down event
12147 * @fires icon
12148 */
12149 OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
12150 if ( e.which === 1 ) {
12151 this.$input[ 0 ].focus();
12152 return false;
12153 }
12154 };
12155
12156 /**
12157 * Handle indicator mouse down events.
12158 *
12159 * @private
12160 * @param {jQuery.Event} e Mouse down event
12161 * @fires indicator
12162 */
12163 OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
12164 if ( e.which === 1 ) {
12165 this.$input[ 0 ].focus();
12166 return false;
12167 }
12168 };
12169
12170 /**
12171 * Handle key press events.
12172 *
12173 * @private
12174 * @param {jQuery.Event} e Key press event
12175 * @fires enter If enter key is pressed and input is not multiline
12176 */
12177 OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
12178 if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) {
12179 this.emit( 'enter', e );
12180 }
12181 };
12182
12183 /**
12184 * Handle element attach events.
12185 *
12186 * @private
12187 * @param {jQuery.Event} e Element attach event
12188 */
12189 OO.ui.TextInputWidget.prototype.onElementAttach = function () {
12190 // Any previously calculated size is now probably invalid if we reattached elsewhere
12191 this.valCache = null;
12192 this.adjustSize();
12193 this.positionLabel();
12194 };
12195
12196 /**
12197 * @inheritdoc
12198 */
12199 OO.ui.TextInputWidget.prototype.onEdit = function () {
12200 this.adjustSize();
12201
12202 // Parent method
12203 return OO.ui.TextInputWidget.super.prototype.onEdit.call( this );
12204 };
12205
12206 /**
12207 * @inheritdoc
12208 */
12209 OO.ui.TextInputWidget.prototype.setValue = function ( value ) {
12210 // Parent method
12211 OO.ui.TextInputWidget.super.prototype.setValue.call( this, value );
12212
12213 this.setValidityFlag();
12214 this.adjustSize();
12215 return this;
12216 };
12217
12218 /**
12219 * Check if the input is {@link #readOnly read-only}.
12220 *
12221 * @return {boolean}
12222 */
12223 OO.ui.TextInputWidget.prototype.isReadOnly = function () {
12224 return this.readOnly;
12225 };
12226
12227 /**
12228 * Set the {@link #readOnly read-only} state of the input.
12229 *
12230 * @param {boolean} state Make input read-only
12231 * @chainable
12232 */
12233 OO.ui.TextInputWidget.prototype.setReadOnly = function ( state ) {
12234 this.readOnly = !!state;
12235 this.$input.prop( 'readOnly', this.readOnly );
12236 return this;
12237 };
12238
12239 /**
12240 * Support function for making #onElementAttach work across browsers.
12241 *
12242 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
12243 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
12244 *
12245 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
12246 * first time that the element gets attached to the documented.
12247 */
12248 OO.ui.TextInputWidget.prototype.installParentChangeDetector = function () {
12249 var mutationObserver, onRemove, topmostNode, fakeParentNode,
12250 MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
12251 widget = this;
12252
12253 if ( MutationObserver ) {
12254 // The new way. If only it wasn't so ugly.
12255
12256 if ( this.$element.closest( 'html' ).length ) {
12257 // Widget is attached already, do nothing. This breaks the functionality of this function when
12258 // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
12259 // would require observation of the whole document, which would hurt performance of other,
12260 // more important code.
12261 return;
12262 }
12263
12264 // Find topmost node in the tree
12265 topmostNode = this.$element[0];
12266 while ( topmostNode.parentNode ) {
12267 topmostNode = topmostNode.parentNode;
12268 }
12269
12270 // We have no way to detect the $element being attached somewhere without observing the entire
12271 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
12272 // parent node of $element, and instead detect when $element is removed from it (and thus
12273 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
12274 // doesn't get attached, we end up back here and create the parent.
12275
12276 mutationObserver = new MutationObserver( function ( mutations ) {
12277 var i, j, removedNodes;
12278 for ( i = 0; i < mutations.length; i++ ) {
12279 removedNodes = mutations[ i ].removedNodes;
12280 for ( j = 0; j < removedNodes.length; j++ ) {
12281 if ( removedNodes[ j ] === topmostNode ) {
12282 setTimeout( onRemove, 0 );
12283 return;
12284 }
12285 }
12286 }
12287 } );
12288
12289 onRemove = function () {
12290 // If the node was attached somewhere else, report it
12291 if ( widget.$element.closest( 'html' ).length ) {
12292 widget.onElementAttach();
12293 }
12294 mutationObserver.disconnect();
12295 widget.installParentChangeDetector();
12296 };
12297
12298 // Create a fake parent and observe it
12299 fakeParentNode = $( '<div>' ).append( this.$element )[0];
12300 mutationObserver.observe( fakeParentNode, { childList: true } );
12301 } else {
12302 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
12303 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
12304 this.$element.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach.bind( this ) );
12305 }
12306 };
12307
12308 /**
12309 * Automatically adjust the size of the text input.
12310 *
12311 * This only affects #multiline inputs that are {@link #autosize autosized}.
12312 *
12313 * @chainable
12314 */
12315 OO.ui.TextInputWidget.prototype.adjustSize = function () {
12316 var scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError, idealHeight;
12317
12318 if ( this.multiline && this.autosize && this.$input.val() !== this.valCache ) {
12319 this.$clone
12320 .val( this.$input.val() )
12321 .attr( 'rows', '' )
12322 // Set inline height property to 0 to measure scroll height
12323 .css( 'height', 0 );
12324
12325 this.$clone.removeClass( 'oo-ui-element-hidden' );
12326
12327 this.valCache = this.$input.val();
12328
12329 scrollHeight = this.$clone[ 0 ].scrollHeight;
12330
12331 // Remove inline height property to measure natural heights
12332 this.$clone.css( 'height', '' );
12333 innerHeight = this.$clone.innerHeight();
12334 outerHeight = this.$clone.outerHeight();
12335
12336 // Measure max rows height
12337 this.$clone
12338 .attr( 'rows', this.maxRows )
12339 .css( 'height', 'auto' )
12340 .val( '' );
12341 maxInnerHeight = this.$clone.innerHeight();
12342
12343 // Difference between reported innerHeight and scrollHeight with no scrollbars present
12344 // Equals 1 on Blink-based browsers and 0 everywhere else
12345 measurementError = maxInnerHeight - this.$clone[ 0 ].scrollHeight;
12346 idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
12347
12348 this.$clone.addClass( 'oo-ui-element-hidden' );
12349
12350 // Only apply inline height when expansion beyond natural height is needed
12351 if ( idealHeight > innerHeight ) {
12352 // Use the difference between the inner and outer height as a buffer
12353 this.$input.css( 'height', idealHeight + ( outerHeight - innerHeight ) );
12354 } else {
12355 this.$input.css( 'height', '' );
12356 }
12357 }
12358 return this;
12359 };
12360
12361 /**
12362 * @inheritdoc
12363 * @private
12364 */
12365 OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
12366 return config.multiline ? $( '<textarea>' ) : $( '<input type="' + config.type + '" />' );
12367 };
12368
12369 /**
12370 * Check if the input supports multiple lines.
12371 *
12372 * @return {boolean}
12373 */
12374 OO.ui.TextInputWidget.prototype.isMultiline = function () {
12375 return !!this.multiline;
12376 };
12377
12378 /**
12379 * Check if the input automatically adjusts its size.
12380 *
12381 * @return {boolean}
12382 */
12383 OO.ui.TextInputWidget.prototype.isAutosizing = function () {
12384 return !!this.autosize;
12385 };
12386
12387 /**
12388 * Select the entire text of the input.
12389 *
12390 * @chainable
12391 */
12392 OO.ui.TextInputWidget.prototype.select = function () {
12393 this.$input.select();
12394 return this;
12395 };
12396
12397 /**
12398 * Set the validation pattern.
12399 *
12400 * The validation pattern is either a regular expression, a function, or the symbolic name of a
12401 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
12402 * value must contain only numbers).
12403 *
12404 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
12405 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
12406 */
12407 OO.ui.TextInputWidget.prototype.setValidation = function ( validate ) {
12408 if ( validate instanceof RegExp || validate instanceof Function ) {
12409 this.validate = validate;
12410 } else {
12411 this.validate = this.constructor.static.validationPatterns[ validate ] || /.*/;
12412 }
12413 };
12414
12415 /**
12416 * Sets the 'invalid' flag appropriately.
12417 */
12418 OO.ui.TextInputWidget.prototype.setValidityFlag = function () {
12419 var widget = this;
12420 this.isValid().done( function ( valid ) {
12421 widget.setFlags( { invalid: !valid } );
12422 } );
12423 };
12424
12425 /**
12426 * Check if a value is valid.
12427 *
12428 * This method returns a promise that resolves with a boolean `true` if the current value is
12429 * considered valid according to the supplied {@link #validate validation pattern}.
12430 *
12431 * @return {jQuery.Promise} A promise that resolves to a boolean `true` if the value is valid.
12432 */
12433 OO.ui.TextInputWidget.prototype.isValid = function () {
12434 if ( this.validate instanceof Function ) {
12435 var result = this.validate( this.getValue() );
12436 if ( $.isFunction( result.promise ) ) {
12437 return result.promise();
12438 } else {
12439 return $.Deferred().resolve( !!result ).promise();
12440 }
12441 } else {
12442 return $.Deferred().resolve( !!this.getValue().match( this.validate ) ).promise();
12443 }
12444 };
12445
12446 /**
12447 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
12448 *
12449 * @param {string} labelPosition Label position, 'before' or 'after'
12450 * @chainable
12451 */
12452 OO.ui.TextInputWidget.prototype.setLabelPosition = function ( labelPosition ) {
12453 this.labelPosition = labelPosition;
12454 this.updatePosition();
12455 return this;
12456 };
12457
12458 /**
12459 * Deprecated alias of #setLabelPosition
12460 *
12461 * @deprecated Use setLabelPosition instead.
12462 */
12463 OO.ui.TextInputWidget.prototype.setPosition =
12464 OO.ui.TextInputWidget.prototype.setLabelPosition;
12465
12466 /**
12467 * Update the position of the inline label.
12468 *
12469 * This method is called by #setLabelPosition, and can also be called on its own if
12470 * something causes the label to be mispositioned.
12471 *
12472 *
12473 * @chainable
12474 */
12475 OO.ui.TextInputWidget.prototype.updatePosition = function () {
12476 var after = this.labelPosition === 'after';
12477
12478 this.$element
12479 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label && after )
12480 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label && !after );
12481
12482 if ( this.label ) {
12483 this.positionLabel();
12484 }
12485
12486 return this;
12487 };
12488
12489 /**
12490 * Position the label by setting the correct padding on the input.
12491 *
12492 * @private
12493 * @chainable
12494 */
12495 OO.ui.TextInputWidget.prototype.positionLabel = function () {
12496 // Clear old values
12497 this.$input
12498 // Clear old values if present
12499 .css( {
12500 'padding-right': '',
12501 'padding-left': ''
12502 } );
12503
12504 if ( this.label ) {
12505 this.$element.append( this.$label );
12506 } else {
12507 this.$label.detach();
12508 return;
12509 }
12510
12511 var after = this.labelPosition === 'after',
12512 rtl = this.$element.css( 'direction' ) === 'rtl',
12513 property = after === rtl ? 'padding-left' : 'padding-right';
12514
12515 this.$input.css( property, this.$label.outerWidth( true ) );
12516
12517 return this;
12518 };
12519
12520 /**
12521 * ComboBoxWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
12522 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
12523 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
12524 *
12525 * - by typing a value in the text input field. If the value exactly matches the value of a menu
12526 * option, that option will appear to be selected.
12527 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
12528 * input field.
12529 *
12530 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
12531 *
12532 * @example
12533 * // Example: A ComboBoxWidget.
12534 * var comboBox = new OO.ui.ComboBoxWidget( {
12535 * label: 'ComboBoxWidget',
12536 * input: { value: 'Option One' },
12537 * menu: {
12538 * items: [
12539 * new OO.ui.MenuOptionWidget( {
12540 * data: 'Option 1',
12541 * label: 'Option One'
12542 * } ),
12543 * new OO.ui.MenuOptionWidget( {
12544 * data: 'Option 2',
12545 * label: 'Option Two'
12546 * } ),
12547 * new OO.ui.MenuOptionWidget( {
12548 * data: 'Option 3',
12549 * label: 'Option Three'
12550 * } ),
12551 * new OO.ui.MenuOptionWidget( {
12552 * data: 'Option 4',
12553 * label: 'Option Four'
12554 * } ),
12555 * new OO.ui.MenuOptionWidget( {
12556 * data: 'Option 5',
12557 * label: 'Option Five'
12558 * } )
12559 * ]
12560 * }
12561 * } );
12562 * $( 'body' ).append( comboBox.$element );
12563 *
12564 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
12565 *
12566 * @class
12567 * @extends OO.ui.Widget
12568 * @mixins OO.ui.TabIndexedElement
12569 *
12570 * @constructor
12571 * @param {Object} [config] Configuration options
12572 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}.
12573 * @cfg {Object} [input] Configuration options to pass to the {@link OO.ui.TextInputWidget text input widget}.
12574 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
12575 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
12576 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
12577 */
12578 OO.ui.ComboBoxWidget = function OoUiComboBoxWidget( config ) {
12579 // Configuration initialization
12580 config = config || {};
12581
12582 // Parent constructor
12583 OO.ui.ComboBoxWidget.super.call( this, config );
12584
12585 // Properties (must be set before TabIndexedElement constructor call)
12586 this.$indicator = this.$( '<span>' );
12587
12588 // Mixin constructors
12589 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$indicator } ) );
12590
12591 // Properties
12592 this.$overlay = config.$overlay || this.$element;
12593 this.input = new OO.ui.TextInputWidget( $.extend(
12594 {
12595 indicator: 'down',
12596 $indicator: this.$indicator,
12597 disabled: this.isDisabled()
12598 },
12599 config.input
12600 ) );
12601 this.input.$input.eq( 0 ).attr( {
12602 role: 'combobox',
12603 'aria-autocomplete': 'list'
12604 } );
12605 this.menu = new OO.ui.TextInputMenuSelectWidget( this.input, $.extend(
12606 {
12607 widget: this,
12608 input: this.input,
12609 disabled: this.isDisabled()
12610 },
12611 config.menu
12612 ) );
12613
12614 // Events
12615 this.$indicator.on( {
12616 click: this.onClick.bind( this ),
12617 keypress: this.onKeyPress.bind( this )
12618 } );
12619 this.input.connect( this, {
12620 change: 'onInputChange',
12621 enter: 'onInputEnter'
12622 } );
12623 this.menu.connect( this, {
12624 choose: 'onMenuChoose',
12625 add: 'onMenuItemsChange',
12626 remove: 'onMenuItemsChange'
12627 } );
12628
12629 // Initialization
12630 this.$element.addClass( 'oo-ui-comboBoxWidget' ).append( this.input.$element );
12631 this.$overlay.append( this.menu.$element );
12632 this.onMenuItemsChange();
12633 };
12634
12635 /* Setup */
12636
12637 OO.inheritClass( OO.ui.ComboBoxWidget, OO.ui.Widget );
12638 OO.mixinClass( OO.ui.ComboBoxWidget, OO.ui.TabIndexedElement );
12639
12640 /* Methods */
12641
12642 /**
12643 * Get the combobox's menu.
12644 * @return {OO.ui.TextInputMenuSelectWidget} Menu widget
12645 */
12646 OO.ui.ComboBoxWidget.prototype.getMenu = function () {
12647 return this.menu;
12648 };
12649
12650 /**
12651 * Handle input change events.
12652 *
12653 * @private
12654 * @param {string} value New value
12655 */
12656 OO.ui.ComboBoxWidget.prototype.onInputChange = function ( value ) {
12657 var match = this.menu.getItemFromData( value );
12658
12659 this.menu.selectItem( match );
12660 if ( this.menu.getHighlightedItem() ) {
12661 this.menu.highlightItem( match );
12662 }
12663
12664 if ( !this.isDisabled() ) {
12665 this.menu.toggle( true );
12666 }
12667 };
12668
12669 /**
12670 * Handle mouse click events.
12671 *
12672 *
12673 * @private
12674 * @param {jQuery.Event} e Mouse click event
12675 */
12676 OO.ui.ComboBoxWidget.prototype.onClick = function ( e ) {
12677 if ( !this.isDisabled() && e.which === 1 ) {
12678 this.menu.toggle();
12679 this.input.$input[ 0 ].focus();
12680 }
12681 return false;
12682 };
12683
12684 /**
12685 * Handle key press events.
12686 *
12687 *
12688 * @private
12689 * @param {jQuery.Event} e Key press event
12690 */
12691 OO.ui.ComboBoxWidget.prototype.onKeyPress = function ( e ) {
12692 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
12693 this.menu.toggle();
12694 this.input.$input[ 0 ].focus();
12695 return false;
12696 }
12697 };
12698
12699 /**
12700 * Handle input enter events.
12701 *
12702 * @private
12703 */
12704 OO.ui.ComboBoxWidget.prototype.onInputEnter = function () {
12705 if ( !this.isDisabled() ) {
12706 this.menu.toggle( false );
12707 }
12708 };
12709
12710 /**
12711 * Handle menu choose events.
12712 *
12713 * @private
12714 * @param {OO.ui.OptionWidget} item Chosen item
12715 */
12716 OO.ui.ComboBoxWidget.prototype.onMenuChoose = function ( item ) {
12717 this.input.setValue( item.getData() );
12718 };
12719
12720 /**
12721 * Handle menu item change events.
12722 *
12723 * @private
12724 */
12725 OO.ui.ComboBoxWidget.prototype.onMenuItemsChange = function () {
12726 var match = this.menu.getItemFromData( this.input.getValue() );
12727 this.menu.selectItem( match );
12728 if ( this.menu.getHighlightedItem() ) {
12729 this.menu.highlightItem( match );
12730 }
12731 this.$element.toggleClass( 'oo-ui-comboBoxWidget-empty', this.menu.isEmpty() );
12732 };
12733
12734 /**
12735 * @inheritdoc
12736 */
12737 OO.ui.ComboBoxWidget.prototype.setDisabled = function ( disabled ) {
12738 // Parent method
12739 OO.ui.ComboBoxWidget.super.prototype.setDisabled.call( this, disabled );
12740
12741 if ( this.input ) {
12742 this.input.setDisabled( this.isDisabled() );
12743 }
12744 if ( this.menu ) {
12745 this.menu.setDisabled( this.isDisabled() );
12746 }
12747
12748 return this;
12749 };
12750
12751 /**
12752 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
12753 * be configured with a `label` option that is set to a string, a label node, or a function:
12754 *
12755 * - String: a plaintext string
12756 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
12757 * label that includes a link or special styling, such as a gray color or additional graphical elements.
12758 * - Function: a function that will produce a string in the future. Functions are used
12759 * in cases where the value of the label is not currently defined.
12760 *
12761 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
12762 * will come into focus when the label is clicked.
12763 *
12764 * @example
12765 * // Examples of LabelWidgets
12766 * var label1 = new OO.ui.LabelWidget( {
12767 * label: 'plaintext label'
12768 * } );
12769 * var label2 = new OO.ui.LabelWidget( {
12770 * label: $( '<a href="default.html">jQuery label</a>' )
12771 * } );
12772 * // Create a fieldset layout with fields for each example
12773 * var fieldset = new OO.ui.FieldsetLayout();
12774 * fieldset.addItems( [
12775 * new OO.ui.FieldLayout( label1 ),
12776 * new OO.ui.FieldLayout( label2 )
12777 * ] );
12778 * $( 'body' ).append( fieldset.$element );
12779 *
12780 *
12781 * @class
12782 * @extends OO.ui.Widget
12783 * @mixins OO.ui.LabelElement
12784 *
12785 * @constructor
12786 * @param {Object} [config] Configuration options
12787 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
12788 * Clicking the label will focus the specified input field.
12789 */
12790 OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
12791 // Configuration initialization
12792 config = config || {};
12793
12794 // Parent constructor
12795 OO.ui.LabelWidget.super.call( this, config );
12796
12797 // Mixin constructors
12798 OO.ui.LabelElement.call( this, $.extend( {}, config, { $label: this.$element } ) );
12799 OO.ui.TitledElement.call( this, config );
12800
12801 // Properties
12802 this.input = config.input;
12803
12804 // Events
12805 if ( this.input instanceof OO.ui.InputWidget ) {
12806 this.$element.on( 'click', this.onClick.bind( this ) );
12807 }
12808
12809 // Initialization
12810 this.$element.addClass( 'oo-ui-labelWidget' );
12811 };
12812
12813 /* Setup */
12814
12815 OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
12816 OO.mixinClass( OO.ui.LabelWidget, OO.ui.LabelElement );
12817 OO.mixinClass( OO.ui.LabelWidget, OO.ui.TitledElement );
12818
12819 /* Static Properties */
12820
12821 OO.ui.LabelWidget.static.tagName = 'span';
12822
12823 /* Methods */
12824
12825 /**
12826 * Handles label mouse click events.
12827 *
12828 * @private
12829 * @param {jQuery.Event} e Mouse click event
12830 */
12831 OO.ui.LabelWidget.prototype.onClick = function () {
12832 this.input.simulateLabelClick();
12833 return false;
12834 };
12835
12836 /**
12837 * OptionWidgets are special elements that can be selected and configured with data. The
12838 * data is often unique for each option, but it does not have to be. OptionWidgets are used
12839 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
12840 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
12841 *
12842 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
12843 *
12844 * @class
12845 * @extends OO.ui.Widget
12846 * @mixins OO.ui.LabelElement
12847 * @mixins OO.ui.FlaggedElement
12848 *
12849 * @constructor
12850 * @param {Object} [config] Configuration options
12851 */
12852 OO.ui.OptionWidget = function OoUiOptionWidget( config ) {
12853 // Configuration initialization
12854 config = config || {};
12855
12856 // Parent constructor
12857 OO.ui.OptionWidget.super.call( this, config );
12858
12859 // Mixin constructors
12860 OO.ui.ItemWidget.call( this );
12861 OO.ui.LabelElement.call( this, config );
12862 OO.ui.FlaggedElement.call( this, config );
12863
12864 // Properties
12865 this.selected = false;
12866 this.highlighted = false;
12867 this.pressed = false;
12868
12869 // Initialization
12870 this.$element
12871 .data( 'oo-ui-optionWidget', this )
12872 .attr( 'role', 'option' )
12873 .addClass( 'oo-ui-optionWidget' )
12874 .append( this.$label );
12875 };
12876
12877 /* Setup */
12878
12879 OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
12880 OO.mixinClass( OO.ui.OptionWidget, OO.ui.ItemWidget );
12881 OO.mixinClass( OO.ui.OptionWidget, OO.ui.LabelElement );
12882 OO.mixinClass( OO.ui.OptionWidget, OO.ui.FlaggedElement );
12883
12884 /* Static Properties */
12885
12886 OO.ui.OptionWidget.static.selectable = true;
12887
12888 OO.ui.OptionWidget.static.highlightable = true;
12889
12890 OO.ui.OptionWidget.static.pressable = true;
12891
12892 OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
12893
12894 /* Methods */
12895
12896 /**
12897 * Check if the option can be selected.
12898 *
12899 * @return {boolean} Item is selectable
12900 */
12901 OO.ui.OptionWidget.prototype.isSelectable = function () {
12902 return this.constructor.static.selectable && !this.isDisabled();
12903 };
12904
12905 /**
12906 * Check if the option can be highlighted. A highlight indicates that the option
12907 * may be selected when a user presses enter or clicks. Disabled items cannot
12908 * be highlighted.
12909 *
12910 * @return {boolean} Item is highlightable
12911 */
12912 OO.ui.OptionWidget.prototype.isHighlightable = function () {
12913 return this.constructor.static.highlightable && !this.isDisabled();
12914 };
12915
12916 /**
12917 * Check if the option can be pressed. The pressed state occurs when a user mouses
12918 * down on an item, but has not yet let go of the mouse.
12919 *
12920 * @return {boolean} Item is pressable
12921 */
12922 OO.ui.OptionWidget.prototype.isPressable = function () {
12923 return this.constructor.static.pressable && !this.isDisabled();
12924 };
12925
12926 /**
12927 * Check if the option is selected.
12928 *
12929 * @return {boolean} Item is selected
12930 */
12931 OO.ui.OptionWidget.prototype.isSelected = function () {
12932 return this.selected;
12933 };
12934
12935 /**
12936 * Check if the option is highlighted. A highlight indicates that the
12937 * item may be selected when a user presses enter or clicks.
12938 *
12939 * @return {boolean} Item is highlighted
12940 */
12941 OO.ui.OptionWidget.prototype.isHighlighted = function () {
12942 return this.highlighted;
12943 };
12944
12945 /**
12946 * Check if the option is pressed. The pressed state occurs when a user mouses
12947 * down on an item, but has not yet let go of the mouse. The item may appear
12948 * selected, but it will not be selected until the user releases the mouse.
12949 *
12950 * @return {boolean} Item is pressed
12951 */
12952 OO.ui.OptionWidget.prototype.isPressed = function () {
12953 return this.pressed;
12954 };
12955
12956 /**
12957 * Set the option’s selected state. In general, all modifications to the selection
12958 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
12959 * method instead of this method.
12960 *
12961 * @param {boolean} [state=false] Select option
12962 * @chainable
12963 */
12964 OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
12965 if ( this.constructor.static.selectable ) {
12966 this.selected = !!state;
12967 this.$element
12968 .toggleClass( 'oo-ui-optionWidget-selected', state )
12969 .attr( 'aria-selected', state.toString() );
12970 if ( state && this.constructor.static.scrollIntoViewOnSelect ) {
12971 this.scrollElementIntoView();
12972 }
12973 this.updateThemeClasses();
12974 }
12975 return this;
12976 };
12977
12978 /**
12979 * Set the option’s highlighted state. In general, all programmatic
12980 * modifications to the highlight should be handled by the
12981 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
12982 * method instead of this method.
12983 *
12984 * @param {boolean} [state=false] Highlight option
12985 * @chainable
12986 */
12987 OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
12988 if ( this.constructor.static.highlightable ) {
12989 this.highlighted = !!state;
12990 this.$element.toggleClass( 'oo-ui-optionWidget-highlighted', state );
12991 this.updateThemeClasses();
12992 }
12993 return this;
12994 };
12995
12996 /**
12997 * Set the option’s pressed state. In general, all
12998 * programmatic modifications to the pressed state should be handled by the
12999 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
13000 * method instead of this method.
13001 *
13002 * @param {boolean} [state=false] Press option
13003 * @chainable
13004 */
13005 OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
13006 if ( this.constructor.static.pressable ) {
13007 this.pressed = !!state;
13008 this.$element.toggleClass( 'oo-ui-optionWidget-pressed', state );
13009 this.updateThemeClasses();
13010 }
13011 return this;
13012 };
13013
13014 /**
13015 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
13016 * with an {@link OO.ui.IconElement icon} and/or {@link OO.ui.IndicatorElement indicator}.
13017 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
13018 * options. For more information about options and selects, please see the
13019 * [OOjs UI documentation on MediaWiki][1].
13020 *
13021 * @example
13022 * // Decorated options in a select widget
13023 * var select = new OO.ui.SelectWidget( {
13024 * items: [
13025 * new OO.ui.DecoratedOptionWidget( {
13026 * data: 'a',
13027 * label: 'Option with icon',
13028 * icon: 'help'
13029 * } ),
13030 * new OO.ui.DecoratedOptionWidget( {
13031 * data: 'b',
13032 * label: 'Option with indicator',
13033 * indicator: 'next'
13034 * } )
13035 * ]
13036 * } );
13037 * $( 'body' ).append( select.$element );
13038 *
13039 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
13040 *
13041 * @class
13042 * @extends OO.ui.OptionWidget
13043 * @mixins OO.ui.IconElement
13044 * @mixins OO.ui.IndicatorElement
13045 *
13046 * @constructor
13047 * @param {Object} [config] Configuration options
13048 */
13049 OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( config ) {
13050 // Parent constructor
13051 OO.ui.DecoratedOptionWidget.super.call( this, config );
13052
13053 // Mixin constructors
13054 OO.ui.IconElement.call( this, config );
13055 OO.ui.IndicatorElement.call( this, config );
13056
13057 // Initialization
13058 this.$element
13059 .addClass( 'oo-ui-decoratedOptionWidget' )
13060 .prepend( this.$icon )
13061 .append( this.$indicator );
13062 };
13063
13064 /* Setup */
13065
13066 OO.inheritClass( OO.ui.DecoratedOptionWidget, OO.ui.OptionWidget );
13067 OO.mixinClass( OO.ui.OptionWidget, OO.ui.IconElement );
13068 OO.mixinClass( OO.ui.OptionWidget, OO.ui.IndicatorElement );
13069
13070 /**
13071 * ButtonOptionWidget is a special type of {@link OO.ui.ButtonElement button element} that
13072 * can be selected and configured with data. The class is
13073 * used with OO.ui.ButtonSelectWidget to create a selection of button options. Please see the
13074 * [OOjs UI documentation on MediaWiki] [1] for more information.
13075 *
13076 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_options
13077 *
13078 * @class
13079 * @extends OO.ui.DecoratedOptionWidget
13080 * @mixins OO.ui.ButtonElement
13081 * @mixins OO.ui.TabIndexedElement
13082 *
13083 * @constructor
13084 * @param {Object} [config] Configuration options
13085 */
13086 OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( config ) {
13087 // Configuration initialization
13088 config = $.extend( { tabIndex: -1 }, config );
13089
13090 // Parent constructor
13091 OO.ui.ButtonOptionWidget.super.call( this, config );
13092
13093 // Mixin constructors
13094 OO.ui.ButtonElement.call( this, config );
13095 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
13096
13097 // Initialization
13098 this.$element.addClass( 'oo-ui-buttonOptionWidget' );
13099 this.$button.append( this.$element.contents() );
13100 this.$element.append( this.$button );
13101 };
13102
13103 /* Setup */
13104
13105 OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.DecoratedOptionWidget );
13106 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.ButtonElement );
13107 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.TabIndexedElement );
13108
13109 /* Static Properties */
13110
13111 // Allow button mouse down events to pass through so they can be handled by the parent select widget
13112 OO.ui.ButtonOptionWidget.static.cancelButtonMouseDownEvents = false;
13113
13114 OO.ui.ButtonOptionWidget.static.highlightable = false;
13115
13116 /* Methods */
13117
13118 /**
13119 * @inheritdoc
13120 */
13121 OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) {
13122 OO.ui.ButtonOptionWidget.super.prototype.setSelected.call( this, state );
13123
13124 if ( this.constructor.static.selectable ) {
13125 this.setActive( state );
13126 }
13127
13128 return this;
13129 };
13130
13131 /**
13132 * RadioOptionWidget is an option widget that looks like a radio button.
13133 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
13134 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
13135 *
13136 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
13137 *
13138 * @class
13139 * @extends OO.ui.OptionWidget
13140 *
13141 * @constructor
13142 * @param {Object} [config] Configuration options
13143 */
13144 OO.ui.RadioOptionWidget = function OoUiRadioOptionWidget( config ) {
13145 // Configuration initialization
13146 config = config || {};
13147
13148 // Properties (must be done before parent constructor which calls #setDisabled)
13149 this.radio = new OO.ui.RadioInputWidget( { value: config.data, tabIndex: -1 } );
13150
13151 // Parent constructor
13152 OO.ui.RadioOptionWidget.super.call( this, config );
13153
13154 // Initialization
13155 this.$element
13156 .addClass( 'oo-ui-radioOptionWidget' )
13157 .prepend( this.radio.$element );
13158 };
13159
13160 /* Setup */
13161
13162 OO.inheritClass( OO.ui.RadioOptionWidget, OO.ui.OptionWidget );
13163
13164 /* Static Properties */
13165
13166 OO.ui.RadioOptionWidget.static.highlightable = false;
13167
13168 OO.ui.RadioOptionWidget.static.scrollIntoViewOnSelect = true;
13169
13170 OO.ui.RadioOptionWidget.static.pressable = false;
13171
13172 OO.ui.RadioOptionWidget.static.tagName = 'label';
13173
13174 /* Methods */
13175
13176 /**
13177 * @inheritdoc
13178 */
13179 OO.ui.RadioOptionWidget.prototype.setSelected = function ( state ) {
13180 OO.ui.RadioOptionWidget.super.prototype.setSelected.call( this, state );
13181
13182 this.radio.setSelected( state );
13183
13184 return this;
13185 };
13186
13187 /**
13188 * @inheritdoc
13189 */
13190 OO.ui.RadioOptionWidget.prototype.setDisabled = function ( disabled ) {
13191 OO.ui.RadioOptionWidget.super.prototype.setDisabled.call( this, disabled );
13192
13193 this.radio.setDisabled( this.isDisabled() );
13194
13195 return this;
13196 };
13197
13198 /**
13199 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
13200 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
13201 * the [OOjs UI documentation on MediaWiki] [1] for more information.
13202 *
13203 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
13204 *
13205 * @class
13206 * @extends OO.ui.DecoratedOptionWidget
13207 *
13208 * @constructor
13209 * @param {Object} [config] Configuration options
13210 */
13211 OO.ui.MenuOptionWidget = function OoUiMenuOptionWidget( config ) {
13212 // Configuration initialization
13213 config = $.extend( { icon: 'check' }, config );
13214
13215 // Parent constructor
13216 OO.ui.MenuOptionWidget.super.call( this, config );
13217
13218 // Initialization
13219 this.$element
13220 .attr( 'role', 'menuitem' )
13221 .addClass( 'oo-ui-menuOptionWidget' );
13222 };
13223
13224 /* Setup */
13225
13226 OO.inheritClass( OO.ui.MenuOptionWidget, OO.ui.DecoratedOptionWidget );
13227
13228 /* Static Properties */
13229
13230 OO.ui.MenuOptionWidget.static.scrollIntoViewOnSelect = true;
13231
13232 /**
13233 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
13234 * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
13235 *
13236 * @example
13237 * var myDropdown = new OO.ui.DropdownWidget( {
13238 * menu: {
13239 * items: [
13240 * new OO.ui.MenuSectionOptionWidget( {
13241 * label: 'Dogs'
13242 * } ),
13243 * new OO.ui.MenuOptionWidget( {
13244 * data: 'corgi',
13245 * label: 'Welsh Corgi'
13246 * } ),
13247 * new OO.ui.MenuOptionWidget( {
13248 * data: 'poodle',
13249 * label: 'Standard Poodle'
13250 * } ),
13251 * new OO.ui.MenuSectionOptionWidget( {
13252 * label: 'Cats'
13253 * } ),
13254 * new OO.ui.MenuOptionWidget( {
13255 * data: 'lion',
13256 * label: 'Lion'
13257 * } )
13258 * ]
13259 * }
13260 * } );
13261 * $( 'body' ).append( myDropdown.$element );
13262 *
13263 *
13264 * @class
13265 * @extends OO.ui.DecoratedOptionWidget
13266 *
13267 * @constructor
13268 * @param {Object} [config] Configuration options
13269 */
13270 OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( config ) {
13271 // Parent constructor
13272 OO.ui.MenuSectionOptionWidget.super.call( this, config );
13273
13274 // Initialization
13275 this.$element.addClass( 'oo-ui-menuSectionOptionWidget' );
13276 };
13277
13278 /* Setup */
13279
13280 OO.inheritClass( OO.ui.MenuSectionOptionWidget, OO.ui.DecoratedOptionWidget );
13281
13282 /* Static Properties */
13283
13284 OO.ui.MenuSectionOptionWidget.static.selectable = false;
13285
13286 OO.ui.MenuSectionOptionWidget.static.highlightable = false;
13287
13288 /**
13289 * OutlineOptionWidget is an item in an {@link OO.ui.OutlineSelectWidget OutlineSelectWidget}.
13290 *
13291 * Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}, which contain
13292 * {@link OO.ui.PageLayout page layouts}. See {@link OO.ui.BookletLayout BookletLayout}
13293 * for an example.
13294 *
13295 * @class
13296 * @extends OO.ui.DecoratedOptionWidget
13297 *
13298 * @constructor
13299 * @param {Object} [config] Configuration options
13300 * @cfg {number} [level] Indentation level
13301 * @cfg {boolean} [movable] Allow modification from {@link OO.ui.OutlineControlsWidget outline controls}.
13302 */
13303 OO.ui.OutlineOptionWidget = function OoUiOutlineOptionWidget( config ) {
13304 // Configuration initialization
13305 config = config || {};
13306
13307 // Parent constructor
13308 OO.ui.OutlineOptionWidget.super.call( this, config );
13309
13310 // Properties
13311 this.level = 0;
13312 this.movable = !!config.movable;
13313 this.removable = !!config.removable;
13314
13315 // Initialization
13316 this.$element.addClass( 'oo-ui-outlineOptionWidget' );
13317 this.setLevel( config.level );
13318 };
13319
13320 /* Setup */
13321
13322 OO.inheritClass( OO.ui.OutlineOptionWidget, OO.ui.DecoratedOptionWidget );
13323
13324 /* Static Properties */
13325
13326 OO.ui.OutlineOptionWidget.static.highlightable = false;
13327
13328 OO.ui.OutlineOptionWidget.static.scrollIntoViewOnSelect = true;
13329
13330 OO.ui.OutlineOptionWidget.static.levelClass = 'oo-ui-outlineOptionWidget-level-';
13331
13332 OO.ui.OutlineOptionWidget.static.levels = 3;
13333
13334 /* Methods */
13335
13336 /**
13337 * Check if item is movable.
13338 *
13339 * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
13340 *
13341 * @return {boolean} Item is movable
13342 */
13343 OO.ui.OutlineOptionWidget.prototype.isMovable = function () {
13344 return this.movable;
13345 };
13346
13347 /**
13348 * Check if item is removable.
13349 *
13350 * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
13351 *
13352 * @return {boolean} Item is removable
13353 */
13354 OO.ui.OutlineOptionWidget.prototype.isRemovable = function () {
13355 return this.removable;
13356 };
13357
13358 /**
13359 * Get indentation level.
13360 *
13361 * @return {number} Indentation level
13362 */
13363 OO.ui.OutlineOptionWidget.prototype.getLevel = function () {
13364 return this.level;
13365 };
13366
13367 /**
13368 * Set movability.
13369 *
13370 * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
13371 *
13372 * @param {boolean} movable Item is movable
13373 * @chainable
13374 */
13375 OO.ui.OutlineOptionWidget.prototype.setMovable = function ( movable ) {
13376 this.movable = !!movable;
13377 this.updateThemeClasses();
13378 return this;
13379 };
13380
13381 /**
13382 * Set removability.
13383 *
13384 * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
13385 *
13386 * @param {boolean} movable Item is removable
13387 * @chainable
13388 */
13389 OO.ui.OutlineOptionWidget.prototype.setRemovable = function ( removable ) {
13390 this.removable = !!removable;
13391 this.updateThemeClasses();
13392 return this;
13393 };
13394
13395 /**
13396 * Set indentation level.
13397 *
13398 * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
13399 * @chainable
13400 */
13401 OO.ui.OutlineOptionWidget.prototype.setLevel = function ( level ) {
13402 var levels = this.constructor.static.levels,
13403 levelClass = this.constructor.static.levelClass,
13404 i = levels;
13405
13406 this.level = level ? Math.max( 0, Math.min( levels - 1, level ) ) : 0;
13407 while ( i-- ) {
13408 if ( this.level === i ) {
13409 this.$element.addClass( levelClass + i );
13410 } else {
13411 this.$element.removeClass( levelClass + i );
13412 }
13413 }
13414 this.updateThemeClasses();
13415
13416 return this;
13417 };
13418
13419 /**
13420 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
13421 * By default, each popup has an anchor that points toward its origin.
13422 * Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples.
13423 *
13424 * @example
13425 * // A popup widget.
13426 * var popup = new OO.ui.PopupWidget( {
13427 * $content: $( '<p>Hi there!</p>' ),
13428 * padded: true,
13429 * width: 300
13430 * } );
13431 *
13432 * $( 'body' ).append( popup.$element );
13433 * // To display the popup, toggle the visibility to 'true'.
13434 * popup.toggle( true );
13435 *
13436 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups
13437 *
13438 * @class
13439 * @extends OO.ui.Widget
13440 * @mixins OO.ui.LabelElement
13441 *
13442 * @constructor
13443 * @param {Object} [config] Configuration options
13444 * @cfg {number} [width=320] Width of popup in pixels
13445 * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
13446 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
13447 * @cfg {string} [align='center'] Alignment of the popup: `center`, `force-left`, `force-right`, `backwards` or `forwards`.
13448 * If the popup is forced-left the popup body is leaning towards the left. For force-right alignment, the body of the
13449 * popup is leaning towards the right of the screen.
13450 * Using 'backwards' is a logical direction which will result in the popup leaning towards the beginning of the sentence
13451 * in the given language, which means it will flip to the correct positioning in right-to-left languages.
13452 * Using 'forward' will also result in a logical alignment where the body of the popup leans towards the end of the
13453 * sentence in the given language.
13454 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
13455 * See the [OOjs UI docs on MediaWiki][3] for an example.
13456 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
13457 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
13458 * @cfg {jQuery} [$content] Content to append to the popup's body
13459 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
13460 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
13461 * This config option is only relevant if #autoClose is set to `true`. See the [OOjs UI docs on MediaWiki][2]
13462 * for an example.
13463 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#autocloseExample
13464 * @cfg {boolean} [head] Show a popup header that contains a #label (if specified) and close
13465 * button.
13466 * @cfg {boolean} [padded] Add padding to the popup's body
13467 */
13468 OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
13469 // Configuration initialization
13470 config = config || {};
13471
13472 // Parent constructor
13473 OO.ui.PopupWidget.super.call( this, config );
13474
13475 // Properties (must be set before ClippableElement constructor call)
13476 this.$body = $( '<div>' );
13477
13478 // Mixin constructors
13479 OO.ui.LabelElement.call( this, config );
13480 OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$body } ) );
13481
13482 // Properties
13483 this.$popup = $( '<div>' );
13484 this.$head = $( '<div>' );
13485 this.$anchor = $( '<div>' );
13486 // If undefined, will be computed lazily in updateDimensions()
13487 this.$container = config.$container;
13488 this.containerPadding = config.containerPadding !== undefined ? config.containerPadding : 10;
13489 this.autoClose = !!config.autoClose;
13490 this.$autoCloseIgnore = config.$autoCloseIgnore;
13491 this.transitionTimeout = null;
13492 this.anchor = null;
13493 this.width = config.width !== undefined ? config.width : 320;
13494 this.height = config.height !== undefined ? config.height : null;
13495 // Validate alignment and transform deprecated values
13496 if ( [ 'left', 'right', 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( config.align ) > -1 ) {
13497 this.align = { left: 'force-right', right: 'force-left' }[ config.align ] || config.align;
13498 } else {
13499 this.align = 'center';
13500 }
13501 this.closeButton = new OO.ui.ButtonWidget( { framed: false, icon: 'close' } );
13502 this.onMouseDownHandler = this.onMouseDown.bind( this );
13503 this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
13504
13505 // Events
13506 this.closeButton.connect( this, { click: 'onCloseButtonClick' } );
13507
13508 // Initialization
13509 this.toggleAnchor( config.anchor === undefined || config.anchor );
13510 this.$body.addClass( 'oo-ui-popupWidget-body' );
13511 this.$anchor.addClass( 'oo-ui-popupWidget-anchor' );
13512 this.$head
13513 .addClass( 'oo-ui-popupWidget-head' )
13514 .append( this.$label, this.closeButton.$element );
13515 if ( !config.head ) {
13516 this.$head.addClass( 'oo-ui-element-hidden' );
13517 }
13518 this.$popup
13519 .addClass( 'oo-ui-popupWidget-popup' )
13520 .append( this.$head, this.$body );
13521 this.$element
13522 .addClass( 'oo-ui-popupWidget' )
13523 .append( this.$popup, this.$anchor );
13524 // Move content, which was added to #$element by OO.ui.Widget, to the body
13525 if ( config.$content instanceof jQuery ) {
13526 this.$body.append( config.$content );
13527 }
13528 if ( config.padded ) {
13529 this.$body.addClass( 'oo-ui-popupWidget-body-padded' );
13530 }
13531
13532 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
13533 // that reference properties not initialized at that time of parent class construction
13534 // TODO: Find a better way to handle post-constructor setup
13535 this.visible = false;
13536 this.$element.addClass( 'oo-ui-element-hidden' );
13537 };
13538
13539 /* Setup */
13540
13541 OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
13542 OO.mixinClass( OO.ui.PopupWidget, OO.ui.LabelElement );
13543 OO.mixinClass( OO.ui.PopupWidget, OO.ui.ClippableElement );
13544
13545 /* Methods */
13546
13547 /**
13548 * Handles mouse down events.
13549 *
13550 * @private
13551 * @param {MouseEvent} e Mouse down event
13552 */
13553 OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
13554 if (
13555 this.isVisible() &&
13556 !$.contains( this.$element[ 0 ], e.target ) &&
13557 ( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length )
13558 ) {
13559 this.toggle( false );
13560 }
13561 };
13562
13563 /**
13564 * Bind mouse down listener.
13565 *
13566 * @private
13567 */
13568 OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
13569 // Capture clicks outside popup
13570 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
13571 };
13572
13573 /**
13574 * Handles close button click events.
13575 *
13576 * @private
13577 */
13578 OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
13579 if ( this.isVisible() ) {
13580 this.toggle( false );
13581 }
13582 };
13583
13584 /**
13585 * Unbind mouse down listener.
13586 *
13587 * @private
13588 */
13589 OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
13590 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
13591 };
13592
13593 /**
13594 * Handles key down events.
13595 *
13596 * @private
13597 * @param {KeyboardEvent} e Key down event
13598 */
13599 OO.ui.PopupWidget.prototype.onDocumentKeyDown = function ( e ) {
13600 if (
13601 e.which === OO.ui.Keys.ESCAPE &&
13602 this.isVisible()
13603 ) {
13604 this.toggle( false );
13605 e.preventDefault();
13606 e.stopPropagation();
13607 }
13608 };
13609
13610 /**
13611 * Bind key down listener.
13612 *
13613 * @private
13614 */
13615 OO.ui.PopupWidget.prototype.bindKeyDownListener = function () {
13616 this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
13617 };
13618
13619 /**
13620 * Unbind key down listener.
13621 *
13622 * @private
13623 */
13624 OO.ui.PopupWidget.prototype.unbindKeyDownListener = function () {
13625 this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
13626 };
13627
13628 /**
13629 * Show, hide, or toggle the visibility of the anchor.
13630 *
13631 * @param {boolean} [show] Show anchor, omit to toggle
13632 */
13633 OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) {
13634 show = show === undefined ? !this.anchored : !!show;
13635
13636 if ( this.anchored !== show ) {
13637 if ( show ) {
13638 this.$element.addClass( 'oo-ui-popupWidget-anchored' );
13639 } else {
13640 this.$element.removeClass( 'oo-ui-popupWidget-anchored' );
13641 }
13642 this.anchored = show;
13643 }
13644 };
13645
13646 /**
13647 * Check if the anchor is visible.
13648 *
13649 * @return {boolean} Anchor is visible
13650 */
13651 OO.ui.PopupWidget.prototype.hasAnchor = function () {
13652 return this.anchor;
13653 };
13654
13655 /**
13656 * @inheritdoc
13657 */
13658 OO.ui.PopupWidget.prototype.toggle = function ( show ) {
13659 show = show === undefined ? !this.isVisible() : !!show;
13660
13661 var change = show !== this.isVisible();
13662
13663 // Parent method
13664 OO.ui.PopupWidget.super.prototype.toggle.call( this, show );
13665
13666 if ( change ) {
13667 if ( show ) {
13668 if ( this.autoClose ) {
13669 this.bindMouseDownListener();
13670 this.bindKeyDownListener();
13671 }
13672 this.updateDimensions();
13673 this.toggleClipping( true );
13674 } else {
13675 this.toggleClipping( false );
13676 if ( this.autoClose ) {
13677 this.unbindMouseDownListener();
13678 this.unbindKeyDownListener();
13679 }
13680 }
13681 }
13682
13683 return this;
13684 };
13685
13686 /**
13687 * Set the size of the popup.
13688 *
13689 * Changing the size may also change the popup's position depending on the alignment.
13690 *
13691 * @param {number} width Width in pixels
13692 * @param {number} height Height in pixels
13693 * @param {boolean} [transition=false] Use a smooth transition
13694 * @chainable
13695 */
13696 OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) {
13697 this.width = width;
13698 this.height = height !== undefined ? height : null;
13699 if ( this.isVisible() ) {
13700 this.updateDimensions( transition );
13701 }
13702 };
13703
13704 /**
13705 * Update the size and position.
13706 *
13707 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
13708 * be called automatically.
13709 *
13710 * @param {boolean} [transition=false] Use a smooth transition
13711 * @chainable
13712 */
13713 OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
13714 var popupOffset, originOffset, containerLeft, containerWidth, containerRight,
13715 popupLeft, popupRight, overlapLeft, overlapRight, anchorWidth,
13716 align = this.align,
13717 widget = this;
13718
13719 if ( !this.$container ) {
13720 // Lazy-initialize $container if not specified in constructor
13721 this.$container = $( this.getClosestScrollableElementContainer() );
13722 }
13723
13724 // Set height and width before measuring things, since it might cause our measurements
13725 // to change (e.g. due to scrollbars appearing or disappearing)
13726 this.$popup.css( {
13727 width: this.width,
13728 height: this.height !== null ? this.height : 'auto'
13729 } );
13730
13731 // If we are in RTL, we need to flip the alignment, unless it is center
13732 if ( align === 'forwards' || align === 'backwards' ) {
13733 if ( this.$container.css( 'direction' ) === 'rtl' ) {
13734 align = ( { forwards: 'force-left', backwards: 'force-right' } )[ this.align ];
13735 } else {
13736 align = ( { forwards: 'force-right', backwards: 'force-left' } )[ this.align ];
13737 }
13738
13739 }
13740
13741 // Compute initial popupOffset based on alignment
13742 popupOffset = this.width * ( { 'force-left': -1, center: -0.5, 'force-right': 0 } )[ align ];
13743
13744 // Figure out if this will cause the popup to go beyond the edge of the container
13745 originOffset = this.$element.offset().left;
13746 containerLeft = this.$container.offset().left;
13747 containerWidth = this.$container.innerWidth();
13748 containerRight = containerLeft + containerWidth;
13749 popupLeft = popupOffset - this.containerPadding;
13750 popupRight = popupOffset + this.containerPadding + this.width + this.containerPadding;
13751 overlapLeft = ( originOffset + popupLeft ) - containerLeft;
13752 overlapRight = containerRight - ( originOffset + popupRight );
13753
13754 // Adjust offset to make the popup not go beyond the edge, if needed
13755 if ( overlapRight < 0 ) {
13756 popupOffset += overlapRight;
13757 } else if ( overlapLeft < 0 ) {
13758 popupOffset -= overlapLeft;
13759 }
13760
13761 // Adjust offset to avoid anchor being rendered too close to the edge
13762 // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
13763 // TODO: Find a measurement that works for CSS anchors and image anchors
13764 anchorWidth = this.$anchor[ 0 ].scrollWidth * 2;
13765 if ( popupOffset + this.width < anchorWidth ) {
13766 popupOffset = anchorWidth - this.width;
13767 } else if ( -popupOffset < anchorWidth ) {
13768 popupOffset = -anchorWidth;
13769 }
13770
13771 // Prevent transition from being interrupted
13772 clearTimeout( this.transitionTimeout );
13773 if ( transition ) {
13774 // Enable transition
13775 this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
13776 }
13777
13778 // Position body relative to anchor
13779 this.$popup.css( 'margin-left', popupOffset );
13780
13781 if ( transition ) {
13782 // Prevent transitioning after transition is complete
13783 this.transitionTimeout = setTimeout( function () {
13784 widget.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
13785 }, 200 );
13786 } else {
13787 // Prevent transitioning immediately
13788 this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
13789 }
13790
13791 // Reevaluate clipping state since we've relocated and resized the popup
13792 this.clip();
13793
13794 return this;
13795 };
13796
13797 /**
13798 * Progress bars visually display the status of an operation, such as a download,
13799 * and can be either determinate or indeterminate:
13800 *
13801 * - **determinate** process bars show the percent of an operation that is complete.
13802 *
13803 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
13804 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
13805 * not use percentages.
13806 *
13807 * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
13808 *
13809 * @example
13810 * // Examples of determinate and indeterminate progress bars.
13811 * var progressBar1 = new OO.ui.ProgressBarWidget( {
13812 * progress: 33
13813 * } );
13814 * var progressBar2 = new OO.ui.ProgressBarWidget();
13815 *
13816 * // Create a FieldsetLayout to layout progress bars
13817 * var fieldset = new OO.ui.FieldsetLayout;
13818 * fieldset.addItems( [
13819 * new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
13820 * new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
13821 * ] );
13822 * $( 'body' ).append( fieldset.$element );
13823 *
13824 * @class
13825 * @extends OO.ui.Widget
13826 *
13827 * @constructor
13828 * @param {Object} [config] Configuration options
13829 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
13830 * To create a determinate progress bar, specify a number that reflects the initial percent complete.
13831 * By default, the progress bar is indeterminate.
13832 */
13833 OO.ui.ProgressBarWidget = function OoUiProgressBarWidget( config ) {
13834 // Configuration initialization
13835 config = config || {};
13836
13837 // Parent constructor
13838 OO.ui.ProgressBarWidget.super.call( this, config );
13839
13840 // Properties
13841 this.$bar = $( '<div>' );
13842 this.progress = null;
13843
13844 // Initialization
13845 this.setProgress( config.progress !== undefined ? config.progress : false );
13846 this.$bar.addClass( 'oo-ui-progressBarWidget-bar' );
13847 this.$element
13848 .attr( {
13849 role: 'progressbar',
13850 'aria-valuemin': 0,
13851 'aria-valuemax': 100
13852 } )
13853 .addClass( 'oo-ui-progressBarWidget' )
13854 .append( this.$bar );
13855 };
13856
13857 /* Setup */
13858
13859 OO.inheritClass( OO.ui.ProgressBarWidget, OO.ui.Widget );
13860
13861 /* Static Properties */
13862
13863 OO.ui.ProgressBarWidget.static.tagName = 'div';
13864
13865 /* Methods */
13866
13867 /**
13868 * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
13869 *
13870 * @return {number|boolean} Progress percent
13871 */
13872 OO.ui.ProgressBarWidget.prototype.getProgress = function () {
13873 return this.progress;
13874 };
13875
13876 /**
13877 * Set the percent of the process completed or `false` for an indeterminate process.
13878 *
13879 * @param {number|boolean} progress Progress percent or `false` for indeterminate
13880 */
13881 OO.ui.ProgressBarWidget.prototype.setProgress = function ( progress ) {
13882 this.progress = progress;
13883
13884 if ( progress !== false ) {
13885 this.$bar.css( 'width', this.progress + '%' );
13886 this.$element.attr( 'aria-valuenow', this.progress );
13887 } else {
13888 this.$bar.css( 'width', '' );
13889 this.$element.removeAttr( 'aria-valuenow' );
13890 }
13891 this.$element.toggleClass( 'oo-ui-progressBarWidget-indeterminate', !progress );
13892 };
13893
13894 /**
13895 * SearchWidgets combine a {@link OO.ui.TextInputWidget text input field}, where users can type a search query,
13896 * and a {@link OO.ui.TextInputMenuSelectWidget menu} of search results, which is displayed beneath the query
13897 * field. Unlike {@link OO.ui.LookupElement lookup menus}, search result menus are always visible to the user.
13898 * Users can choose an item from the menu or type a query into the text field to search for a matching result item.
13899 * In general, search widgets are used inside a separate {@link OO.ui.Dialog dialog} window.
13900 *
13901 * Each time the query is changed, the search result menu is cleared and repopulated. Please see
13902 * the [OOjs UI demos][1] for an example.
13903 *
13904 * [1]: https://tools.wmflabs.org/oojs-ui/oojs-ui/demos/#dialogs-mediawiki-vector-ltr
13905 *
13906 * @class
13907 * @extends OO.ui.Widget
13908 *
13909 * @constructor
13910 * @param {Object} [config] Configuration options
13911 * @cfg {string|jQuery} [placeholder] Placeholder text for query input
13912 * @cfg {string} [value] Initial query value
13913 */
13914 OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
13915 // Configuration initialization
13916 config = config || {};
13917
13918 // Parent constructor
13919 OO.ui.SearchWidget.super.call( this, config );
13920
13921 // Properties
13922 this.query = new OO.ui.TextInputWidget( {
13923 icon: 'search',
13924 placeholder: config.placeholder,
13925 value: config.value
13926 } );
13927 this.results = new OO.ui.SelectWidget();
13928 this.$query = $( '<div>' );
13929 this.$results = $( '<div>' );
13930
13931 // Events
13932 this.query.connect( this, {
13933 change: 'onQueryChange',
13934 enter: 'onQueryEnter'
13935 } );
13936 this.results.connect( this, {
13937 highlight: 'onResultsHighlight',
13938 select: 'onResultsSelect'
13939 } );
13940 this.query.$input.on( 'keydown', this.onQueryKeydown.bind( this ) );
13941
13942 // Initialization
13943 this.$query
13944 .addClass( 'oo-ui-searchWidget-query' )
13945 .append( this.query.$element );
13946 this.$results
13947 .addClass( 'oo-ui-searchWidget-results' )
13948 .append( this.results.$element );
13949 this.$element
13950 .addClass( 'oo-ui-searchWidget' )
13951 .append( this.$results, this.$query );
13952 };
13953
13954 /* Setup */
13955
13956 OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget );
13957
13958 /* Events */
13959
13960 /**
13961 * A 'highlight' event is emitted when an item is highlighted. The highlight indicates which
13962 * item will be selected. When a user mouses over a menu item, it is highlighted. If a search
13963 * string is typed into the query field instead, the first menu item that matches the query
13964 * will be highlighted.
13965
13966 * @event highlight
13967 * @deprecated Connect straight to getResults() events instead
13968 * @param {Object|null} item Item data or null if no item is highlighted
13969 */
13970
13971 /**
13972 * A 'select' event is emitted when an item is selected. A menu item is selected when it is clicked,
13973 * or when a user types a search query, a menu result is highlighted, and the user presses enter.
13974 *
13975 * @event select
13976 * @deprecated Connect straight to getResults() events instead
13977 * @param {Object|null} item Item data or null if no item is selected
13978 */
13979
13980 /* Methods */
13981
13982 /**
13983 * Handle query key down events.
13984 *
13985 * @private
13986 * @param {jQuery.Event} e Key down event
13987 */
13988 OO.ui.SearchWidget.prototype.onQueryKeydown = function ( e ) {
13989 var highlightedItem, nextItem,
13990 dir = e.which === OO.ui.Keys.DOWN ? 1 : ( e.which === OO.ui.Keys.UP ? -1 : 0 );
13991
13992 if ( dir ) {
13993 highlightedItem = this.results.getHighlightedItem();
13994 if ( !highlightedItem ) {
13995 highlightedItem = this.results.getSelectedItem();
13996 }
13997 nextItem = this.results.getRelativeSelectableItem( highlightedItem, dir );
13998 this.results.highlightItem( nextItem );
13999 nextItem.scrollElementIntoView();
14000 }
14001 };
14002
14003 /**
14004 * Handle select widget select events.
14005 *
14006 * Clears existing results. Subclasses should repopulate items according to new query.
14007 *
14008 * @private
14009 * @param {string} value New value
14010 */
14011 OO.ui.SearchWidget.prototype.onQueryChange = function () {
14012 // Reset
14013 this.results.clearItems();
14014 };
14015
14016 /**
14017 * Handle select widget enter key events.
14018 *
14019 * Selects highlighted item.
14020 *
14021 * @private
14022 * @param {string} value New value
14023 */
14024 OO.ui.SearchWidget.prototype.onQueryEnter = function () {
14025 // Reset
14026 this.results.selectItem( this.results.getHighlightedItem() );
14027 };
14028
14029 /**
14030 * Handle select widget highlight events.
14031 *
14032 * @private
14033 * @deprecated Connect straight to getResults() events instead
14034 * @param {OO.ui.OptionWidget} item Highlighted item
14035 * @fires highlight
14036 */
14037 OO.ui.SearchWidget.prototype.onResultsHighlight = function ( item ) {
14038 this.emit( 'highlight', item ? item.getData() : null );
14039 };
14040
14041 /**
14042 * Handle select widget select events.
14043 *
14044 * @private
14045 * @deprecated Connect straight to getResults() events instead
14046 * @param {OO.ui.OptionWidget} item Selected item
14047 * @fires select
14048 */
14049 OO.ui.SearchWidget.prototype.onResultsSelect = function ( item ) {
14050 this.emit( 'select', item ? item.getData() : null );
14051 };
14052
14053 /**
14054 * Get the query input.
14055 *
14056 * @return {OO.ui.TextInputWidget} Query input
14057 */
14058 OO.ui.SearchWidget.prototype.getQuery = function () {
14059 return this.query;
14060 };
14061
14062 /**
14063 * Get the search results menu.
14064 *
14065 * @return {OO.ui.SelectWidget} Menu of search results
14066 */
14067 OO.ui.SearchWidget.prototype.getResults = function () {
14068 return this.results;
14069 };
14070
14071 /**
14072 * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
14073 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
14074 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
14075 * menu selects}.
14076 *
14077 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
14078 * information, please see the [OOjs UI documentation on MediaWiki][1].
14079 *
14080 * @example
14081 * // Example of a select widget with three options
14082 * var select = new OO.ui.SelectWidget( {
14083 * items: [
14084 * new OO.ui.OptionWidget( {
14085 * data: 'a',
14086 * label: 'Option One',
14087 * } ),
14088 * new OO.ui.OptionWidget( {
14089 * data: 'b',
14090 * label: 'Option Two',
14091 * } ),
14092 * new OO.ui.OptionWidget( {
14093 * data: 'c',
14094 * label: 'Option Three',
14095 * } )
14096 * ]
14097 * } );
14098 * $( 'body' ).append( select.$element );
14099 *
14100 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
14101 *
14102 * @class
14103 * @extends OO.ui.Widget
14104 * @mixins OO.ui.GroupElement
14105 *
14106 * @constructor
14107 * @param {Object} [config] Configuration options
14108 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
14109 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
14110 * the [OOjs UI documentation on MediaWiki] [2] for examples.
14111 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
14112 */
14113 OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
14114 // Configuration initialization
14115 config = config || {};
14116
14117 // Parent constructor
14118 OO.ui.SelectWidget.super.call( this, config );
14119
14120 // Mixin constructors
14121 OO.ui.GroupWidget.call( this, $.extend( {}, config, { $group: this.$element } ) );
14122
14123 // Properties
14124 this.pressed = false;
14125 this.selecting = null;
14126 this.onMouseUpHandler = this.onMouseUp.bind( this );
14127 this.onMouseMoveHandler = this.onMouseMove.bind( this );
14128 this.onKeyDownHandler = this.onKeyDown.bind( this );
14129
14130 // Events
14131 this.$element.on( {
14132 mousedown: this.onMouseDown.bind( this ),
14133 mouseover: this.onMouseOver.bind( this ),
14134 mouseleave: this.onMouseLeave.bind( this )
14135 } );
14136
14137 // Initialization
14138 this.$element
14139 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
14140 .attr( 'role', 'listbox' );
14141 if ( Array.isArray( config.items ) ) {
14142 this.addItems( config.items );
14143 }
14144 };
14145
14146 /* Setup */
14147
14148 OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
14149
14150 // Need to mixin base class as well
14151 OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupElement );
14152 OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupWidget );
14153
14154 /* Events */
14155
14156 /**
14157 * @event highlight
14158 *
14159 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
14160 *
14161 * @param {OO.ui.OptionWidget|null} item Highlighted item
14162 */
14163
14164 /**
14165 * @event press
14166 *
14167 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
14168 * pressed state of an option.
14169 *
14170 * @param {OO.ui.OptionWidget|null} item Pressed item
14171 */
14172
14173 /**
14174 * @event select
14175 *
14176 * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
14177 *
14178 * @param {OO.ui.OptionWidget|null} item Selected item
14179 */
14180
14181 /**
14182 * @event choose
14183 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
14184 * @param {OO.ui.OptionWidget} item Chosen item
14185 */
14186
14187 /**
14188 * @event add
14189 *
14190 * An `add` event is emitted when options are added to the select with the #addItems method.
14191 *
14192 * @param {OO.ui.OptionWidget[]} items Added items
14193 * @param {number} index Index of insertion point
14194 */
14195
14196 /**
14197 * @event remove
14198 *
14199 * A `remove` event is emitted when options are removed from the select with the #clearItems
14200 * or #removeItems methods.
14201 *
14202 * @param {OO.ui.OptionWidget[]} items Removed items
14203 */
14204
14205 /* Methods */
14206
14207 /**
14208 * Handle mouse down events.
14209 *
14210 * @private
14211 * @param {jQuery.Event} e Mouse down event
14212 */
14213 OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
14214 var item;
14215
14216 if ( !this.isDisabled() && e.which === 1 ) {
14217 this.togglePressed( true );
14218 item = this.getTargetItem( e );
14219 if ( item && item.isSelectable() ) {
14220 this.pressItem( item );
14221 this.selecting = item;
14222 this.getElementDocument().addEventListener(
14223 'mouseup',
14224 this.onMouseUpHandler,
14225 true
14226 );
14227 this.getElementDocument().addEventListener(
14228 'mousemove',
14229 this.onMouseMoveHandler,
14230 true
14231 );
14232 }
14233 }
14234 return false;
14235 };
14236
14237 /**
14238 * Handle mouse up events.
14239 *
14240 * @private
14241 * @param {jQuery.Event} e Mouse up event
14242 */
14243 OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
14244 var item;
14245
14246 this.togglePressed( false );
14247 if ( !this.selecting ) {
14248 item = this.getTargetItem( e );
14249 if ( item && item.isSelectable() ) {
14250 this.selecting = item;
14251 }
14252 }
14253 if ( !this.isDisabled() && e.which === 1 && this.selecting ) {
14254 this.pressItem( null );
14255 this.chooseItem( this.selecting );
14256 this.selecting = null;
14257 }
14258
14259 this.getElementDocument().removeEventListener(
14260 'mouseup',
14261 this.onMouseUpHandler,
14262 true
14263 );
14264 this.getElementDocument().removeEventListener(
14265 'mousemove',
14266 this.onMouseMoveHandler,
14267 true
14268 );
14269
14270 return false;
14271 };
14272
14273 /**
14274 * Handle mouse move events.
14275 *
14276 * @private
14277 * @param {jQuery.Event} e Mouse move event
14278 */
14279 OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
14280 var item;
14281
14282 if ( !this.isDisabled() && this.pressed ) {
14283 item = this.getTargetItem( e );
14284 if ( item && item !== this.selecting && item.isSelectable() ) {
14285 this.pressItem( item );
14286 this.selecting = item;
14287 }
14288 }
14289 return false;
14290 };
14291
14292 /**
14293 * Handle mouse over events.
14294 *
14295 * @private
14296 * @param {jQuery.Event} e Mouse over event
14297 */
14298 OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
14299 var item;
14300
14301 if ( !this.isDisabled() ) {
14302 item = this.getTargetItem( e );
14303 this.highlightItem( item && item.isHighlightable() ? item : null );
14304 }
14305 return false;
14306 };
14307
14308 /**
14309 * Handle mouse leave events.
14310 *
14311 * @private
14312 * @param {jQuery.Event} e Mouse over event
14313 */
14314 OO.ui.SelectWidget.prototype.onMouseLeave = function () {
14315 if ( !this.isDisabled() ) {
14316 this.highlightItem( null );
14317 }
14318 return false;
14319 };
14320
14321 /**
14322 * Handle key down events.
14323 *
14324 * @protected
14325 * @param {jQuery.Event} e Key down event
14326 */
14327 OO.ui.SelectWidget.prototype.onKeyDown = function ( e ) {
14328 var nextItem,
14329 handled = false,
14330 currentItem = this.getHighlightedItem() || this.getSelectedItem();
14331
14332 if ( !this.isDisabled() && this.isVisible() ) {
14333 switch ( e.keyCode ) {
14334 case OO.ui.Keys.ENTER:
14335 if ( currentItem && currentItem.constructor.static.highlightable ) {
14336 // Was only highlighted, now let's select it. No-op if already selected.
14337 this.chooseItem( currentItem );
14338 handled = true;
14339 }
14340 break;
14341 case OO.ui.Keys.UP:
14342 case OO.ui.Keys.LEFT:
14343 nextItem = this.getRelativeSelectableItem( currentItem, -1 );
14344 handled = true;
14345 break;
14346 case OO.ui.Keys.DOWN:
14347 case OO.ui.Keys.RIGHT:
14348 nextItem = this.getRelativeSelectableItem( currentItem, 1 );
14349 handled = true;
14350 break;
14351 case OO.ui.Keys.ESCAPE:
14352 case OO.ui.Keys.TAB:
14353 if ( currentItem && currentItem.constructor.static.highlightable ) {
14354 currentItem.setHighlighted( false );
14355 }
14356 this.unbindKeyDownListener();
14357 // Don't prevent tabbing away / defocusing
14358 handled = false;
14359 break;
14360 }
14361
14362 if ( nextItem ) {
14363 if ( nextItem.constructor.static.highlightable ) {
14364 this.highlightItem( nextItem );
14365 } else {
14366 this.chooseItem( nextItem );
14367 }
14368 nextItem.scrollElementIntoView();
14369 }
14370
14371 if ( handled ) {
14372 // Can't just return false, because e is not always a jQuery event
14373 e.preventDefault();
14374 e.stopPropagation();
14375 }
14376 }
14377 };
14378
14379 /**
14380 * Bind key down listener.
14381 *
14382 * @protected
14383 */
14384 OO.ui.SelectWidget.prototype.bindKeyDownListener = function () {
14385 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
14386 };
14387
14388 /**
14389 * Unbind key down listener.
14390 *
14391 * @protected
14392 */
14393 OO.ui.SelectWidget.prototype.unbindKeyDownListener = function () {
14394 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
14395 };
14396
14397 /**
14398 * Get the closest item to a jQuery.Event.
14399 *
14400 * @private
14401 * @param {jQuery.Event} e
14402 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
14403 */
14404 OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) {
14405 return $( e.target ).closest( '.oo-ui-optionWidget' ).data( 'oo-ui-optionWidget' ) || null;
14406 };
14407
14408 /**
14409 * Get selected item.
14410 *
14411 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
14412 */
14413 OO.ui.SelectWidget.prototype.getSelectedItem = function () {
14414 var i, len;
14415
14416 for ( i = 0, len = this.items.length; i < len; i++ ) {
14417 if ( this.items[ i ].isSelected() ) {
14418 return this.items[ i ];
14419 }
14420 }
14421 return null;
14422 };
14423
14424 /**
14425 * Get highlighted item.
14426 *
14427 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
14428 */
14429 OO.ui.SelectWidget.prototype.getHighlightedItem = function () {
14430 var i, len;
14431
14432 for ( i = 0, len = this.items.length; i < len; i++ ) {
14433 if ( this.items[ i ].isHighlighted() ) {
14434 return this.items[ i ];
14435 }
14436 }
14437 return null;
14438 };
14439
14440 /**
14441 * Toggle pressed state.
14442 *
14443 * Press is a state that occurs when a user mouses down on an item, but
14444 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
14445 * until the user releases the mouse.
14446 *
14447 * @param {boolean} pressed An option is being pressed
14448 */
14449 OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) {
14450 if ( pressed === undefined ) {
14451 pressed = !this.pressed;
14452 }
14453 if ( pressed !== this.pressed ) {
14454 this.$element
14455 .toggleClass( 'oo-ui-selectWidget-pressed', pressed )
14456 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed );
14457 this.pressed = pressed;
14458 }
14459 };
14460
14461 /**
14462 * Highlight an option. If the `item` param is omitted, no options will be highlighted
14463 * and any existing highlight will be removed. The highlight is mutually exclusive.
14464 *
14465 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
14466 * @fires highlight
14467 * @chainable
14468 */
14469 OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
14470 var i, len, highlighted,
14471 changed = false;
14472
14473 for ( i = 0, len = this.items.length; i < len; i++ ) {
14474 highlighted = this.items[ i ] === item;
14475 if ( this.items[ i ].isHighlighted() !== highlighted ) {
14476 this.items[ i ].setHighlighted( highlighted );
14477 changed = true;
14478 }
14479 }
14480 if ( changed ) {
14481 this.emit( 'highlight', item );
14482 }
14483
14484 return this;
14485 };
14486
14487 /**
14488 * Programmatically select an option by its reference. If the `item` parameter is omitted,
14489 * all options will be deselected.
14490 *
14491 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
14492 * @fires select
14493 * @chainable
14494 */
14495 OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
14496 var i, len, selected,
14497 changed = false;
14498
14499 for ( i = 0, len = this.items.length; i < len; i++ ) {
14500 selected = this.items[ i ] === item;
14501 if ( this.items[ i ].isSelected() !== selected ) {
14502 this.items[ i ].setSelected( selected );
14503 changed = true;
14504 }
14505 }
14506 if ( changed ) {
14507 this.emit( 'select', item );
14508 }
14509
14510 return this;
14511 };
14512
14513 /**
14514 * Press an item.
14515 *
14516 * Press is a state that occurs when a user mouses down on an item, but has not
14517 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
14518 * releases the mouse.
14519 *
14520 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
14521 * @fires press
14522 * @chainable
14523 */
14524 OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
14525 var i, len, pressed,
14526 changed = false;
14527
14528 for ( i = 0, len = this.items.length; i < len; i++ ) {
14529 pressed = this.items[ i ] === item;
14530 if ( this.items[ i ].isPressed() !== pressed ) {
14531 this.items[ i ].setPressed( pressed );
14532 changed = true;
14533 }
14534 }
14535 if ( changed ) {
14536 this.emit( 'press', item );
14537 }
14538
14539 return this;
14540 };
14541
14542 /**
14543 * Choose an item.
14544 *
14545 * Note that ‘choose’ should never be modified programmatically. A user can choose
14546 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
14547 * use the #selectItem method.
14548 *
14549 * This method is identical to #selectItem, but may vary in subclasses that take additional action
14550 * when users choose an item with the keyboard or mouse.
14551 *
14552 * @param {OO.ui.OptionWidget} item Item to choose
14553 * @fires choose
14554 * @chainable
14555 */
14556 OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
14557 this.selectItem( item );
14558 this.emit( 'choose', item );
14559
14560 return this;
14561 };
14562
14563 /**
14564 * Get an option by its position relative to the specified item (or to the start of the option array,
14565 * if item is `null`). The direction in which to search through the option array is specified with a
14566 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
14567 * `null` if there are no options in the array.
14568 *
14569 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
14570 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
14571 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
14572 */
14573 OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction ) {
14574 var currentIndex, nextIndex, i,
14575 increase = direction > 0 ? 1 : -1,
14576 len = this.items.length;
14577
14578 if ( item instanceof OO.ui.OptionWidget ) {
14579 currentIndex = $.inArray( item, this.items );
14580 nextIndex = ( currentIndex + increase + len ) % len;
14581 } else {
14582 // If no item is selected and moving forward, start at the beginning.
14583 // If moving backward, start at the end.
14584 nextIndex = direction > 0 ? 0 : len - 1;
14585 }
14586
14587 for ( i = 0; i < len; i++ ) {
14588 item = this.items[ nextIndex ];
14589 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
14590 return item;
14591 }
14592 nextIndex = ( nextIndex + increase + len ) % len;
14593 }
14594 return null;
14595 };
14596
14597 /**
14598 * Get the next selectable item or `null` if there are no selectable items.
14599 * Disabled options and menu-section markers and breaks are not selectable.
14600 *
14601 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
14602 */
14603 OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
14604 var i, len, item;
14605
14606 for ( i = 0, len = this.items.length; i < len; i++ ) {
14607 item = this.items[ i ];
14608 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
14609 return item;
14610 }
14611 }
14612
14613 return null;
14614 };
14615
14616 /**
14617 * Add an array of options to the select. Optionally, an index number can be used to
14618 * specify an insertion point.
14619 *
14620 * @param {OO.ui.OptionWidget[]} items Items to add
14621 * @param {number} [index] Index to insert items after
14622 * @fires add
14623 * @chainable
14624 */
14625 OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
14626 // Mixin method
14627 OO.ui.GroupWidget.prototype.addItems.call( this, items, index );
14628
14629 // Always provide an index, even if it was omitted
14630 this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index );
14631
14632 return this;
14633 };
14634
14635 /**
14636 * Remove the specified array of options from the select. Options will be detached
14637 * from the DOM, not removed, so they can be reused later. To remove all options from
14638 * the select, you may wish to use the #clearItems method instead.
14639 *
14640 * @param {OO.ui.OptionWidget[]} items Items to remove
14641 * @fires remove
14642 * @chainable
14643 */
14644 OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
14645 var i, len, item;
14646
14647 // Deselect items being removed
14648 for ( i = 0, len = items.length; i < len; i++ ) {
14649 item = items[ i ];
14650 if ( item.isSelected() ) {
14651 this.selectItem( null );
14652 }
14653 }
14654
14655 // Mixin method
14656 OO.ui.GroupWidget.prototype.removeItems.call( this, items );
14657
14658 this.emit( 'remove', items );
14659
14660 return this;
14661 };
14662
14663 /**
14664 * Clear all options from the select. Options will be detached from the DOM, not removed,
14665 * so that they can be reused later. To remove a subset of options from the select, use
14666 * the #removeItems method.
14667 *
14668 * @fires remove
14669 * @chainable
14670 */
14671 OO.ui.SelectWidget.prototype.clearItems = function () {
14672 var items = this.items.slice();
14673
14674 // Mixin method
14675 OO.ui.GroupWidget.prototype.clearItems.call( this );
14676
14677 // Clear selection
14678 this.selectItem( null );
14679
14680 this.emit( 'remove', items );
14681
14682 return this;
14683 };
14684
14685 /**
14686 * ButtonSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains
14687 * button options and is used together with
14688 * OO.ui.ButtonOptionWidget. The ButtonSelectWidget provides an interface for
14689 * highlighting, choosing, and selecting mutually exclusive options. Please see
14690 * the [OOjs UI documentation on MediaWiki] [1] for more information.
14691 *
14692 * @example
14693 * // Example: A ButtonSelectWidget that contains three ButtonOptionWidgets
14694 * var option1 = new OO.ui.ButtonOptionWidget( {
14695 * data: 1,
14696 * label: 'Option 1',
14697 * title: 'Button option 1'
14698 * } );
14699 *
14700 * var option2 = new OO.ui.ButtonOptionWidget( {
14701 * data: 2,
14702 * label: 'Option 2',
14703 * title: 'Button option 2'
14704 * } );
14705 *
14706 * var option3 = new OO.ui.ButtonOptionWidget( {
14707 * data: 3,
14708 * label: 'Option 3',
14709 * title: 'Button option 3'
14710 * } );
14711 *
14712 * var buttonSelect=new OO.ui.ButtonSelectWidget( {
14713 * items: [ option1, option2, option3 ]
14714 * } );
14715 * $( 'body' ).append( buttonSelect.$element );
14716 *
14717 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
14718 *
14719 * @class
14720 * @extends OO.ui.SelectWidget
14721 * @mixins OO.ui.TabIndexedElement
14722 *
14723 * @constructor
14724 * @param {Object} [config] Configuration options
14725 */
14726 OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
14727 // Parent constructor
14728 OO.ui.ButtonSelectWidget.super.call( this, config );
14729
14730 // Mixin constructors
14731 OO.ui.TabIndexedElement.call( this, config );
14732
14733 // Events
14734 this.$element.on( {
14735 focus: this.bindKeyDownListener.bind( this ),
14736 blur: this.unbindKeyDownListener.bind( this )
14737 } );
14738
14739 // Initialization
14740 this.$element.addClass( 'oo-ui-buttonSelectWidget' );
14741 };
14742
14743 /* Setup */
14744
14745 OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
14746 OO.mixinClass( OO.ui.ButtonSelectWidget, OO.ui.TabIndexedElement );
14747
14748 /**
14749 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
14750 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
14751 * an interface for adding, removing and selecting options.
14752 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
14753 *
14754 * @example
14755 * // A RadioSelectWidget with RadioOptions.
14756 * var option1 = new OO.ui.RadioOptionWidget( {
14757 * data: 'a',
14758 * label: 'Selected radio option'
14759 * } );
14760 *
14761 * var option2 = new OO.ui.RadioOptionWidget( {
14762 * data: 'b',
14763 * label: 'Unselected radio option'
14764 * } );
14765 *
14766 * var radioSelect=new OO.ui.RadioSelectWidget( {
14767 * items: [ option1, option2 ]
14768 * } );
14769 *
14770 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
14771 * radioSelect.selectItem( option1 );
14772 *
14773 * $( 'body' ).append( radioSelect.$element );
14774 *
14775 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
14776
14777 *
14778 * @class
14779 * @extends OO.ui.SelectWidget
14780 * @mixins OO.ui.TabIndexedElement
14781 *
14782 * @constructor
14783 * @param {Object} [config] Configuration options
14784 */
14785 OO.ui.RadioSelectWidget = function OoUiRadioSelectWidget( config ) {
14786 // Parent constructor
14787 OO.ui.RadioSelectWidget.super.call( this, config );
14788
14789 // Mixin constructors
14790 OO.ui.TabIndexedElement.call( this, config );
14791
14792 // Events
14793 this.$element.on( {
14794 focus: this.bindKeyDownListener.bind( this ),
14795 blur: this.unbindKeyDownListener.bind( this )
14796 } );
14797
14798 // Initialization
14799 this.$element.addClass( 'oo-ui-radioSelectWidget' );
14800 };
14801
14802 /* Setup */
14803
14804 OO.inheritClass( OO.ui.RadioSelectWidget, OO.ui.SelectWidget );
14805 OO.mixinClass( OO.ui.RadioSelectWidget, OO.ui.TabIndexedElement );
14806
14807 /**
14808 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
14809 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
14810 * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxWidget ComboBoxWidget},
14811 * and {@link OO.ui.LookupElement LookupElement} for examples of widgets that contain menus.
14812 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
14813 * and customized to be opened, closed, and displayed as needed.
14814 *
14815 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
14816 * mouse outside the menu.
14817 *
14818 * Menus also have support for keyboard interaction:
14819 *
14820 * - Enter/Return key: choose and select a menu option
14821 * - Up-arrow key: highlight the previous menu option
14822 * - Down-arrow key: highlight the next menu option
14823 * - Esc key: hide the menu
14824 *
14825 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
14826 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
14827 *
14828 * @class
14829 * @extends OO.ui.SelectWidget
14830 * @mixins OO.ui.ClippableElement
14831 *
14832 * @constructor
14833 * @param {Object} [config] Configuration options
14834 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
14835 * the text the user types. This config is used by {@link OO.ui.ComboBoxWidget ComboBoxWidget}
14836 * and {@link OO.ui.LookupElement LookupElement}
14837 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu’s active state. If the user clicks the mouse
14838 * anywhere on the page outside of this widget, the menu is hidden.
14839 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
14840 */
14841 OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) {
14842 // Configuration initialization
14843 config = config || {};
14844
14845 // Parent constructor
14846 OO.ui.MenuSelectWidget.super.call( this, config );
14847
14848 // Mixin constructors
14849 OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
14850
14851 // Properties
14852 this.newItems = null;
14853 this.autoHide = config.autoHide === undefined || !!config.autoHide;
14854 this.$input = config.input ? config.input.$input : null;
14855 this.$widget = config.widget ? config.widget.$element : null;
14856 this.onDocumentMouseDownHandler = this.onDocumentMouseDown.bind( this );
14857
14858 // Initialization
14859 this.$element
14860 .addClass( 'oo-ui-menuSelectWidget' )
14861 .attr( 'role', 'menu' );
14862
14863 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
14864 // that reference properties not initialized at that time of parent class construction
14865 // TODO: Find a better way to handle post-constructor setup
14866 this.visible = false;
14867 this.$element.addClass( 'oo-ui-element-hidden' );
14868 };
14869
14870 /* Setup */
14871
14872 OO.inheritClass( OO.ui.MenuSelectWidget, OO.ui.SelectWidget );
14873 OO.mixinClass( OO.ui.MenuSelectWidget, OO.ui.ClippableElement );
14874
14875 /* Methods */
14876
14877 /**
14878 * Handles document mouse down events.
14879 *
14880 * @protected
14881 * @param {jQuery.Event} e Key down event
14882 */
14883 OO.ui.MenuSelectWidget.prototype.onDocumentMouseDown = function ( e ) {
14884 if (
14885 !OO.ui.contains( this.$element[ 0 ], e.target, true ) &&
14886 ( !this.$widget || !OO.ui.contains( this.$widget[ 0 ], e.target, true ) )
14887 ) {
14888 this.toggle( false );
14889 }
14890 };
14891
14892 /**
14893 * @inheritdoc
14894 */
14895 OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) {
14896 var currentItem = this.getHighlightedItem() || this.getSelectedItem();
14897
14898 if ( !this.isDisabled() && this.isVisible() ) {
14899 switch ( e.keyCode ) {
14900 case OO.ui.Keys.LEFT:
14901 case OO.ui.Keys.RIGHT:
14902 // Do nothing if a text field is associated, arrow keys will be handled natively
14903 if ( !this.$input ) {
14904 OO.ui.MenuSelectWidget.super.prototype.onKeyDown.call( this, e );
14905 }
14906 break;
14907 case OO.ui.Keys.ESCAPE:
14908 case OO.ui.Keys.TAB:
14909 if ( currentItem ) {
14910 currentItem.setHighlighted( false );
14911 }
14912 this.toggle( false );
14913 // Don't prevent tabbing away, prevent defocusing
14914 if ( e.keyCode === OO.ui.Keys.ESCAPE ) {
14915 e.preventDefault();
14916 e.stopPropagation();
14917 }
14918 break;
14919 default:
14920 OO.ui.MenuSelectWidget.super.prototype.onKeyDown.call( this, e );
14921 return;
14922 }
14923 }
14924 };
14925
14926 /**
14927 * @inheritdoc
14928 */
14929 OO.ui.MenuSelectWidget.prototype.bindKeyDownListener = function () {
14930 if ( this.$input ) {
14931 this.$input.on( 'keydown', this.onKeyDownHandler );
14932 } else {
14933 OO.ui.MenuSelectWidget.super.prototype.bindKeyDownListener.call( this );
14934 }
14935 };
14936
14937 /**
14938 * @inheritdoc
14939 */
14940 OO.ui.MenuSelectWidget.prototype.unbindKeyDownListener = function () {
14941 if ( this.$input ) {
14942 this.$input.off( 'keydown', this.onKeyDownHandler );
14943 } else {
14944 OO.ui.MenuSelectWidget.super.prototype.unbindKeyDownListener.call( this );
14945 }
14946 };
14947
14948 /**
14949 * Choose an item.
14950 *
14951 * When a user chooses an item, the menu is closed.
14952 *
14953 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
14954 * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
14955 * @param {OO.ui.OptionWidget} item Item to choose
14956 * @chainable
14957 */
14958 OO.ui.MenuSelectWidget.prototype.chooseItem = function ( item ) {
14959 OO.ui.MenuSelectWidget.super.prototype.chooseItem.call( this, item );
14960 this.toggle( false );
14961 return this;
14962 };
14963
14964 /**
14965 * @inheritdoc
14966 */
14967 OO.ui.MenuSelectWidget.prototype.addItems = function ( items, index ) {
14968 var i, len, item;
14969
14970 // Parent method
14971 OO.ui.MenuSelectWidget.super.prototype.addItems.call( this, items, index );
14972
14973 // Auto-initialize
14974 if ( !this.newItems ) {
14975 this.newItems = [];
14976 }
14977
14978 for ( i = 0, len = items.length; i < len; i++ ) {
14979 item = items[ i ];
14980 if ( this.isVisible() ) {
14981 // Defer fitting label until item has been attached
14982 item.fitLabel();
14983 } else {
14984 this.newItems.push( item );
14985 }
14986 }
14987
14988 // Reevaluate clipping
14989 this.clip();
14990
14991 return this;
14992 };
14993
14994 /**
14995 * @inheritdoc
14996 */
14997 OO.ui.MenuSelectWidget.prototype.removeItems = function ( items ) {
14998 // Parent method
14999 OO.ui.MenuSelectWidget.super.prototype.removeItems.call( this, items );
15000
15001 // Reevaluate clipping
15002 this.clip();
15003
15004 return this;
15005 };
15006
15007 /**
15008 * @inheritdoc
15009 */
15010 OO.ui.MenuSelectWidget.prototype.clearItems = function () {
15011 // Parent method
15012 OO.ui.MenuSelectWidget.super.prototype.clearItems.call( this );
15013
15014 // Reevaluate clipping
15015 this.clip();
15016
15017 return this;
15018 };
15019
15020 /**
15021 * @inheritdoc
15022 */
15023 OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
15024 visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
15025
15026 var i, len,
15027 change = visible !== this.isVisible();
15028
15029 // Parent method
15030 OO.ui.MenuSelectWidget.super.prototype.toggle.call( this, visible );
15031
15032 if ( change ) {
15033 if ( visible ) {
15034 this.bindKeyDownListener();
15035
15036 if ( this.newItems && this.newItems.length ) {
15037 for ( i = 0, len = this.newItems.length; i < len; i++ ) {
15038 this.newItems[ i ].fitLabel();
15039 }
15040 this.newItems = null;
15041 }
15042 this.toggleClipping( true );
15043
15044 // Auto-hide
15045 if ( this.autoHide ) {
15046 this.getElementDocument().addEventListener(
15047 'mousedown', this.onDocumentMouseDownHandler, true
15048 );
15049 }
15050 } else {
15051 this.unbindKeyDownListener();
15052 this.getElementDocument().removeEventListener(
15053 'mousedown', this.onDocumentMouseDownHandler, true
15054 );
15055 this.toggleClipping( false );
15056 }
15057 }
15058
15059 return this;
15060 };
15061
15062 /**
15063 * TextInputMenuSelectWidget is a menu that is specially designed to be positioned beneath
15064 * a {@link OO.ui.TextInputWidget text input} field. The menu's position is automatically
15065 * calculated and maintained when the menu is toggled or the window is resized.
15066 * See OO.ui.ComboBoxWidget for an example of a widget that uses this class.
15067 *
15068 * @class
15069 * @extends OO.ui.MenuSelectWidget
15070 *
15071 * @constructor
15072 * @param {OO.ui.TextInputWidget} inputWidget Text input widget to provide menu for
15073 * @param {Object} [config] Configuration options
15074 * @cfg {jQuery} [$container=input.$element] Element to render menu under
15075 */
15076 OO.ui.TextInputMenuSelectWidget = function OoUiTextInputMenuSelectWidget( inputWidget, config ) {
15077 // Allow passing positional parameters inside the config object
15078 if ( OO.isPlainObject( inputWidget ) && config === undefined ) {
15079 config = inputWidget;
15080 inputWidget = config.inputWidget;
15081 }
15082
15083 // Configuration initialization
15084 config = config || {};
15085
15086 // Parent constructor
15087 OO.ui.TextInputMenuSelectWidget.super.call( this, config );
15088
15089 // Properties
15090 this.inputWidget = inputWidget;
15091 this.$container = config.$container || this.inputWidget.$element;
15092 this.onWindowResizeHandler = this.onWindowResize.bind( this );
15093
15094 // Initialization
15095 this.$element.addClass( 'oo-ui-textInputMenuSelectWidget' );
15096 };
15097
15098 /* Setup */
15099
15100 OO.inheritClass( OO.ui.TextInputMenuSelectWidget, OO.ui.MenuSelectWidget );
15101
15102 /* Methods */
15103
15104 /**
15105 * Handle window resize event.
15106 *
15107 * @private
15108 * @param {jQuery.Event} e Window resize event
15109 */
15110 OO.ui.TextInputMenuSelectWidget.prototype.onWindowResize = function () {
15111 this.position();
15112 };
15113
15114 /**
15115 * @inheritdoc
15116 */
15117 OO.ui.TextInputMenuSelectWidget.prototype.toggle = function ( visible ) {
15118 visible = visible === undefined ? !this.isVisible() : !!visible;
15119
15120 var change = visible !== this.isVisible();
15121
15122 if ( change && visible ) {
15123 // Make sure the width is set before the parent method runs.
15124 // After this we have to call this.position(); again to actually
15125 // position ourselves correctly.
15126 this.position();
15127 }
15128
15129 // Parent method
15130 OO.ui.TextInputMenuSelectWidget.super.prototype.toggle.call( this, visible );
15131
15132 if ( change ) {
15133 if ( this.isVisible() ) {
15134 this.position();
15135 $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
15136 } else {
15137 $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
15138 }
15139 }
15140
15141 return this;
15142 };
15143
15144 /**
15145 * Position the menu.
15146 *
15147 * @private
15148 * @chainable
15149 */
15150 OO.ui.TextInputMenuSelectWidget.prototype.position = function () {
15151 var $container = this.$container,
15152 pos = OO.ui.Element.static.getRelativePosition( $container, this.$element.offsetParent() );
15153
15154 // Position under input
15155 pos.top += $container.height();
15156 this.$element.css( pos );
15157
15158 // Set width
15159 this.setIdealSize( $container.width() );
15160 // We updated the position, so re-evaluate the clipping state
15161 this.clip();
15162
15163 return this;
15164 };
15165
15166 /**
15167 * OutlineSelectWidget is a structured list that contains {@link OO.ui.OutlineOptionWidget outline options}
15168 * A set of controls can be provided with an {@link OO.ui.OutlineControlsWidget outline controls} widget.
15169 *
15170 * ####Currently, this class is only used by {@link OO.ui.BookletLayout BookletLayouts}.####
15171 *
15172 * @class
15173 * @extends OO.ui.SelectWidget
15174 * @mixins OO.ui.TabIndexedElement
15175 *
15176 * @constructor
15177 * @param {Object} [config] Configuration options
15178 */
15179 OO.ui.OutlineSelectWidget = function OoUiOutlineSelectWidget( config ) {
15180 // Parent constructor
15181 OO.ui.OutlineSelectWidget.super.call( this, config );
15182
15183 // Mixin constructors
15184 OO.ui.TabIndexedElement.call( this, config );
15185
15186 // Events
15187 this.$element.on( {
15188 focus: this.bindKeyDownListener.bind( this ),
15189 blur: this.unbindKeyDownListener.bind( this )
15190 } );
15191
15192 // Initialization
15193 this.$element.addClass( 'oo-ui-outlineSelectWidget' );
15194 };
15195
15196 /* Setup */
15197
15198 OO.inheritClass( OO.ui.OutlineSelectWidget, OO.ui.SelectWidget );
15199 OO.mixinClass( OO.ui.OutlineSelectWidget, OO.ui.TabIndexedElement );
15200
15201 /**
15202 * ToggleSwitches are switches that slide on and off. Their state is represented by a Boolean
15203 * value (`true` for ‘on’, and `false` otherwise, the default). The ‘off’ state is represented
15204 * visually by a slider in the leftmost position.
15205 *
15206 * @example
15207 * // Toggle switches in the 'off' and 'on' position.
15208 * var toggleSwitch1 = new OO.ui.ToggleSwitchWidget();
15209 * var toggleSwitch2 = new OO.ui.ToggleSwitchWidget( {
15210 * value: true
15211 * } );
15212 *
15213 * // Create a FieldsetLayout to layout and label switches
15214 * var fieldset = new OO.ui.FieldsetLayout( {
15215 * label: 'Toggle switches'
15216 * } );
15217 * fieldset.addItems( [
15218 * new OO.ui.FieldLayout( toggleSwitch1, { label: 'Off', align: 'top' } ),
15219 * new OO.ui.FieldLayout( toggleSwitch2, { label: 'On', align: 'top' } )
15220 * ] );
15221 * $( 'body' ).append( fieldset.$element );
15222 *
15223 * @class
15224 * @extends OO.ui.ToggleWidget
15225 * @mixins OO.ui.TabIndexedElement
15226 *
15227 * @constructor
15228 * @param {Object} [config] Configuration options
15229 * @cfg {boolean} [value=false] The toggle switch’s initial on/off state.
15230 * By default, the toggle switch is in the 'off' position.
15231 */
15232 OO.ui.ToggleSwitchWidget = function OoUiToggleSwitchWidget( config ) {
15233 // Parent constructor
15234 OO.ui.ToggleSwitchWidget.super.call( this, config );
15235
15236 // Mixin constructors
15237 OO.ui.TabIndexedElement.call( this, config );
15238
15239 // Properties
15240 this.dragging = false;
15241 this.dragStart = null;
15242 this.sliding = false;
15243 this.$glow = $( '<span>' );
15244 this.$grip = $( '<span>' );
15245
15246 // Events
15247 this.$element.on( {
15248 click: this.onClick.bind( this ),
15249 keypress: this.onKeyPress.bind( this )
15250 } );
15251
15252 // Initialization
15253 this.$glow.addClass( 'oo-ui-toggleSwitchWidget-glow' );
15254 this.$grip.addClass( 'oo-ui-toggleSwitchWidget-grip' );
15255 this.$element
15256 .addClass( 'oo-ui-toggleSwitchWidget' )
15257 .attr( 'role', 'checkbox' )
15258 .append( this.$glow, this.$grip );
15259 };
15260
15261 /* Setup */
15262
15263 OO.inheritClass( OO.ui.ToggleSwitchWidget, OO.ui.ToggleWidget );
15264 OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.TabIndexedElement );
15265
15266 /* Methods */
15267
15268 /**
15269 * Handle mouse click events.
15270 *
15271 * @private
15272 * @param {jQuery.Event} e Mouse click event
15273 */
15274 OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
15275 if ( !this.isDisabled() && e.which === 1 ) {
15276 this.setValue( !this.value );
15277 }
15278 return false;
15279 };
15280
15281 /**
15282 * Handle key press events.
15283 *
15284 * @private
15285 * @param {jQuery.Event} e Key press event
15286 */
15287 OO.ui.ToggleSwitchWidget.prototype.onKeyPress = function ( e ) {
15288 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
15289 this.setValue( !this.value );
15290 return false;
15291 }
15292 };
15293
15294 }( OO ) );