Merge "Add Content-Length header to thumb.php error responses"
[lhc/web/wiklou.git] / resources / lib / oojs-ui / oojs-ui.js
1 /*!
2 * OOjs UI v0.11.8
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-07-08T01:31:38Z
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 * @property {Number}
49 */
50 OO.ui.elementId = 0;
51
52 /**
53 * Generate a unique ID for element
54 *
55 * @return {String} [id]
56 */
57 OO.ui.generateElementId = function () {
58 OO.ui.elementId += 1;
59 return 'oojsui-' + OO.ui.elementId;
60 };
61
62 /**
63 * Check if an element is focusable.
64 * Inspired from :focusable in jQueryUI v1.11.4 - 2015-04-14
65 *
66 * @param {jQuery} element Element to test
67 * @return {Boolean} [description]
68 */
69 OO.ui.isFocusableElement = function ( $element ) {
70 var node = $element[0],
71 nodeName = node.nodeName.toLowerCase(),
72 // Check if the element have tabindex set
73 isInElementGroup = /^(input|select|textarea|button|object)$/.test( nodeName ),
74 // Check if the element is a link with href or if it has tabindex
75 isOtherElement = (
76 ( nodeName === 'a' && node.href ) ||
77 !isNaN( $element.attr( 'tabindex' ) )
78 ),
79 // Check if the element is visible
80 isVisible = (
81 // This is quicker than calling $element.is( ':visible' )
82 $.expr.filters.visible( node ) &&
83 // Check that all parents are visible
84 !$element.parents().addBack().filter( function () {
85 return $.css( this, 'visibility' ) === 'hidden';
86 } ).length
87 );
88
89 return (
90 ( isInElementGroup ? !node.disabled : isOtherElement ) &&
91 isVisible
92 );
93 };
94
95 /**
96 * Get the user's language and any fallback languages.
97 *
98 * These language codes are used to localize user interface elements in the user's language.
99 *
100 * In environments that provide a localization system, this function should be overridden to
101 * return the user's language(s). The default implementation returns English (en) only.
102 *
103 * @return {string[]} Language codes, in descending order of priority
104 */
105 OO.ui.getUserLanguages = function () {
106 return [ 'en' ];
107 };
108
109 /**
110 * Get a value in an object keyed by language code.
111 *
112 * @param {Object.<string,Mixed>} obj Object keyed by language code
113 * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
114 * @param {string} [fallback] Fallback code, used if no matching language can be found
115 * @return {Mixed} Local value
116 */
117 OO.ui.getLocalValue = function ( obj, lang, fallback ) {
118 var i, len, langs;
119
120 // Requested language
121 if ( obj[ lang ] ) {
122 return obj[ lang ];
123 }
124 // Known user language
125 langs = OO.ui.getUserLanguages();
126 for ( i = 0, len = langs.length; i < len; i++ ) {
127 lang = langs[ i ];
128 if ( obj[ lang ] ) {
129 return obj[ lang ];
130 }
131 }
132 // Fallback language
133 if ( obj[ fallback ] ) {
134 return obj[ fallback ];
135 }
136 // First existing language
137 for ( lang in obj ) {
138 return obj[ lang ];
139 }
140
141 return undefined;
142 };
143
144 /**
145 * Check if a node is contained within another node
146 *
147 * Similar to jQuery#contains except a list of containers can be supplied
148 * and a boolean argument allows you to include the container in the match list
149 *
150 * @param {HTMLElement|HTMLElement[]} containers Container node(s) to search in
151 * @param {HTMLElement} contained Node to find
152 * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match, otherwise only match descendants
153 * @return {boolean} The node is in the list of target nodes
154 */
155 OO.ui.contains = function ( containers, contained, matchContainers ) {
156 var i;
157 if ( !Array.isArray( containers ) ) {
158 containers = [ containers ];
159 }
160 for ( i = containers.length - 1; i >= 0; i-- ) {
161 if ( ( matchContainers && contained === containers[ i ] ) || $.contains( containers[ i ], contained ) ) {
162 return true;
163 }
164 }
165 return false;
166 };
167
168 /**
169 * Return a function, that, as long as it continues to be invoked, will not
170 * be triggered. The function will be called after it stops being called for
171 * N milliseconds. If `immediate` is passed, trigger the function on the
172 * leading edge, instead of the trailing.
173 *
174 * Ported from: http://underscorejs.org/underscore.js
175 *
176 * @param {Function} func
177 * @param {number} wait
178 * @param {boolean} immediate
179 * @return {Function}
180 */
181 OO.ui.debounce = function ( func, wait, immediate ) {
182 var timeout;
183 return function () {
184 var context = this,
185 args = arguments,
186 later = function () {
187 timeout = null;
188 if ( !immediate ) {
189 func.apply( context, args );
190 }
191 };
192 if ( immediate && !timeout ) {
193 func.apply( context, args );
194 }
195 clearTimeout( timeout );
196 timeout = setTimeout( later, wait );
197 };
198 };
199
200 /**
201 * Reconstitute a JavaScript object corresponding to a widget created by
202 * the PHP implementation.
203 *
204 * This is an alias for `OO.ui.Element.static.infuse()`.
205 *
206 * @param {string|HTMLElement|jQuery} idOrNode
207 * A DOM id (if a string) or node for the widget to infuse.
208 * @return {OO.ui.Element}
209 * The `OO.ui.Element` corresponding to this (infusable) document node.
210 */
211 OO.ui.infuse = function ( idOrNode ) {
212 return OO.ui.Element.static.infuse( idOrNode );
213 };
214
215 ( function () {
216 /**
217 * Message store for the default implementation of OO.ui.msg
218 *
219 * Environments that provide a localization system should not use this, but should override
220 * OO.ui.msg altogether.
221 *
222 * @private
223 */
224 var messages = {
225 // Tool tip for a button that moves items in a list down one place
226 'ooui-outline-control-move-down': 'Move item down',
227 // Tool tip for a button that moves items in a list up one place
228 'ooui-outline-control-move-up': 'Move item up',
229 // Tool tip for a button that removes items from a list
230 'ooui-outline-control-remove': 'Remove item',
231 // Label for the toolbar group that contains a list of all other available tools
232 'ooui-toolbar-more': 'More',
233 // Label for the fake tool that expands the full list of tools in a toolbar group
234 'ooui-toolgroup-expand': 'More',
235 // Label for the fake tool that collapses the full list of tools in a toolbar group
236 'ooui-toolgroup-collapse': 'Fewer',
237 // Default label for the accept button of a confirmation dialog
238 'ooui-dialog-message-accept': 'OK',
239 // Default label for the reject button of a confirmation dialog
240 'ooui-dialog-message-reject': 'Cancel',
241 // Title for process dialog error description
242 'ooui-dialog-process-error': 'Something went wrong',
243 // Label for process dialog dismiss error button, visible when describing errors
244 'ooui-dialog-process-dismiss': 'Dismiss',
245 // Label for process dialog retry action button, visible when describing only recoverable errors
246 'ooui-dialog-process-retry': 'Try again',
247 // Label for process dialog retry action button, visible when describing only warnings
248 'ooui-dialog-process-continue': 'Continue',
249 // Default placeholder for file selection widgets
250 'ooui-selectfile-not-supported': 'File selection is not supported',
251 // Default placeholder for file selection widgets
252 'ooui-selectfile-placeholder': 'No file is selected',
253 // Semicolon separator
254 'ooui-semicolon-separator': '; '
255 };
256
257 /**
258 * Get a localized message.
259 *
260 * In environments that provide a localization system, this function should be overridden to
261 * return the message translated in the user's language. The default implementation always returns
262 * English messages.
263 *
264 * After the message key, message parameters may optionally be passed. In the default implementation,
265 * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
266 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
267 * they support unnamed, ordered message parameters.
268 *
269 * @abstract
270 * @param {string} key Message key
271 * @param {Mixed...} [params] Message parameters
272 * @return {string} Translated message with parameters substituted
273 */
274 OO.ui.msg = function ( key ) {
275 var message = messages[ key ],
276 params = Array.prototype.slice.call( arguments, 1 );
277 if ( typeof message === 'string' ) {
278 // Perform $1 substitution
279 message = message.replace( /\$(\d+)/g, function ( unused, n ) {
280 var i = parseInt( n, 10 );
281 return params[ i - 1 ] !== undefined ? params[ i - 1 ] : '$' + n;
282 } );
283 } else {
284 // Return placeholder if message not found
285 message = '[' + key + ']';
286 }
287 return message;
288 };
289
290 /**
291 * Package a message and arguments for deferred resolution.
292 *
293 * Use this when you are statically specifying a message and the message may not yet be present.
294 *
295 * @param {string} key Message key
296 * @param {Mixed...} [params] Message parameters
297 * @return {Function} Function that returns the resolved message when executed
298 */
299 OO.ui.deferMsg = function () {
300 var args = arguments;
301 return function () {
302 return OO.ui.msg.apply( OO.ui, args );
303 };
304 };
305
306 /**
307 * Resolve a message.
308 *
309 * If the message is a function it will be executed, otherwise it will pass through directly.
310 *
311 * @param {Function|string} msg Deferred message, or message text
312 * @return {string} Resolved message
313 */
314 OO.ui.resolveMsg = function ( msg ) {
315 if ( $.isFunction( msg ) ) {
316 return msg();
317 }
318 return msg;
319 };
320
321 } )();
322
323 /*!
324 * Mixin namespace.
325 */
326
327 /**
328 * Namespace for OOjs UI mixins.
329 *
330 * Mixins are named according to the type of object they are intended to
331 * be mixed in to. For example, OO.ui.mixin.GroupElement is intended to be
332 * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
333 * is intended to be mixed in to an instance of OO.ui.Widget.
334 *
335 * @class
336 * @singleton
337 */
338 OO.ui.mixin = {};
339
340 /**
341 * PendingElement is a mixin that is used to create elements that notify users that something is happening
342 * and that they should wait before proceeding. The pending state is visually represented with a pending
343 * texture that appears in the head of a pending {@link OO.ui.ProcessDialog process dialog} or in the input
344 * field of a {@link OO.ui.TextInputWidget text input widget}.
345 *
346 * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked as pending, but only when
347 * used in {@link OO.ui.MessageDialog message dialogs}. The behavior is not currently supported for action widgets used
348 * in process dialogs.
349 *
350 * @example
351 * function MessageDialog( config ) {
352 * MessageDialog.parent.call( this, config );
353 * }
354 * OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
355 *
356 * MessageDialog.static.actions = [
357 * { action: 'save', label: 'Done', flags: 'primary' },
358 * { label: 'Cancel', flags: 'safe' }
359 * ];
360 *
361 * MessageDialog.prototype.initialize = function () {
362 * MessageDialog.parent.prototype.initialize.apply( this, arguments );
363 * this.content = new OO.ui.PanelLayout( { $: this.$, padded: true } );
364 * this.content.$element.append( '<p>Click the \'Done\' action widget to see its pending state. Note that action widgets can be marked pending in message dialogs but not process dialogs.</p>' );
365 * this.$body.append( this.content.$element );
366 * };
367 * MessageDialog.prototype.getBodyHeight = function () {
368 * return 100;
369 * }
370 * MessageDialog.prototype.getActionProcess = function ( action ) {
371 * var dialog = this;
372 * if ( action === 'save' ) {
373 * dialog.getActions().get({actions: 'save'})[0].pushPending();
374 * return new OO.ui.Process()
375 * .next( 1000 )
376 * .next( function () {
377 * dialog.getActions().get({actions: 'save'})[0].popPending();
378 * } );
379 * }
380 * return MessageDialog.parent.prototype.getActionProcess.call( this, action );
381 * };
382 *
383 * var windowManager = new OO.ui.WindowManager();
384 * $( 'body' ).append( windowManager.$element );
385 *
386 * var dialog = new MessageDialog();
387 * windowManager.addWindows( [ dialog ] );
388 * windowManager.openWindow( dialog );
389 *
390 * @abstract
391 * @class
392 *
393 * @constructor
394 * @param {Object} [config] Configuration options
395 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
396 */
397 OO.ui.mixin.PendingElement = function OoUiMixinPendingElement( config ) {
398 // Configuration initialization
399 config = config || {};
400
401 // Properties
402 this.pending = 0;
403 this.$pending = null;
404
405 // Initialisation
406 this.setPendingElement( config.$pending || this.$element );
407 };
408
409 /* Setup */
410
411 OO.initClass( OO.ui.mixin.PendingElement );
412
413 /* Methods */
414
415 /**
416 * Set the pending element (and clean up any existing one).
417 *
418 * @param {jQuery} $pending The element to set to pending.
419 */
420 OO.ui.mixin.PendingElement.prototype.setPendingElement = function ( $pending ) {
421 if ( this.$pending ) {
422 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
423 }
424
425 this.$pending = $pending;
426 if ( this.pending > 0 ) {
427 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
428 }
429 };
430
431 /**
432 * Check if an element is pending.
433 *
434 * @return {boolean} Element is pending
435 */
436 OO.ui.mixin.PendingElement.prototype.isPending = function () {
437 return !!this.pending;
438 };
439
440 /**
441 * Increase the pending counter. The pending state will remain active until the counter is zero
442 * (i.e., the number of calls to #pushPending and #popPending is the same).
443 *
444 * @chainable
445 */
446 OO.ui.mixin.PendingElement.prototype.pushPending = function () {
447 if ( this.pending === 0 ) {
448 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
449 this.updateThemeClasses();
450 }
451 this.pending++;
452
453 return this;
454 };
455
456 /**
457 * Decrease the pending counter. The pending state will remain active until the counter is zero
458 * (i.e., the number of calls to #pushPending and #popPending is the same).
459 *
460 * @chainable
461 */
462 OO.ui.mixin.PendingElement.prototype.popPending = function () {
463 if ( this.pending === 1 ) {
464 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
465 this.updateThemeClasses();
466 }
467 this.pending = Math.max( 0, this.pending - 1 );
468
469 return this;
470 };
471
472 /**
473 * ActionSets manage the behavior of the {@link OO.ui.ActionWidget action widgets} that comprise them.
474 * Actions can be made available for specific contexts (modes) and circumstances
475 * (abilities). Action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
476 *
477 * ActionSets contain two types of actions:
478 *
479 * - 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.
480 * - Other: Other actions include all non-special visible actions.
481 *
482 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
483 *
484 * @example
485 * // Example: An action set used in a process dialog
486 * function MyProcessDialog( config ) {
487 * MyProcessDialog.parent.call( this, config );
488 * }
489 * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
490 * MyProcessDialog.static.title = 'An action set in a process dialog';
491 * // An action set that uses modes ('edit' and 'help' mode, in this example).
492 * MyProcessDialog.static.actions = [
493 * { action: 'continue', modes: 'edit', label: 'Continue', flags: [ 'primary', 'constructive' ] },
494 * { action: 'help', modes: 'edit', label: 'Help' },
495 * { modes: 'edit', label: 'Cancel', flags: 'safe' },
496 * { action: 'back', modes: 'help', label: 'Back', flags: 'safe' }
497 * ];
498 *
499 * MyProcessDialog.prototype.initialize = function () {
500 * MyProcessDialog.parent.prototype.initialize.apply( this, arguments );
501 * this.panel1 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
502 * 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>' );
503 * this.panel2 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
504 * 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>' );
505 * this.stackLayout = new OO.ui.StackLayout( {
506 * items: [ this.panel1, this.panel2 ]
507 * } );
508 * this.$body.append( this.stackLayout.$element );
509 * };
510 * MyProcessDialog.prototype.getSetupProcess = function ( data ) {
511 * return MyProcessDialog.parent.prototype.getSetupProcess.call( this, data )
512 * .next( function () {
513 * this.actions.setMode( 'edit' );
514 * }, this );
515 * };
516 * MyProcessDialog.prototype.getActionProcess = function ( action ) {
517 * if ( action === 'help' ) {
518 * this.actions.setMode( 'help' );
519 * this.stackLayout.setItem( this.panel2 );
520 * } else if ( action === 'back' ) {
521 * this.actions.setMode( 'edit' );
522 * this.stackLayout.setItem( this.panel1 );
523 * } else if ( action === 'continue' ) {
524 * var dialog = this;
525 * return new OO.ui.Process( function () {
526 * dialog.close();
527 * } );
528 * }
529 * return MyProcessDialog.parent.prototype.getActionProcess.call( this, action );
530 * };
531 * MyProcessDialog.prototype.getBodyHeight = function () {
532 * return this.panel1.$element.outerHeight( true );
533 * };
534 * var windowManager = new OO.ui.WindowManager();
535 * $( 'body' ).append( windowManager.$element );
536 * var dialog = new MyProcessDialog( {
537 * size: 'medium'
538 * } );
539 * windowManager.addWindows( [ dialog ] );
540 * windowManager.openWindow( dialog );
541 *
542 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
543 *
544 * @abstract
545 * @class
546 * @mixins OO.EventEmitter
547 *
548 * @constructor
549 * @param {Object} [config] Configuration options
550 */
551 OO.ui.ActionSet = function OoUiActionSet( config ) {
552 // Configuration initialization
553 config = config || {};
554
555 // Mixin constructors
556 OO.EventEmitter.call( this );
557
558 // Properties
559 this.list = [];
560 this.categories = {
561 actions: 'getAction',
562 flags: 'getFlags',
563 modes: 'getModes'
564 };
565 this.categorized = {};
566 this.special = {};
567 this.others = [];
568 this.organized = false;
569 this.changing = false;
570 this.changed = false;
571 };
572
573 /* Setup */
574
575 OO.mixinClass( OO.ui.ActionSet, OO.EventEmitter );
576
577 /* Static Properties */
578
579 /**
580 * Symbolic name of the flags used to identify special actions. Special actions are displayed in the
581 * header of a {@link OO.ui.ProcessDialog process dialog}.
582 * See the [OOjs UI documentation on MediaWiki][2] for more information and examples.
583 *
584 * [2]:https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
585 *
586 * @abstract
587 * @static
588 * @inheritable
589 * @property {string}
590 */
591 OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ];
592
593 /* Events */
594
595 /**
596 * @event click
597 *
598 * A 'click' event is emitted when an action is clicked.
599 *
600 * @param {OO.ui.ActionWidget} action Action that was clicked
601 */
602
603 /**
604 * @event resize
605 *
606 * A 'resize' event is emitted when an action widget is resized.
607 *
608 * @param {OO.ui.ActionWidget} action Action that was resized
609 */
610
611 /**
612 * @event add
613 *
614 * An 'add' event is emitted when actions are {@link #method-add added} to the action set.
615 *
616 * @param {OO.ui.ActionWidget[]} added Actions added
617 */
618
619 /**
620 * @event remove
621 *
622 * A 'remove' event is emitted when actions are {@link #method-remove removed}
623 * or {@link #clear cleared}.
624 *
625 * @param {OO.ui.ActionWidget[]} added Actions removed
626 */
627
628 /**
629 * @event change
630 *
631 * A 'change' event is emitted when actions are {@link #method-add added}, {@link #clear cleared},
632 * or {@link #method-remove removed} from the action set or when the {@link #setMode mode} is changed.
633 *
634 */
635
636 /* Methods */
637
638 /**
639 * Handle action change events.
640 *
641 * @private
642 * @fires change
643 */
644 OO.ui.ActionSet.prototype.onActionChange = function () {
645 this.organized = false;
646 if ( this.changing ) {
647 this.changed = true;
648 } else {
649 this.emit( 'change' );
650 }
651 };
652
653 /**
654 * Check if an action is one of the special actions.
655 *
656 * @param {OO.ui.ActionWidget} action Action to check
657 * @return {boolean} Action is special
658 */
659 OO.ui.ActionSet.prototype.isSpecial = function ( action ) {
660 var flag;
661
662 for ( flag in this.special ) {
663 if ( action === this.special[ flag ] ) {
664 return true;
665 }
666 }
667
668 return false;
669 };
670
671 /**
672 * Get action widgets based on the specified filter: ‘actions’, ‘flags’, ‘modes’, ‘visible’,
673 * or ‘disabled’.
674 *
675 * @param {Object} [filters] Filters to use, omit to get all actions
676 * @param {string|string[]} [filters.actions] Actions that action widgets must have
677 * @param {string|string[]} [filters.flags] Flags that action widgets must have (e.g., 'safe')
678 * @param {string|string[]} [filters.modes] Modes that action widgets must have
679 * @param {boolean} [filters.visible] Action widgets must be visible
680 * @param {boolean} [filters.disabled] Action widgets must be disabled
681 * @return {OO.ui.ActionWidget[]} Action widgets matching all criteria
682 */
683 OO.ui.ActionSet.prototype.get = function ( filters ) {
684 var i, len, list, category, actions, index, match, matches;
685
686 if ( filters ) {
687 this.organize();
688
689 // Collect category candidates
690 matches = [];
691 for ( category in this.categorized ) {
692 list = filters[ category ];
693 if ( list ) {
694 if ( !Array.isArray( list ) ) {
695 list = [ list ];
696 }
697 for ( i = 0, len = list.length; i < len; i++ ) {
698 actions = this.categorized[ category ][ list[ i ] ];
699 if ( Array.isArray( actions ) ) {
700 matches.push.apply( matches, actions );
701 }
702 }
703 }
704 }
705 // Remove by boolean filters
706 for ( i = 0, len = matches.length; i < len; i++ ) {
707 match = matches[ i ];
708 if (
709 ( filters.visible !== undefined && match.isVisible() !== filters.visible ) ||
710 ( filters.disabled !== undefined && match.isDisabled() !== filters.disabled )
711 ) {
712 matches.splice( i, 1 );
713 len--;
714 i--;
715 }
716 }
717 // Remove duplicates
718 for ( i = 0, len = matches.length; i < len; i++ ) {
719 match = matches[ i ];
720 index = matches.lastIndexOf( match );
721 while ( index !== i ) {
722 matches.splice( index, 1 );
723 len--;
724 index = matches.lastIndexOf( match );
725 }
726 }
727 return matches;
728 }
729 return this.list.slice();
730 };
731
732 /**
733 * Get 'special' actions.
734 *
735 * Special actions are the first visible action widgets with special flags, such as 'safe' and 'primary'.
736 * Special flags can be configured in subclasses by changing the static #specialFlags property.
737 *
738 * @return {OO.ui.ActionWidget[]|null} 'Special' action widgets.
739 */
740 OO.ui.ActionSet.prototype.getSpecial = function () {
741 this.organize();
742 return $.extend( {}, this.special );
743 };
744
745 /**
746 * Get 'other' actions.
747 *
748 * Other actions include all non-special visible action widgets.
749 *
750 * @return {OO.ui.ActionWidget[]} 'Other' action widgets
751 */
752 OO.ui.ActionSet.prototype.getOthers = function () {
753 this.organize();
754 return this.others.slice();
755 };
756
757 /**
758 * Set the mode (e.g., ‘edit’ or ‘view’). Only {@link OO.ui.ActionWidget#modes actions} configured
759 * to be available in the specified mode will be made visible. All other actions will be hidden.
760 *
761 * @param {string} mode The mode. Only actions configured to be available in the specified
762 * mode will be made visible.
763 * @chainable
764 * @fires toggle
765 * @fires change
766 */
767 OO.ui.ActionSet.prototype.setMode = function ( mode ) {
768 var i, len, action;
769
770 this.changing = true;
771 for ( i = 0, len = this.list.length; i < len; i++ ) {
772 action = this.list[ i ];
773 action.toggle( action.hasMode( mode ) );
774 }
775
776 this.organized = false;
777 this.changing = false;
778 this.emit( 'change' );
779
780 return this;
781 };
782
783 /**
784 * Set the abilities of the specified actions.
785 *
786 * Action widgets that are configured with the specified actions will be enabled
787 * or disabled based on the boolean values specified in the `actions`
788 * parameter.
789 *
790 * @param {Object.<string,boolean>} actions A list keyed by action name with boolean
791 * values that indicate whether or not the action should be enabled.
792 * @chainable
793 */
794 OO.ui.ActionSet.prototype.setAbilities = function ( actions ) {
795 var i, len, action, item;
796
797 for ( i = 0, len = this.list.length; i < len; i++ ) {
798 item = this.list[ i ];
799 action = item.getAction();
800 if ( actions[ action ] !== undefined ) {
801 item.setDisabled( !actions[ action ] );
802 }
803 }
804
805 return this;
806 };
807
808 /**
809 * Executes a function once per action.
810 *
811 * When making changes to multiple actions, use this method instead of iterating over the actions
812 * manually to defer emitting a #change event until after all actions have been changed.
813 *
814 * @param {Object|null} actions Filters to use to determine which actions to iterate over; see #get
815 * @param {Function} callback Callback to run for each action; callback is invoked with three
816 * arguments: the action, the action's index, the list of actions being iterated over
817 * @chainable
818 */
819 OO.ui.ActionSet.prototype.forEach = function ( filter, callback ) {
820 this.changed = false;
821 this.changing = true;
822 this.get( filter ).forEach( callback );
823 this.changing = false;
824 if ( this.changed ) {
825 this.emit( 'change' );
826 }
827
828 return this;
829 };
830
831 /**
832 * Add action widgets to the action set.
833 *
834 * @param {OO.ui.ActionWidget[]} actions Action widgets to add
835 * @chainable
836 * @fires add
837 * @fires change
838 */
839 OO.ui.ActionSet.prototype.add = function ( actions ) {
840 var i, len, action;
841
842 this.changing = true;
843 for ( i = 0, len = actions.length; i < len; i++ ) {
844 action = actions[ i ];
845 action.connect( this, {
846 click: [ 'emit', 'click', action ],
847 resize: [ 'emit', 'resize', action ],
848 toggle: [ 'onActionChange' ]
849 } );
850 this.list.push( action );
851 }
852 this.organized = false;
853 this.emit( 'add', actions );
854 this.changing = false;
855 this.emit( 'change' );
856
857 return this;
858 };
859
860 /**
861 * Remove action widgets from the set.
862 *
863 * To remove all actions, you may wish to use the #clear method instead.
864 *
865 * @param {OO.ui.ActionWidget[]} actions Action widgets to remove
866 * @chainable
867 * @fires remove
868 * @fires change
869 */
870 OO.ui.ActionSet.prototype.remove = function ( actions ) {
871 var i, len, index, action;
872
873 this.changing = true;
874 for ( i = 0, len = actions.length; i < len; i++ ) {
875 action = actions[ i ];
876 index = this.list.indexOf( action );
877 if ( index !== -1 ) {
878 action.disconnect( this );
879 this.list.splice( index, 1 );
880 }
881 }
882 this.organized = false;
883 this.emit( 'remove', actions );
884 this.changing = false;
885 this.emit( 'change' );
886
887 return this;
888 };
889
890 /**
891 * Remove all action widets from the set.
892 *
893 * To remove only specified actions, use the {@link #method-remove remove} method instead.
894 *
895 * @chainable
896 * @fires remove
897 * @fires change
898 */
899 OO.ui.ActionSet.prototype.clear = function () {
900 var i, len, action,
901 removed = this.list.slice();
902
903 this.changing = true;
904 for ( i = 0, len = this.list.length; i < len; i++ ) {
905 action = this.list[ i ];
906 action.disconnect( this );
907 }
908
909 this.list = [];
910
911 this.organized = false;
912 this.emit( 'remove', removed );
913 this.changing = false;
914 this.emit( 'change' );
915
916 return this;
917 };
918
919 /**
920 * Organize actions.
921 *
922 * This is called whenever organized information is requested. It will only reorganize the actions
923 * if something has changed since the last time it ran.
924 *
925 * @private
926 * @chainable
927 */
928 OO.ui.ActionSet.prototype.organize = function () {
929 var i, iLen, j, jLen, flag, action, category, list, item, special,
930 specialFlags = this.constructor.static.specialFlags;
931
932 if ( !this.organized ) {
933 this.categorized = {};
934 this.special = {};
935 this.others = [];
936 for ( i = 0, iLen = this.list.length; i < iLen; i++ ) {
937 action = this.list[ i ];
938 if ( action.isVisible() ) {
939 // Populate categories
940 for ( category in this.categories ) {
941 if ( !this.categorized[ category ] ) {
942 this.categorized[ category ] = {};
943 }
944 list = action[ this.categories[ category ] ]();
945 if ( !Array.isArray( list ) ) {
946 list = [ list ];
947 }
948 for ( j = 0, jLen = list.length; j < jLen; j++ ) {
949 item = list[ j ];
950 if ( !this.categorized[ category ][ item ] ) {
951 this.categorized[ category ][ item ] = [];
952 }
953 this.categorized[ category ][ item ].push( action );
954 }
955 }
956 // Populate special/others
957 special = false;
958 for ( j = 0, jLen = specialFlags.length; j < jLen; j++ ) {
959 flag = specialFlags[ j ];
960 if ( !this.special[ flag ] && action.hasFlag( flag ) ) {
961 this.special[ flag ] = action;
962 special = true;
963 break;
964 }
965 }
966 if ( !special ) {
967 this.others.push( action );
968 }
969 }
970 }
971 this.organized = true;
972 }
973
974 return this;
975 };
976
977 /**
978 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
979 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
980 * connected to them and can't be interacted with.
981 *
982 * @abstract
983 * @class
984 *
985 * @constructor
986 * @param {Object} [config] Configuration options
987 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
988 * to the top level (e.g., the outermost div) of the element. See the [OOjs UI documentation on MediaWiki][2]
989 * for an example.
990 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#cssExample
991 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
992 * @cfg {string} [text] Text to insert
993 * @cfg {Array} [content] An array of content elements to append (after #text).
994 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
995 * Instances of OO.ui.Element will have their $element appended.
996 * @cfg {jQuery} [$content] Content elements to append (after #text)
997 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
998 * Data can also be specified with the #setData method.
999 */
1000 OO.ui.Element = function OoUiElement( config ) {
1001 // Configuration initialization
1002 config = config || {};
1003
1004 // Properties
1005 this.$ = $;
1006 this.visible = true;
1007 this.data = config.data;
1008 this.$element = config.$element ||
1009 $( document.createElement( this.getTagName() ) );
1010 this.elementGroup = null;
1011 this.debouncedUpdateThemeClassesHandler = this.debouncedUpdateThemeClasses.bind( this );
1012 this.updateThemeClassesPending = false;
1013
1014 // Initialization
1015 if ( Array.isArray( config.classes ) ) {
1016 this.$element.addClass( config.classes.join( ' ' ) );
1017 }
1018 if ( config.id ) {
1019 this.$element.attr( 'id', config.id );
1020 }
1021 if ( config.text ) {
1022 this.$element.text( config.text );
1023 }
1024 if ( config.content ) {
1025 // The `content` property treats plain strings as text; use an
1026 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
1027 // appropriate $element appended.
1028 this.$element.append( config.content.map( function ( v ) {
1029 if ( typeof v === 'string' ) {
1030 // Escape string so it is properly represented in HTML.
1031 return document.createTextNode( v );
1032 } else if ( v instanceof OO.ui.HtmlSnippet ) {
1033 // Bypass escaping.
1034 return v.toString();
1035 } else if ( v instanceof OO.ui.Element ) {
1036 return v.$element;
1037 }
1038 return v;
1039 } ) );
1040 }
1041 if ( config.$content ) {
1042 // The `$content` property treats plain strings as HTML.
1043 this.$element.append( config.$content );
1044 }
1045 };
1046
1047 /* Setup */
1048
1049 OO.initClass( OO.ui.Element );
1050
1051 /* Static Properties */
1052
1053 /**
1054 * The name of the HTML tag used by the element.
1055 *
1056 * The static value may be ignored if the #getTagName method is overridden.
1057 *
1058 * @static
1059 * @inheritable
1060 * @property {string}
1061 */
1062 OO.ui.Element.static.tagName = 'div';
1063
1064 /* Static Methods */
1065
1066 /**
1067 * Reconstitute a JavaScript object corresponding to a widget created
1068 * by the PHP implementation.
1069 *
1070 * @param {string|HTMLElement|jQuery} idOrNode
1071 * A DOM id (if a string) or node for the widget to infuse.
1072 * @return {OO.ui.Element}
1073 * The `OO.ui.Element` corresponding to this (infusable) document node.
1074 * For `Tag` objects emitted on the HTML side (used occasionally for content)
1075 * the value returned is a newly-created Element wrapping around the existing
1076 * DOM node.
1077 */
1078 OO.ui.Element.static.infuse = function ( idOrNode ) {
1079 var obj = OO.ui.Element.static.unsafeInfuse( idOrNode, true );
1080 // Verify that the type matches up.
1081 // FIXME: uncomment after T89721 is fixed (see T90929)
1082 /*
1083 if ( !( obj instanceof this['class'] ) ) {
1084 throw new Error( 'Infusion type mismatch!' );
1085 }
1086 */
1087 return obj;
1088 };
1089
1090 /**
1091 * Implementation helper for `infuse`; skips the type check and has an
1092 * extra property so that only the top-level invocation touches the DOM.
1093 * @private
1094 * @param {string|HTMLElement|jQuery} idOrNode
1095 * @param {boolean} top True only for top-level invocation.
1096 * @return {OO.ui.Element}
1097 */
1098 OO.ui.Element.static.unsafeInfuse = function ( idOrNode, top ) {
1099 // look for a cached result of a previous infusion.
1100 var id, $elem, data, cls, obj;
1101 if ( typeof idOrNode === 'string' ) {
1102 id = idOrNode;
1103 $elem = $( document.getElementById( id ) );
1104 } else {
1105 $elem = $( idOrNode );
1106 id = $elem.attr( 'id' );
1107 }
1108 data = $elem.data( 'ooui-infused' );
1109 if ( data ) {
1110 // cached!
1111 if ( data === true ) {
1112 throw new Error( 'Circular dependency! ' + id );
1113 }
1114 return data;
1115 }
1116 if ( !$elem.length ) {
1117 throw new Error( 'Widget not found: ' + id );
1118 }
1119 data = $elem.attr( 'data-ooui' );
1120 if ( !data ) {
1121 throw new Error( 'No infusion data found: ' + id );
1122 }
1123 try {
1124 data = $.parseJSON( data );
1125 } catch ( _ ) {
1126 data = null;
1127 }
1128 if ( !( data && data._ ) ) {
1129 throw new Error( 'No valid infusion data found: ' + id );
1130 }
1131 if ( data._ === 'Tag' ) {
1132 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
1133 return new OO.ui.Element( { $element: $elem } );
1134 }
1135 cls = OO.ui[data._];
1136 if ( !cls ) {
1137 throw new Error( 'Unknown widget type: ' + id );
1138 }
1139 $elem.data( 'ooui-infused', true ); // prevent loops
1140 data.id = id; // implicit
1141 data = OO.copy( data, null, function deserialize( value ) {
1142 if ( OO.isPlainObject( value ) ) {
1143 if ( value.tag ) {
1144 return OO.ui.Element.static.unsafeInfuse( value.tag, false );
1145 }
1146 if ( value.html ) {
1147 return new OO.ui.HtmlSnippet( value.html );
1148 }
1149 }
1150 } );
1151 // jscs:disable requireCapitalizedConstructors
1152 obj = new cls( data ); // rebuild widget
1153 // now replace old DOM with this new DOM.
1154 if ( top ) {
1155 $elem.replaceWith( obj.$element );
1156 }
1157 obj.$element.data( 'ooui-infused', obj );
1158 // set the 'data-ooui' attribute so we can identify infused widgets
1159 obj.$element.attr( 'data-ooui', '' );
1160 return obj;
1161 };
1162
1163 /**
1164 * Get a jQuery function within a specific document.
1165 *
1166 * @static
1167 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
1168 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
1169 * not in an iframe
1170 * @return {Function} Bound jQuery function
1171 */
1172 OO.ui.Element.static.getJQuery = function ( context, $iframe ) {
1173 function wrapper( selector ) {
1174 return $( selector, wrapper.context );
1175 }
1176
1177 wrapper.context = this.getDocument( context );
1178
1179 if ( $iframe ) {
1180 wrapper.$iframe = $iframe;
1181 }
1182
1183 return wrapper;
1184 };
1185
1186 /**
1187 * Get the document of an element.
1188 *
1189 * @static
1190 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
1191 * @return {HTMLDocument|null} Document object
1192 */
1193 OO.ui.Element.static.getDocument = function ( obj ) {
1194 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
1195 return ( obj[ 0 ] && obj[ 0 ].ownerDocument ) ||
1196 // Empty jQuery selections might have a context
1197 obj.context ||
1198 // HTMLElement
1199 obj.ownerDocument ||
1200 // Window
1201 obj.document ||
1202 // HTMLDocument
1203 ( obj.nodeType === 9 && obj ) ||
1204 null;
1205 };
1206
1207 /**
1208 * Get the window of an element or document.
1209 *
1210 * @static
1211 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
1212 * @return {Window} Window object
1213 */
1214 OO.ui.Element.static.getWindow = function ( obj ) {
1215 var doc = this.getDocument( obj );
1216 return doc.parentWindow || doc.defaultView;
1217 };
1218
1219 /**
1220 * Get the direction of an element or document.
1221 *
1222 * @static
1223 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
1224 * @return {string} Text direction, either 'ltr' or 'rtl'
1225 */
1226 OO.ui.Element.static.getDir = function ( obj ) {
1227 var isDoc, isWin;
1228
1229 if ( obj instanceof jQuery ) {
1230 obj = obj[ 0 ];
1231 }
1232 isDoc = obj.nodeType === 9;
1233 isWin = obj.document !== undefined;
1234 if ( isDoc || isWin ) {
1235 if ( isWin ) {
1236 obj = obj.document;
1237 }
1238 obj = obj.body;
1239 }
1240 return $( obj ).css( 'direction' );
1241 };
1242
1243 /**
1244 * Get the offset between two frames.
1245 *
1246 * TODO: Make this function not use recursion.
1247 *
1248 * @static
1249 * @param {Window} from Window of the child frame
1250 * @param {Window} [to=window] Window of the parent frame
1251 * @param {Object} [offset] Offset to start with, used internally
1252 * @return {Object} Offset object, containing left and top properties
1253 */
1254 OO.ui.Element.static.getFrameOffset = function ( from, to, offset ) {
1255 var i, len, frames, frame, rect;
1256
1257 if ( !to ) {
1258 to = window;
1259 }
1260 if ( !offset ) {
1261 offset = { top: 0, left: 0 };
1262 }
1263 if ( from.parent === from ) {
1264 return offset;
1265 }
1266
1267 // Get iframe element
1268 frames = from.parent.document.getElementsByTagName( 'iframe' );
1269 for ( i = 0, len = frames.length; i < len; i++ ) {
1270 if ( frames[ i ].contentWindow === from ) {
1271 frame = frames[ i ];
1272 break;
1273 }
1274 }
1275
1276 // Recursively accumulate offset values
1277 if ( frame ) {
1278 rect = frame.getBoundingClientRect();
1279 offset.left += rect.left;
1280 offset.top += rect.top;
1281 if ( from !== to ) {
1282 this.getFrameOffset( from.parent, offset );
1283 }
1284 }
1285 return offset;
1286 };
1287
1288 /**
1289 * Get the offset between two elements.
1290 *
1291 * The two elements may be in a different frame, but in that case the frame $element is in must
1292 * be contained in the frame $anchor is in.
1293 *
1294 * @static
1295 * @param {jQuery} $element Element whose position to get
1296 * @param {jQuery} $anchor Element to get $element's position relative to
1297 * @return {Object} Translated position coordinates, containing top and left properties
1298 */
1299 OO.ui.Element.static.getRelativePosition = function ( $element, $anchor ) {
1300 var iframe, iframePos,
1301 pos = $element.offset(),
1302 anchorPos = $anchor.offset(),
1303 elementDocument = this.getDocument( $element ),
1304 anchorDocument = this.getDocument( $anchor );
1305
1306 // If $element isn't in the same document as $anchor, traverse up
1307 while ( elementDocument !== anchorDocument ) {
1308 iframe = elementDocument.defaultView.frameElement;
1309 if ( !iframe ) {
1310 throw new Error( '$element frame is not contained in $anchor frame' );
1311 }
1312 iframePos = $( iframe ).offset();
1313 pos.left += iframePos.left;
1314 pos.top += iframePos.top;
1315 elementDocument = iframe.ownerDocument;
1316 }
1317 pos.left -= anchorPos.left;
1318 pos.top -= anchorPos.top;
1319 return pos;
1320 };
1321
1322 /**
1323 * Get element border sizes.
1324 *
1325 * @static
1326 * @param {HTMLElement} el Element to measure
1327 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1328 */
1329 OO.ui.Element.static.getBorders = function ( el ) {
1330 var doc = el.ownerDocument,
1331 win = doc.parentWindow || doc.defaultView,
1332 style = win && win.getComputedStyle ?
1333 win.getComputedStyle( el, null ) :
1334 el.currentStyle,
1335 $el = $( el ),
1336 top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0,
1337 left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0,
1338 bottom = parseFloat( style ? style.borderBottomWidth : $el.css( 'borderBottomWidth' ) ) || 0,
1339 right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0;
1340
1341 return {
1342 top: top,
1343 left: left,
1344 bottom: bottom,
1345 right: right
1346 };
1347 };
1348
1349 /**
1350 * Get dimensions of an element or window.
1351 *
1352 * @static
1353 * @param {HTMLElement|Window} el Element to measure
1354 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1355 */
1356 OO.ui.Element.static.getDimensions = function ( el ) {
1357 var $el, $win,
1358 doc = el.ownerDocument || el.document,
1359 win = doc.parentWindow || doc.defaultView;
1360
1361 if ( win === el || el === doc.documentElement ) {
1362 $win = $( win );
1363 return {
1364 borders: { top: 0, left: 0, bottom: 0, right: 0 },
1365 scroll: {
1366 top: $win.scrollTop(),
1367 left: $win.scrollLeft()
1368 },
1369 scrollbar: { right: 0, bottom: 0 },
1370 rect: {
1371 top: 0,
1372 left: 0,
1373 bottom: $win.innerHeight(),
1374 right: $win.innerWidth()
1375 }
1376 };
1377 } else {
1378 $el = $( el );
1379 return {
1380 borders: this.getBorders( el ),
1381 scroll: {
1382 top: $el.scrollTop(),
1383 left: $el.scrollLeft()
1384 },
1385 scrollbar: {
1386 right: $el.innerWidth() - el.clientWidth,
1387 bottom: $el.innerHeight() - el.clientHeight
1388 },
1389 rect: el.getBoundingClientRect()
1390 };
1391 }
1392 };
1393
1394 /**
1395 * Get scrollable object parent
1396 *
1397 * documentElement can't be used to get or set the scrollTop
1398 * property on Blink. Changing and testing its value lets us
1399 * use 'body' or 'documentElement' based on what is working.
1400 *
1401 * https://code.google.com/p/chromium/issues/detail?id=303131
1402 *
1403 * @static
1404 * @param {HTMLElement} el Element to find scrollable parent for
1405 * @return {HTMLElement} Scrollable parent
1406 */
1407 OO.ui.Element.static.getRootScrollableElement = function ( el ) {
1408 var scrollTop, body;
1409
1410 if ( OO.ui.scrollableElement === undefined ) {
1411 body = el.ownerDocument.body;
1412 scrollTop = body.scrollTop;
1413 body.scrollTop = 1;
1414
1415 if ( body.scrollTop === 1 ) {
1416 body.scrollTop = scrollTop;
1417 OO.ui.scrollableElement = 'body';
1418 } else {
1419 OO.ui.scrollableElement = 'documentElement';
1420 }
1421 }
1422
1423 return el.ownerDocument[ OO.ui.scrollableElement ];
1424 };
1425
1426 /**
1427 * Get closest scrollable container.
1428 *
1429 * Traverses up until either a scrollable element or the root is reached, in which case the window
1430 * will be returned.
1431 *
1432 * @static
1433 * @param {HTMLElement} el Element to find scrollable container for
1434 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1435 * @return {HTMLElement} Closest scrollable container
1436 */
1437 OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) {
1438 var i, val,
1439 // props = [ 'overflow' ] doesn't work due to https://bugzilla.mozilla.org/show_bug.cgi?id=889091
1440 props = [ 'overflow-x', 'overflow-y' ],
1441 $parent = $( el ).parent();
1442
1443 if ( dimension === 'x' || dimension === 'y' ) {
1444 props = [ 'overflow-' + dimension ];
1445 }
1446
1447 while ( $parent.length ) {
1448 if ( $parent[ 0 ] === this.getRootScrollableElement( el ) ) {
1449 return $parent[ 0 ];
1450 }
1451 i = props.length;
1452 while ( i-- ) {
1453 val = $parent.css( props[ i ] );
1454 if ( val === 'auto' || val === 'scroll' ) {
1455 return $parent[ 0 ];
1456 }
1457 }
1458 $parent = $parent.parent();
1459 }
1460 return this.getDocument( el ).body;
1461 };
1462
1463 /**
1464 * Scroll element into view.
1465 *
1466 * @static
1467 * @param {HTMLElement} el Element to scroll into view
1468 * @param {Object} [config] Configuration options
1469 * @param {string} [config.duration] jQuery animation duration value
1470 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1471 * to scroll in both directions
1472 * @param {Function} [config.complete] Function to call when scrolling completes
1473 */
1474 OO.ui.Element.static.scrollIntoView = function ( el, config ) {
1475 // Configuration initialization
1476 config = config || {};
1477
1478 var rel, anim = {},
1479 callback = typeof config.complete === 'function' && config.complete,
1480 sc = this.getClosestScrollableContainer( el, config.direction ),
1481 $sc = $( sc ),
1482 eld = this.getDimensions( el ),
1483 scd = this.getDimensions( sc ),
1484 $win = $( this.getWindow( el ) );
1485
1486 // Compute the distances between the edges of el and the edges of the scroll viewport
1487 if ( $sc.is( 'html, body' ) ) {
1488 // If the scrollable container is the root, this is easy
1489 rel = {
1490 top: eld.rect.top,
1491 bottom: $win.innerHeight() - eld.rect.bottom,
1492 left: eld.rect.left,
1493 right: $win.innerWidth() - eld.rect.right
1494 };
1495 } else {
1496 // Otherwise, we have to subtract el's coordinates from sc's coordinates
1497 rel = {
1498 top: eld.rect.top - ( scd.rect.top + scd.borders.top ),
1499 bottom: scd.rect.bottom - scd.borders.bottom - scd.scrollbar.bottom - eld.rect.bottom,
1500 left: eld.rect.left - ( scd.rect.left + scd.borders.left ),
1501 right: scd.rect.right - scd.borders.right - scd.scrollbar.right - eld.rect.right
1502 };
1503 }
1504
1505 if ( !config.direction || config.direction === 'y' ) {
1506 if ( rel.top < 0 ) {
1507 anim.scrollTop = scd.scroll.top + rel.top;
1508 } else if ( rel.top > 0 && rel.bottom < 0 ) {
1509 anim.scrollTop = scd.scroll.top + Math.min( rel.top, -rel.bottom );
1510 }
1511 }
1512 if ( !config.direction || config.direction === 'x' ) {
1513 if ( rel.left < 0 ) {
1514 anim.scrollLeft = scd.scroll.left + rel.left;
1515 } else if ( rel.left > 0 && rel.right < 0 ) {
1516 anim.scrollLeft = scd.scroll.left + Math.min( rel.left, -rel.right );
1517 }
1518 }
1519 if ( !$.isEmptyObject( anim ) ) {
1520 $sc.stop( true ).animate( anim, config.duration || 'fast' );
1521 if ( callback ) {
1522 $sc.queue( function ( next ) {
1523 callback();
1524 next();
1525 } );
1526 }
1527 } else {
1528 if ( callback ) {
1529 callback();
1530 }
1531 }
1532 };
1533
1534 /**
1535 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1536 * and reserve space for them, because it probably doesn't.
1537 *
1538 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1539 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1540 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1541 * and then reattach (or show) them back.
1542 *
1543 * @static
1544 * @param {HTMLElement} el Element to reconsider the scrollbars on
1545 */
1546 OO.ui.Element.static.reconsiderScrollbars = function ( el ) {
1547 var i, len, scrollLeft, scrollTop, nodes = [];
1548 // Save scroll position
1549 scrollLeft = el.scrollLeft;
1550 scrollTop = el.scrollTop;
1551 // Detach all children
1552 while ( el.firstChild ) {
1553 nodes.push( el.firstChild );
1554 el.removeChild( el.firstChild );
1555 }
1556 // Force reflow
1557 void el.offsetHeight;
1558 // Reattach all children
1559 for ( i = 0, len = nodes.length; i < len; i++ ) {
1560 el.appendChild( nodes[ i ] );
1561 }
1562 // Restore scroll position (no-op if scrollbars disappeared)
1563 el.scrollLeft = scrollLeft;
1564 el.scrollTop = scrollTop;
1565 };
1566
1567 /* Methods */
1568
1569 /**
1570 * Toggle visibility of an element.
1571 *
1572 * @param {boolean} [show] Make element visible, omit to toggle visibility
1573 * @fires visible
1574 * @chainable
1575 */
1576 OO.ui.Element.prototype.toggle = function ( show ) {
1577 show = show === undefined ? !this.visible : !!show;
1578
1579 if ( show !== this.isVisible() ) {
1580 this.visible = show;
1581 this.$element.toggleClass( 'oo-ui-element-hidden', !this.visible );
1582 this.emit( 'toggle', show );
1583 }
1584
1585 return this;
1586 };
1587
1588 /**
1589 * Check if element is visible.
1590 *
1591 * @return {boolean} element is visible
1592 */
1593 OO.ui.Element.prototype.isVisible = function () {
1594 return this.visible;
1595 };
1596
1597 /**
1598 * Get element data.
1599 *
1600 * @return {Mixed} Element data
1601 */
1602 OO.ui.Element.prototype.getData = function () {
1603 return this.data;
1604 };
1605
1606 /**
1607 * Set element data.
1608 *
1609 * @param {Mixed} Element data
1610 * @chainable
1611 */
1612 OO.ui.Element.prototype.setData = function ( data ) {
1613 this.data = data;
1614 return this;
1615 };
1616
1617 /**
1618 * Check if element supports one or more methods.
1619 *
1620 * @param {string|string[]} methods Method or list of methods to check
1621 * @return {boolean} All methods are supported
1622 */
1623 OO.ui.Element.prototype.supports = function ( methods ) {
1624 var i, len,
1625 support = 0;
1626
1627 methods = Array.isArray( methods ) ? methods : [ methods ];
1628 for ( i = 0, len = methods.length; i < len; i++ ) {
1629 if ( $.isFunction( this[ methods[ i ] ] ) ) {
1630 support++;
1631 }
1632 }
1633
1634 return methods.length === support;
1635 };
1636
1637 /**
1638 * Update the theme-provided classes.
1639 *
1640 * @localdoc This is called in element mixins and widget classes any time state changes.
1641 * Updating is debounced, minimizing overhead of changing multiple attributes and
1642 * guaranteeing that theme updates do not occur within an element's constructor
1643 */
1644 OO.ui.Element.prototype.updateThemeClasses = function () {
1645 if ( !this.updateThemeClassesPending ) {
1646 this.updateThemeClassesPending = true;
1647 setTimeout( this.debouncedUpdateThemeClassesHandler );
1648 }
1649 };
1650
1651 /**
1652 * @private
1653 */
1654 OO.ui.Element.prototype.debouncedUpdateThemeClasses = function () {
1655 OO.ui.theme.updateElementClasses( this );
1656 this.updateThemeClassesPending = false;
1657 };
1658
1659 /**
1660 * Get the HTML tag name.
1661 *
1662 * Override this method to base the result on instance information.
1663 *
1664 * @return {string} HTML tag name
1665 */
1666 OO.ui.Element.prototype.getTagName = function () {
1667 return this.constructor.static.tagName;
1668 };
1669
1670 /**
1671 * Check if the element is attached to the DOM
1672 * @return {boolean} The element is attached to the DOM
1673 */
1674 OO.ui.Element.prototype.isElementAttached = function () {
1675 return $.contains( this.getElementDocument(), this.$element[ 0 ] );
1676 };
1677
1678 /**
1679 * Get the DOM document.
1680 *
1681 * @return {HTMLDocument} Document object
1682 */
1683 OO.ui.Element.prototype.getElementDocument = function () {
1684 // Don't cache this in other ways either because subclasses could can change this.$element
1685 return OO.ui.Element.static.getDocument( this.$element );
1686 };
1687
1688 /**
1689 * Get the DOM window.
1690 *
1691 * @return {Window} Window object
1692 */
1693 OO.ui.Element.prototype.getElementWindow = function () {
1694 return OO.ui.Element.static.getWindow( this.$element );
1695 };
1696
1697 /**
1698 * Get closest scrollable container.
1699 */
1700 OO.ui.Element.prototype.getClosestScrollableElementContainer = function () {
1701 return OO.ui.Element.static.getClosestScrollableContainer( this.$element[ 0 ] );
1702 };
1703
1704 /**
1705 * Get group element is in.
1706 *
1707 * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1708 */
1709 OO.ui.Element.prototype.getElementGroup = function () {
1710 return this.elementGroup;
1711 };
1712
1713 /**
1714 * Set group element is in.
1715 *
1716 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1717 * @chainable
1718 */
1719 OO.ui.Element.prototype.setElementGroup = function ( group ) {
1720 this.elementGroup = group;
1721 return this;
1722 };
1723
1724 /**
1725 * Scroll element into view.
1726 *
1727 * @param {Object} [config] Configuration options
1728 */
1729 OO.ui.Element.prototype.scrollElementIntoView = function ( config ) {
1730 return OO.ui.Element.static.scrollIntoView( this.$element[ 0 ], config );
1731 };
1732
1733 /**
1734 * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way
1735 * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined.
1736 * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout},
1737 * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1738 * and {@link OO.ui.BookletLayout BookletLayout} for more information and examples.
1739 *
1740 * @abstract
1741 * @class
1742 * @extends OO.ui.Element
1743 * @mixins OO.EventEmitter
1744 *
1745 * @constructor
1746 * @param {Object} [config] Configuration options
1747 */
1748 OO.ui.Layout = function OoUiLayout( config ) {
1749 // Configuration initialization
1750 config = config || {};
1751
1752 // Parent constructor
1753 OO.ui.Layout.parent.call( this, config );
1754
1755 // Mixin constructors
1756 OO.EventEmitter.call( this );
1757
1758 // Initialization
1759 this.$element.addClass( 'oo-ui-layout' );
1760 };
1761
1762 /* Setup */
1763
1764 OO.inheritClass( OO.ui.Layout, OO.ui.Element );
1765 OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
1766
1767 /**
1768 * Widgets are compositions of one or more OOjs UI elements that users can both view
1769 * and interact with. All widgets can be configured and modified via a standard API,
1770 * and their state can change dynamically according to a model.
1771 *
1772 * @abstract
1773 * @class
1774 * @extends OO.ui.Element
1775 * @mixins OO.EventEmitter
1776 *
1777 * @constructor
1778 * @param {Object} [config] Configuration options
1779 * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1780 * appearance reflects this state.
1781 */
1782 OO.ui.Widget = function OoUiWidget( config ) {
1783 // Initialize config
1784 config = $.extend( { disabled: false }, config );
1785
1786 // Parent constructor
1787 OO.ui.Widget.parent.call( this, config );
1788
1789 // Mixin constructors
1790 OO.EventEmitter.call( this );
1791
1792 // Properties
1793 this.disabled = null;
1794 this.wasDisabled = null;
1795
1796 // Initialization
1797 this.$element.addClass( 'oo-ui-widget' );
1798 this.setDisabled( !!config.disabled );
1799 };
1800
1801 /* Setup */
1802
1803 OO.inheritClass( OO.ui.Widget, OO.ui.Element );
1804 OO.mixinClass( OO.ui.Widget, OO.EventEmitter );
1805
1806 /* Static Properties */
1807
1808 /**
1809 * Whether this widget will behave reasonably when wrapped in a HTML `<label>`. If this is true,
1810 * wrappers such as OO.ui.FieldLayout may use a `<label>` instead of implementing own label click
1811 * handling.
1812 *
1813 * @static
1814 * @inheritable
1815 * @property {boolean}
1816 */
1817 OO.ui.Widget.static.supportsSimpleLabel = false;
1818
1819 /* Events */
1820
1821 /**
1822 * @event disable
1823 *
1824 * A 'disable' event is emitted when a widget is disabled.
1825 *
1826 * @param {boolean} disabled Widget is disabled
1827 */
1828
1829 /**
1830 * @event toggle
1831 *
1832 * A 'toggle' event is emitted when the visibility of the widget changes.
1833 *
1834 * @param {boolean} visible Widget is visible
1835 */
1836
1837 /* Methods */
1838
1839 /**
1840 * Check if the widget is disabled.
1841 *
1842 * @return {boolean} Widget is disabled
1843 */
1844 OO.ui.Widget.prototype.isDisabled = function () {
1845 return this.disabled;
1846 };
1847
1848 /**
1849 * Set the 'disabled' state of the widget.
1850 *
1851 * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1852 *
1853 * @param {boolean} disabled Disable widget
1854 * @chainable
1855 */
1856 OO.ui.Widget.prototype.setDisabled = function ( disabled ) {
1857 var isDisabled;
1858
1859 this.disabled = !!disabled;
1860 isDisabled = this.isDisabled();
1861 if ( isDisabled !== this.wasDisabled ) {
1862 this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled );
1863 this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled );
1864 this.$element.attr( 'aria-disabled', isDisabled.toString() );
1865 this.emit( 'disable', isDisabled );
1866 this.updateThemeClasses();
1867 }
1868 this.wasDisabled = isDisabled;
1869
1870 return this;
1871 };
1872
1873 /**
1874 * Update the disabled state, in case of changes in parent widget.
1875 *
1876 * @chainable
1877 */
1878 OO.ui.Widget.prototype.updateDisabled = function () {
1879 this.setDisabled( this.disabled );
1880 return this;
1881 };
1882
1883 /**
1884 * A window is a container for elements that are in a child frame. They are used with
1885 * a window manager (OO.ui.WindowManager), which is used to open and close the window and control
1886 * its presentation. The size of a window is specified using a symbolic name (e.g., ‘small’, ‘medium’,
1887 * ‘large’), which is interpreted by the window manager. If the requested size is not recognized,
1888 * the window manager will choose a sensible fallback.
1889 *
1890 * The lifecycle of a window has three primary stages (opening, opened, and closing) in which
1891 * different processes are executed:
1892 *
1893 * **opening**: The opening stage begins when the window manager's {@link OO.ui.WindowManager#openWindow
1894 * openWindow} or the window's {@link #open open} methods are used, and the window manager begins to open
1895 * the window.
1896 *
1897 * - {@link #getSetupProcess} method is called and its result executed
1898 * - {@link #getReadyProcess} method is called and its result executed
1899 *
1900 * **opened**: The window is now open
1901 *
1902 * **closing**: The closing stage begins when the window manager's
1903 * {@link OO.ui.WindowManager#closeWindow closeWindow}
1904 * or the window's {@link #close} methods are used, and the window manager begins to close the window.
1905 *
1906 * - {@link #getHoldProcess} method is called and its result executed
1907 * - {@link #getTeardownProcess} method is called and its result executed. The window is now closed
1908 *
1909 * Each of the window's processes (setup, ready, hold, and teardown) can be extended in subclasses
1910 * by overriding the window's #getSetupProcess, #getReadyProcess, #getHoldProcess and #getTeardownProcess
1911 * methods. Note that each {@link OO.ui.Process process} is executed in series, so asynchronous
1912 * processing can complete. Always assume window processes are executed asynchronously.
1913 *
1914 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
1915 *
1916 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows
1917 *
1918 * @abstract
1919 * @class
1920 * @extends OO.ui.Element
1921 * @mixins OO.EventEmitter
1922 *
1923 * @constructor
1924 * @param {Object} [config] Configuration options
1925 * @cfg {string} [size] Symbolic name of the dialog size: `small`, `medium`, `large`, `larger` or
1926 * `full`. If omitted, the value of the {@link #static-size static size} property will be used.
1927 */
1928 OO.ui.Window = function OoUiWindow( config ) {
1929 // Configuration initialization
1930 config = config || {};
1931
1932 // Parent constructor
1933 OO.ui.Window.parent.call( this, config );
1934
1935 // Mixin constructors
1936 OO.EventEmitter.call( this );
1937
1938 // Properties
1939 this.manager = null;
1940 this.size = config.size || this.constructor.static.size;
1941 this.$frame = $( '<div>' );
1942 this.$overlay = $( '<div>' );
1943 this.$content = $( '<div>' );
1944
1945 // Initialization
1946 this.$overlay.addClass( 'oo-ui-window-overlay' );
1947 this.$content
1948 .addClass( 'oo-ui-window-content' )
1949 .attr( 'tabindex', 0 );
1950 this.$frame
1951 .addClass( 'oo-ui-window-frame' )
1952 .append( this.$content );
1953
1954 this.$element
1955 .addClass( 'oo-ui-window' )
1956 .append( this.$frame, this.$overlay );
1957
1958 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
1959 // that reference properties not initialized at that time of parent class construction
1960 // TODO: Find a better way to handle post-constructor setup
1961 this.visible = false;
1962 this.$element.addClass( 'oo-ui-element-hidden' );
1963 };
1964
1965 /* Setup */
1966
1967 OO.inheritClass( OO.ui.Window, OO.ui.Element );
1968 OO.mixinClass( OO.ui.Window, OO.EventEmitter );
1969
1970 /* Static Properties */
1971
1972 /**
1973 * Symbolic name of the window size: `small`, `medium`, `large`, `larger` or `full`.
1974 *
1975 * The static size is used if no #size is configured during construction.
1976 *
1977 * @static
1978 * @inheritable
1979 * @property {string}
1980 */
1981 OO.ui.Window.static.size = 'medium';
1982
1983 /* Methods */
1984
1985 /**
1986 * Handle mouse down events.
1987 *
1988 * @private
1989 * @param {jQuery.Event} e Mouse down event
1990 */
1991 OO.ui.Window.prototype.onMouseDown = function ( e ) {
1992 // Prevent clicking on the click-block from stealing focus
1993 if ( e.target === this.$element[ 0 ] ) {
1994 return false;
1995 }
1996 };
1997
1998 /**
1999 * Check if the window has been initialized.
2000 *
2001 * Initialization occurs when a window is added to a manager.
2002 *
2003 * @return {boolean} Window has been initialized
2004 */
2005 OO.ui.Window.prototype.isInitialized = function () {
2006 return !!this.manager;
2007 };
2008
2009 /**
2010 * Check if the window is visible.
2011 *
2012 * @return {boolean} Window is visible
2013 */
2014 OO.ui.Window.prototype.isVisible = function () {
2015 return this.visible;
2016 };
2017
2018 /**
2019 * Check if the window is opening.
2020 *
2021 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpening isOpening}
2022 * method.
2023 *
2024 * @return {boolean} Window is opening
2025 */
2026 OO.ui.Window.prototype.isOpening = function () {
2027 return this.manager.isOpening( this );
2028 };
2029
2030 /**
2031 * Check if the window is closing.
2032 *
2033 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isClosing isClosing} method.
2034 *
2035 * @return {boolean} Window is closing
2036 */
2037 OO.ui.Window.prototype.isClosing = function () {
2038 return this.manager.isClosing( this );
2039 };
2040
2041 /**
2042 * Check if the window is opened.
2043 *
2044 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpened isOpened} method.
2045 *
2046 * @return {boolean} Window is opened
2047 */
2048 OO.ui.Window.prototype.isOpened = function () {
2049 return this.manager.isOpened( this );
2050 };
2051
2052 /**
2053 * Get the window manager.
2054 *
2055 * All windows must be attached to a window manager, which is used to open
2056 * and close the window and control its presentation.
2057 *
2058 * @return {OO.ui.WindowManager} Manager of window
2059 */
2060 OO.ui.Window.prototype.getManager = function () {
2061 return this.manager;
2062 };
2063
2064 /**
2065 * Get the symbolic name of the window size (e.g., `small` or `medium`).
2066 *
2067 * @return {string} Symbolic name of the size: `small`, `medium`, `large`, `larger`, `full`
2068 */
2069 OO.ui.Window.prototype.getSize = function () {
2070 return this.size;
2071 };
2072
2073 /**
2074 * Disable transitions on window's frame for the duration of the callback function, then enable them
2075 * back.
2076 *
2077 * @private
2078 * @param {Function} callback Function to call while transitions are disabled
2079 */
2080 OO.ui.Window.prototype.withoutSizeTransitions = function ( callback ) {
2081 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
2082 // Disable transitions first, otherwise we'll get values from when the window was animating.
2083 var oldTransition,
2084 styleObj = this.$frame[ 0 ].style;
2085 oldTransition = styleObj.transition || styleObj.OTransition || styleObj.MsTransition ||
2086 styleObj.MozTransition || styleObj.WebkitTransition;
2087 styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
2088 styleObj.MozTransition = styleObj.WebkitTransition = 'none';
2089 callback();
2090 // Force reflow to make sure the style changes done inside callback really are not transitioned
2091 this.$frame.height();
2092 styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
2093 styleObj.MozTransition = styleObj.WebkitTransition = oldTransition;
2094 };
2095
2096 /**
2097 * Get the height of the full window contents (i.e., the window head, body and foot together).
2098 *
2099 * What consistitutes the head, body, and foot varies depending on the window type.
2100 * A {@link OO.ui.MessageDialog message dialog} displays a title and message in its body,
2101 * and any actions in the foot. A {@link OO.ui.ProcessDialog process dialog} displays a title
2102 * and special actions in the head, and dialog content in the body.
2103 *
2104 * To get just the height of the dialog body, use the #getBodyHeight method.
2105 *
2106 * @return {number} The height of the window contents (the dialog head, body and foot) in pixels
2107 */
2108 OO.ui.Window.prototype.getContentHeight = function () {
2109 var bodyHeight,
2110 win = this,
2111 bodyStyleObj = this.$body[ 0 ].style,
2112 frameStyleObj = this.$frame[ 0 ].style;
2113
2114 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
2115 // Disable transitions first, otherwise we'll get values from when the window was animating.
2116 this.withoutSizeTransitions( function () {
2117 var oldHeight = frameStyleObj.height,
2118 oldPosition = bodyStyleObj.position;
2119 frameStyleObj.height = '1px';
2120 // Force body to resize to new width
2121 bodyStyleObj.position = 'relative';
2122 bodyHeight = win.getBodyHeight();
2123 frameStyleObj.height = oldHeight;
2124 bodyStyleObj.position = oldPosition;
2125 } );
2126
2127 return (
2128 // Add buffer for border
2129 ( this.$frame.outerHeight() - this.$frame.innerHeight() ) +
2130 // Use combined heights of children
2131 ( this.$head.outerHeight( true ) + bodyHeight + this.$foot.outerHeight( true ) )
2132 );
2133 };
2134
2135 /**
2136 * Get the height of the window body.
2137 *
2138 * To get the height of the full window contents (the window body, head, and foot together),
2139 * use #getContentHeight.
2140 *
2141 * When this function is called, the window will temporarily have been resized
2142 * to height=1px, so .scrollHeight measurements can be taken accurately.
2143 *
2144 * @return {number} Height of the window body in pixels
2145 */
2146 OO.ui.Window.prototype.getBodyHeight = function () {
2147 return this.$body[ 0 ].scrollHeight;
2148 };
2149
2150 /**
2151 * Get the directionality of the frame (right-to-left or left-to-right).
2152 *
2153 * @return {string} Directionality: `'ltr'` or `'rtl'`
2154 */
2155 OO.ui.Window.prototype.getDir = function () {
2156 return this.dir;
2157 };
2158
2159 /**
2160 * Get the 'setup' process.
2161 *
2162 * The setup process is used to set up a window for use in a particular context,
2163 * based on the `data` argument. This method is called during the opening phase of the window’s
2164 * lifecycle.
2165 *
2166 * Override this method to add additional steps to the ‘setup’ process the parent method provides
2167 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2168 * of OO.ui.Process.
2169 *
2170 * To add window content that persists between openings, you may wish to use the #initialize method
2171 * instead.
2172 *
2173 * @abstract
2174 * @param {Object} [data] Window opening data
2175 * @return {OO.ui.Process} Setup process
2176 */
2177 OO.ui.Window.prototype.getSetupProcess = function () {
2178 return new OO.ui.Process();
2179 };
2180
2181 /**
2182 * Get the ‘ready’ process.
2183 *
2184 * The ready process is used to ready a window for use in a particular
2185 * context, based on the `data` argument. This method is called during the opening phase of
2186 * the window’s lifecycle, after the window has been {@link #getSetupProcess setup}.
2187 *
2188 * Override this method to add additional steps to the ‘ready’ process the parent method
2189 * provides using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next}
2190 * methods of OO.ui.Process.
2191 *
2192 * @abstract
2193 * @param {Object} [data] Window opening data
2194 * @return {OO.ui.Process} Ready process
2195 */
2196 OO.ui.Window.prototype.getReadyProcess = function () {
2197 return new OO.ui.Process();
2198 };
2199
2200 /**
2201 * Get the 'hold' process.
2202 *
2203 * The hold proccess is used to keep a window from being used in a particular context,
2204 * based on the `data` argument. This method is called during the closing phase of the window’s
2205 * lifecycle.
2206 *
2207 * Override this method to add additional steps to the 'hold' process the parent method provides
2208 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2209 * of OO.ui.Process.
2210 *
2211 * @abstract
2212 * @param {Object} [data] Window closing data
2213 * @return {OO.ui.Process} Hold process
2214 */
2215 OO.ui.Window.prototype.getHoldProcess = function () {
2216 return new OO.ui.Process();
2217 };
2218
2219 /**
2220 * Get the ‘teardown’ process.
2221 *
2222 * The teardown process is used to teardown a window after use. During teardown,
2223 * user interactions within the window are conveyed and the window is closed, based on the `data`
2224 * argument. This method is called during the closing phase of the window’s lifecycle.
2225 *
2226 * Override this method to add additional steps to the ‘teardown’ process the parent method provides
2227 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2228 * of OO.ui.Process.
2229 *
2230 * @abstract
2231 * @param {Object} [data] Window closing data
2232 * @return {OO.ui.Process} Teardown process
2233 */
2234 OO.ui.Window.prototype.getTeardownProcess = function () {
2235 return new OO.ui.Process();
2236 };
2237
2238 /**
2239 * Set the window manager.
2240 *
2241 * This will cause the window to initialize. Calling it more than once will cause an error.
2242 *
2243 * @param {OO.ui.WindowManager} manager Manager for this window
2244 * @throws {Error} An error is thrown if the method is called more than once
2245 * @chainable
2246 */
2247 OO.ui.Window.prototype.setManager = function ( manager ) {
2248 if ( this.manager ) {
2249 throw new Error( 'Cannot set window manager, window already has a manager' );
2250 }
2251
2252 this.manager = manager;
2253 this.initialize();
2254
2255 return this;
2256 };
2257
2258 /**
2259 * Set the window size by symbolic name (e.g., 'small' or 'medium')
2260 *
2261 * @param {string} size Symbolic name of size: `small`, `medium`, `large`, `larger` or
2262 * `full`
2263 * @chainable
2264 */
2265 OO.ui.Window.prototype.setSize = function ( size ) {
2266 this.size = size;
2267 this.updateSize();
2268 return this;
2269 };
2270
2271 /**
2272 * Update the window size.
2273 *
2274 * @throws {Error} An error is thrown if the window is not attached to a window manager
2275 * @chainable
2276 */
2277 OO.ui.Window.prototype.updateSize = function () {
2278 if ( !this.manager ) {
2279 throw new Error( 'Cannot update window size, must be attached to a manager' );
2280 }
2281
2282 this.manager.updateWindowSize( this );
2283
2284 return this;
2285 };
2286
2287 /**
2288 * Set window dimensions. This method is called by the {@link OO.ui.WindowManager window manager}
2289 * when the window is opening. In general, setDimensions should not be called directly.
2290 *
2291 * To set the size of the window, use the #setSize method.
2292 *
2293 * @param {Object} dim CSS dimension properties
2294 * @param {string|number} [dim.width] Width
2295 * @param {string|number} [dim.minWidth] Minimum width
2296 * @param {string|number} [dim.maxWidth] Maximum width
2297 * @param {string|number} [dim.width] Height, omit to set based on height of contents
2298 * @param {string|number} [dim.minWidth] Minimum height
2299 * @param {string|number} [dim.maxWidth] Maximum height
2300 * @chainable
2301 */
2302 OO.ui.Window.prototype.setDimensions = function ( dim ) {
2303 var height,
2304 win = this,
2305 styleObj = this.$frame[ 0 ].style;
2306
2307 // Calculate the height we need to set using the correct width
2308 if ( dim.height === undefined ) {
2309 this.withoutSizeTransitions( function () {
2310 var oldWidth = styleObj.width;
2311 win.$frame.css( 'width', dim.width || '' );
2312 height = win.getContentHeight();
2313 styleObj.width = oldWidth;
2314 } );
2315 } else {
2316 height = dim.height;
2317 }
2318
2319 this.$frame.css( {
2320 width: dim.width || '',
2321 minWidth: dim.minWidth || '',
2322 maxWidth: dim.maxWidth || '',
2323 height: height || '',
2324 minHeight: dim.minHeight || '',
2325 maxHeight: dim.maxHeight || ''
2326 } );
2327
2328 return this;
2329 };
2330
2331 /**
2332 * Initialize window contents.
2333 *
2334 * Before the window is opened for the first time, #initialize is called so that content that
2335 * persists between openings can be added to the window.
2336 *
2337 * To set up a window with new content each time the window opens, use #getSetupProcess.
2338 *
2339 * @throws {Error} An error is thrown if the window is not attached to a window manager
2340 * @chainable
2341 */
2342 OO.ui.Window.prototype.initialize = function () {
2343 if ( !this.manager ) {
2344 throw new Error( 'Cannot initialize window, must be attached to a manager' );
2345 }
2346
2347 // Properties
2348 this.$head = $( '<div>' );
2349 this.$body = $( '<div>' );
2350 this.$foot = $( '<div>' );
2351 this.dir = OO.ui.Element.static.getDir( this.$content ) || 'ltr';
2352 this.$document = $( this.getElementDocument() );
2353
2354 // Events
2355 this.$element.on( 'mousedown', this.onMouseDown.bind( this ) );
2356
2357 // Initialization
2358 this.$head.addClass( 'oo-ui-window-head' );
2359 this.$body.addClass( 'oo-ui-window-body' );
2360 this.$foot.addClass( 'oo-ui-window-foot' );
2361 this.$content.append( this.$head, this.$body, this.$foot );
2362
2363 return this;
2364 };
2365
2366 /**
2367 * Open the window.
2368 *
2369 * This method is a wrapper around a call to the window manager’s {@link OO.ui.WindowManager#openWindow openWindow}
2370 * method, which returns a promise resolved when the window is done opening.
2371 *
2372 * To customize the window each time it opens, use #getSetupProcess or #getReadyProcess.
2373 *
2374 * @param {Object} [data] Window opening data
2375 * @return {jQuery.Promise} Promise resolved with a value when the window is opened, or rejected
2376 * if the window fails to open. When the promise is resolved successfully, the first argument of the
2377 * value is a new promise, which is resolved when the window begins closing.
2378 * @throws {Error} An error is thrown if the window is not attached to a window manager
2379 */
2380 OO.ui.Window.prototype.open = function ( data ) {
2381 if ( !this.manager ) {
2382 throw new Error( 'Cannot open window, must be attached to a manager' );
2383 }
2384
2385 return this.manager.openWindow( this, data );
2386 };
2387
2388 /**
2389 * Close the window.
2390 *
2391 * This method is a wrapper around a call to the window
2392 * manager’s {@link OO.ui.WindowManager#closeWindow closeWindow} method,
2393 * which returns a closing promise resolved when the window is done closing.
2394 *
2395 * The window's #getHoldProcess and #getTeardownProcess methods are called during the closing
2396 * phase of the window’s lifecycle and can be used to specify closing behavior each time
2397 * the window closes.
2398 *
2399 * @param {Object} [data] Window closing data
2400 * @return {jQuery.Promise} Promise resolved when window is closed
2401 * @throws {Error} An error is thrown if the window is not attached to a window manager
2402 */
2403 OO.ui.Window.prototype.close = function ( data ) {
2404 if ( !this.manager ) {
2405 throw new Error( 'Cannot close window, must be attached to a manager' );
2406 }
2407
2408 return this.manager.closeWindow( this, data );
2409 };
2410
2411 /**
2412 * Setup window.
2413 *
2414 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2415 * by other systems.
2416 *
2417 * @param {Object} [data] Window opening data
2418 * @return {jQuery.Promise} Promise resolved when window is setup
2419 */
2420 OO.ui.Window.prototype.setup = function ( data ) {
2421 var win = this,
2422 deferred = $.Deferred();
2423
2424 this.toggle( true );
2425
2426 this.getSetupProcess( data ).execute().done( function () {
2427 // Force redraw by asking the browser to measure the elements' widths
2428 win.$element.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2429 win.$content.addClass( 'oo-ui-window-content-setup' ).width();
2430 deferred.resolve();
2431 } );
2432
2433 return deferred.promise();
2434 };
2435
2436 /**
2437 * Ready window.
2438 *
2439 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2440 * by other systems.
2441 *
2442 * @param {Object} [data] Window opening data
2443 * @return {jQuery.Promise} Promise resolved when window is ready
2444 */
2445 OO.ui.Window.prototype.ready = function ( data ) {
2446 var win = this,
2447 deferred = $.Deferred();
2448
2449 this.$content.focus();
2450 this.getReadyProcess( data ).execute().done( function () {
2451 // Force redraw by asking the browser to measure the elements' widths
2452 win.$element.addClass( 'oo-ui-window-ready' ).width();
2453 win.$content.addClass( 'oo-ui-window-content-ready' ).width();
2454 deferred.resolve();
2455 } );
2456
2457 return deferred.promise();
2458 };
2459
2460 /**
2461 * Hold window.
2462 *
2463 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2464 * by other systems.
2465 *
2466 * @param {Object} [data] Window closing data
2467 * @return {jQuery.Promise} Promise resolved when window is held
2468 */
2469 OO.ui.Window.prototype.hold = function ( data ) {
2470 var win = this,
2471 deferred = $.Deferred();
2472
2473 this.getHoldProcess( data ).execute().done( function () {
2474 // Get the focused element within the window's content
2475 var $focus = win.$content.find( OO.ui.Element.static.getDocument( win.$content ).activeElement );
2476
2477 // Blur the focused element
2478 if ( $focus.length ) {
2479 $focus[ 0 ].blur();
2480 }
2481
2482 // Force redraw by asking the browser to measure the elements' widths
2483 win.$element.removeClass( 'oo-ui-window-ready' ).width();
2484 win.$content.removeClass( 'oo-ui-window-content-ready' ).width();
2485 deferred.resolve();
2486 } );
2487
2488 return deferred.promise();
2489 };
2490
2491 /**
2492 * Teardown window.
2493 *
2494 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2495 * by other systems.
2496 *
2497 * @param {Object} [data] Window closing data
2498 * @return {jQuery.Promise} Promise resolved when window is torn down
2499 */
2500 OO.ui.Window.prototype.teardown = function ( data ) {
2501 var win = this;
2502
2503 return this.getTeardownProcess( data ).execute()
2504 .done( function () {
2505 // Force redraw by asking the browser to measure the elements' widths
2506 win.$element.removeClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2507 win.$content.removeClass( 'oo-ui-window-content-setup' ).width();
2508 win.toggle( false );
2509 } );
2510 };
2511
2512 /**
2513 * The Dialog class serves as the base class for the other types of dialogs.
2514 * Unless extended to include controls, the rendered dialog box is a simple window
2515 * that users can close by hitting the ‘Esc’ key. Dialog windows are used with OO.ui.WindowManager,
2516 * which opens, closes, and controls the presentation of the window. See the
2517 * [OOjs UI documentation on MediaWiki] [1] for more information.
2518 *
2519 * @example
2520 * // A simple dialog window.
2521 * function MyDialog( config ) {
2522 * MyDialog.parent.call( this, config );
2523 * }
2524 * OO.inheritClass( MyDialog, OO.ui.Dialog );
2525 * MyDialog.prototype.initialize = function () {
2526 * MyDialog.parent.prototype.initialize.call( this );
2527 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
2528 * this.content.$element.append( '<p>A simple dialog window. Press \'Esc\' to close.</p>' );
2529 * this.$body.append( this.content.$element );
2530 * };
2531 * MyDialog.prototype.getBodyHeight = function () {
2532 * return this.content.$element.outerHeight( true );
2533 * };
2534 * var myDialog = new MyDialog( {
2535 * size: 'medium'
2536 * } );
2537 * // Create and append a window manager, which opens and closes the window.
2538 * var windowManager = new OO.ui.WindowManager();
2539 * $( 'body' ).append( windowManager.$element );
2540 * windowManager.addWindows( [ myDialog ] );
2541 * // Open the window!
2542 * windowManager.openWindow( myDialog );
2543 *
2544 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Dialogs
2545 *
2546 * @abstract
2547 * @class
2548 * @extends OO.ui.Window
2549 * @mixins OO.ui.mixin.PendingElement
2550 *
2551 * @constructor
2552 * @param {Object} [config] Configuration options
2553 */
2554 OO.ui.Dialog = function OoUiDialog( config ) {
2555 // Parent constructor
2556 OO.ui.Dialog.parent.call( this, config );
2557
2558 // Mixin constructors
2559 OO.ui.mixin.PendingElement.call( this );
2560
2561 // Properties
2562 this.actions = new OO.ui.ActionSet();
2563 this.attachedActions = [];
2564 this.currentAction = null;
2565 this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
2566
2567 // Events
2568 this.actions.connect( this, {
2569 click: 'onActionClick',
2570 resize: 'onActionResize',
2571 change: 'onActionsChange'
2572 } );
2573
2574 // Initialization
2575 this.$element
2576 .addClass( 'oo-ui-dialog' )
2577 .attr( 'role', 'dialog' );
2578 };
2579
2580 /* Setup */
2581
2582 OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
2583 OO.mixinClass( OO.ui.Dialog, OO.ui.mixin.PendingElement );
2584
2585 /* Static Properties */
2586
2587 /**
2588 * Symbolic name of dialog.
2589 *
2590 * The dialog class must have a symbolic name in order to be registered with OO.Factory.
2591 * Please see the [OOjs UI documentation on MediaWiki] [3] for more information.
2592 *
2593 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
2594 *
2595 * @abstract
2596 * @static
2597 * @inheritable
2598 * @property {string}
2599 */
2600 OO.ui.Dialog.static.name = '';
2601
2602 /**
2603 * The dialog title.
2604 *
2605 * The title can be specified as a plaintext string, a {@link OO.ui.mixin.LabelElement Label} node, or a function
2606 * that will produce a Label node or string. The title can also be specified with data passed to the
2607 * constructor (see #getSetupProcess). In this case, the static value will be overriden.
2608 *
2609 * @abstract
2610 * @static
2611 * @inheritable
2612 * @property {jQuery|string|Function}
2613 */
2614 OO.ui.Dialog.static.title = '';
2615
2616 /**
2617 * An array of configured {@link OO.ui.ActionWidget action widgets}.
2618 *
2619 * Actions can also be specified with data passed to the constructor (see #getSetupProcess). In this case, the static
2620 * value will be overriden.
2621 *
2622 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
2623 *
2624 * @static
2625 * @inheritable
2626 * @property {Object[]}
2627 */
2628 OO.ui.Dialog.static.actions = [];
2629
2630 /**
2631 * Close the dialog when the 'Esc' key is pressed.
2632 *
2633 * @static
2634 * @abstract
2635 * @inheritable
2636 * @property {boolean}
2637 */
2638 OO.ui.Dialog.static.escapable = true;
2639
2640 /* Methods */
2641
2642 /**
2643 * Handle frame document key down events.
2644 *
2645 * @private
2646 * @param {jQuery.Event} e Key down event
2647 */
2648 OO.ui.Dialog.prototype.onDocumentKeyDown = function ( e ) {
2649 if ( e.which === OO.ui.Keys.ESCAPE ) {
2650 this.close();
2651 e.preventDefault();
2652 e.stopPropagation();
2653 }
2654 };
2655
2656 /**
2657 * Handle action resized events.
2658 *
2659 * @private
2660 * @param {OO.ui.ActionWidget} action Action that was resized
2661 */
2662 OO.ui.Dialog.prototype.onActionResize = function () {
2663 // Override in subclass
2664 };
2665
2666 /**
2667 * Handle action click events.
2668 *
2669 * @private
2670 * @param {OO.ui.ActionWidget} action Action that was clicked
2671 */
2672 OO.ui.Dialog.prototype.onActionClick = function ( action ) {
2673 if ( !this.isPending() ) {
2674 this.executeAction( action.getAction() );
2675 }
2676 };
2677
2678 /**
2679 * Handle actions change event.
2680 *
2681 * @private
2682 */
2683 OO.ui.Dialog.prototype.onActionsChange = function () {
2684 this.detachActions();
2685 if ( !this.isClosing() ) {
2686 this.attachActions();
2687 }
2688 };
2689
2690 /**
2691 * Get the set of actions used by the dialog.
2692 *
2693 * @return {OO.ui.ActionSet}
2694 */
2695 OO.ui.Dialog.prototype.getActions = function () {
2696 return this.actions;
2697 };
2698
2699 /**
2700 * Get a process for taking action.
2701 *
2702 * When you override this method, you can create a new OO.ui.Process and return it, or add additional
2703 * accept steps to the process the parent method provides using the {@link OO.ui.Process#first 'first'}
2704 * and {@link OO.ui.Process#next 'next'} methods of OO.ui.Process.
2705 *
2706 * @abstract
2707 * @param {string} [action] Symbolic name of action
2708 * @return {OO.ui.Process} Action process
2709 */
2710 OO.ui.Dialog.prototype.getActionProcess = function ( action ) {
2711 return new OO.ui.Process()
2712 .next( function () {
2713 if ( !action ) {
2714 // An empty action always closes the dialog without data, which should always be
2715 // safe and make no changes
2716 this.close();
2717 }
2718 }, this );
2719 };
2720
2721 /**
2722 * @inheritdoc
2723 *
2724 * @param {Object} [data] Dialog opening data
2725 * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use
2726 * the {@link #static-title static title}
2727 * @param {Object[]} [data.actions] List of configuration options for each
2728 * {@link OO.ui.ActionWidget action widget}, omit to use {@link #static-actions static actions}.
2729 */
2730 OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
2731 data = data || {};
2732
2733 // Parent method
2734 return OO.ui.Dialog.parent.prototype.getSetupProcess.call( this, data )
2735 .next( function () {
2736 var config = this.constructor.static,
2737 actions = data.actions !== undefined ? data.actions : config.actions;
2738
2739 this.title.setLabel(
2740 data.title !== undefined ? data.title : this.constructor.static.title
2741 );
2742 this.actions.add( this.getActionWidgets( actions ) );
2743
2744 if ( this.constructor.static.escapable ) {
2745 this.$document.on( 'keydown', this.onDocumentKeyDownHandler );
2746 }
2747 }, this );
2748 };
2749
2750 /**
2751 * @inheritdoc
2752 */
2753 OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
2754 // Parent method
2755 return OO.ui.Dialog.parent.prototype.getTeardownProcess.call( this, data )
2756 .first( function () {
2757 if ( this.constructor.static.escapable ) {
2758 this.$document.off( 'keydown', this.onDocumentKeyDownHandler );
2759 }
2760
2761 this.actions.clear();
2762 this.currentAction = null;
2763 }, this );
2764 };
2765
2766 /**
2767 * @inheritdoc
2768 */
2769 OO.ui.Dialog.prototype.initialize = function () {
2770 // Parent method
2771 OO.ui.Dialog.parent.prototype.initialize.call( this );
2772
2773 var titleId = OO.ui.generateElementId();
2774
2775 // Properties
2776 this.title = new OO.ui.LabelWidget( {
2777 id: titleId
2778 } );
2779
2780 // Initialization
2781 this.$content.addClass( 'oo-ui-dialog-content' );
2782 this.$element.attr( 'aria-labelledby', titleId );
2783 this.setPendingElement( this.$head );
2784 };
2785
2786 /**
2787 * Get action widgets from a list of configs
2788 *
2789 * @param {Object[]} actions Action widget configs
2790 * @return {OO.ui.ActionWidget[]} Action widgets
2791 */
2792 OO.ui.Dialog.prototype.getActionWidgets = function ( actions ) {
2793 var i, len, widgets = [];
2794 for ( i = 0, len = actions.length; i < len; i++ ) {
2795 widgets.push(
2796 new OO.ui.ActionWidget( actions[ i ] )
2797 );
2798 }
2799 return widgets;
2800 };
2801
2802 /**
2803 * Attach action actions.
2804 *
2805 * @protected
2806 */
2807 OO.ui.Dialog.prototype.attachActions = function () {
2808 // Remember the list of potentially attached actions
2809 this.attachedActions = this.actions.get();
2810 };
2811
2812 /**
2813 * Detach action actions.
2814 *
2815 * @protected
2816 * @chainable
2817 */
2818 OO.ui.Dialog.prototype.detachActions = function () {
2819 var i, len;
2820
2821 // Detach all actions that may have been previously attached
2822 for ( i = 0, len = this.attachedActions.length; i < len; i++ ) {
2823 this.attachedActions[ i ].$element.detach();
2824 }
2825 this.attachedActions = [];
2826 };
2827
2828 /**
2829 * Execute an action.
2830 *
2831 * @param {string} action Symbolic name of action to execute
2832 * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
2833 */
2834 OO.ui.Dialog.prototype.executeAction = function ( action ) {
2835 this.pushPending();
2836 this.currentAction = action;
2837 return this.getActionProcess( action ).execute()
2838 .always( this.popPending.bind( this ) );
2839 };
2840
2841 /**
2842 * Window managers are used to open and close {@link OO.ui.Window windows} and control their presentation.
2843 * Managed windows are mutually exclusive. If a new window is opened while a current window is opening
2844 * or is opened, the current window will be closed and any ongoing {@link OO.ui.Process process} will be cancelled. Windows
2845 * themselves are persistent and—rather than being torn down when closed—can be repopulated with the
2846 * pertinent data and reused.
2847 *
2848 * Over the lifecycle of a window, the window manager makes available three promises: `opening`,
2849 * `opened`, and `closing`, which represent the primary stages of the cycle:
2850 *
2851 * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s
2852 * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window.
2853 *
2854 * - an `opening` event is emitted with an `opening` promise
2855 * - the #getSetupDelay method is called and the returned value is used to time a pause in execution before
2856 * the window’s {@link OO.ui.Window#getSetupProcess getSetupProcess} method is called on the
2857 * window and its result executed
2858 * - a `setup` progress notification is emitted from the `opening` promise
2859 * - the #getReadyDelay method is called the returned value is used to time a pause in execution before
2860 * the window’s {@link OO.ui.Window#getReadyProcess getReadyProcess} method is called on the
2861 * window and its result executed
2862 * - a `ready` progress notification is emitted from the `opening` promise
2863 * - the `opening` promise is resolved with an `opened` promise
2864 *
2865 * **Opened**: the window is now open.
2866 *
2867 * **Closing**: the closing stage begins when the window manager's #closeWindow or the
2868 * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins
2869 * to close the window.
2870 *
2871 * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted
2872 * - the #getHoldDelay method is called and the returned value is used to time a pause in execution before
2873 * the window's {@link OO.ui.Window#getHoldProcess getHoldProces} method is called on the
2874 * window and its result executed
2875 * - a `hold` progress notification is emitted from the `closing` promise
2876 * - the #getTeardownDelay() method is called and the returned value is used to time a pause in execution before
2877 * the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method is called on the
2878 * window and its result executed
2879 * - a `teardown` progress notification is emitted from the `closing` promise
2880 * - the `closing` promise is resolved. The window is now closed
2881 *
2882 * See the [OOjs UI documentation on MediaWiki][1] for more information.
2883 *
2884 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
2885 *
2886 * @class
2887 * @extends OO.ui.Element
2888 * @mixins OO.EventEmitter
2889 *
2890 * @constructor
2891 * @param {Object} [config] Configuration options
2892 * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
2893 * Note that window classes that are instantiated with a factory must have
2894 * a {@link OO.ui.Dialog#static-name static name} property that specifies a symbolic name.
2895 * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
2896 */
2897 OO.ui.WindowManager = function OoUiWindowManager( config ) {
2898 // Configuration initialization
2899 config = config || {};
2900
2901 // Parent constructor
2902 OO.ui.WindowManager.parent.call( this, config );
2903
2904 // Mixin constructors
2905 OO.EventEmitter.call( this );
2906
2907 // Properties
2908 this.factory = config.factory;
2909 this.modal = config.modal === undefined || !!config.modal;
2910 this.windows = {};
2911 this.opening = null;
2912 this.opened = null;
2913 this.closing = null;
2914 this.preparingToOpen = null;
2915 this.preparingToClose = null;
2916 this.currentWindow = null;
2917 this.globalEvents = false;
2918 this.$ariaHidden = null;
2919 this.onWindowResizeTimeout = null;
2920 this.onWindowResizeHandler = this.onWindowResize.bind( this );
2921 this.afterWindowResizeHandler = this.afterWindowResize.bind( this );
2922
2923 // Initialization
2924 this.$element
2925 .addClass( 'oo-ui-windowManager' )
2926 .toggleClass( 'oo-ui-windowManager-modal', this.modal );
2927 };
2928
2929 /* Setup */
2930
2931 OO.inheritClass( OO.ui.WindowManager, OO.ui.Element );
2932 OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter );
2933
2934 /* Events */
2935
2936 /**
2937 * An 'opening' event is emitted when the window begins to be opened.
2938 *
2939 * @event opening
2940 * @param {OO.ui.Window} win Window that's being opened
2941 * @param {jQuery.Promise} opening An `opening` promise resolved with a value when the window is opened successfully.
2942 * When the `opening` promise is resolved, the first argument of the value is an 'opened' promise, the second argument
2943 * is the opening data. The `opening` promise emits `setup` and `ready` notifications when those processes are complete.
2944 * @param {Object} data Window opening data
2945 */
2946
2947 /**
2948 * A 'closing' event is emitted when the window begins to be closed.
2949 *
2950 * @event closing
2951 * @param {OO.ui.Window} win Window that's being closed
2952 * @param {jQuery.Promise} closing A `closing` promise is resolved with a value when the window
2953 * is closed successfully. The promise emits `hold` and `teardown` notifications when those
2954 * processes are complete. When the `closing` promise is resolved, the first argument of its value
2955 * is the closing data.
2956 * @param {Object} data Window closing data
2957 */
2958
2959 /**
2960 * A 'resize' event is emitted when a window is resized.
2961 *
2962 * @event resize
2963 * @param {OO.ui.Window} win Window that was resized
2964 */
2965
2966 /* Static Properties */
2967
2968 /**
2969 * Map of the symbolic name of each window size and its CSS properties.
2970 *
2971 * @static
2972 * @inheritable
2973 * @property {Object}
2974 */
2975 OO.ui.WindowManager.static.sizes = {
2976 small: {
2977 width: 300
2978 },
2979 medium: {
2980 width: 500
2981 },
2982 large: {
2983 width: 700
2984 },
2985 larger: {
2986 width: 900
2987 },
2988 full: {
2989 // These can be non-numeric because they are never used in calculations
2990 width: '100%',
2991 height: '100%'
2992 }
2993 };
2994
2995 /**
2996 * Symbolic name of the default window size.
2997 *
2998 * The default size is used if the window's requested size is not recognized.
2999 *
3000 * @static
3001 * @inheritable
3002 * @property {string}
3003 */
3004 OO.ui.WindowManager.static.defaultSize = 'medium';
3005
3006 /* Methods */
3007
3008 /**
3009 * Handle window resize events.
3010 *
3011 * @private
3012 * @param {jQuery.Event} e Window resize event
3013 */
3014 OO.ui.WindowManager.prototype.onWindowResize = function () {
3015 clearTimeout( this.onWindowResizeTimeout );
3016 this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 );
3017 };
3018
3019 /**
3020 * Handle window resize events.
3021 *
3022 * @private
3023 * @param {jQuery.Event} e Window resize event
3024 */
3025 OO.ui.WindowManager.prototype.afterWindowResize = function () {
3026 if ( this.currentWindow ) {
3027 this.updateWindowSize( this.currentWindow );
3028 }
3029 };
3030
3031 /**
3032 * Check if window is opening.
3033 *
3034 * @return {boolean} Window is opening
3035 */
3036 OO.ui.WindowManager.prototype.isOpening = function ( win ) {
3037 return win === this.currentWindow && !!this.opening && this.opening.state() === 'pending';
3038 };
3039
3040 /**
3041 * Check if window is closing.
3042 *
3043 * @return {boolean} Window is closing
3044 */
3045 OO.ui.WindowManager.prototype.isClosing = function ( win ) {
3046 return win === this.currentWindow && !!this.closing && this.closing.state() === 'pending';
3047 };
3048
3049 /**
3050 * Check if window is opened.
3051 *
3052 * @return {boolean} Window is opened
3053 */
3054 OO.ui.WindowManager.prototype.isOpened = function ( win ) {
3055 return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending';
3056 };
3057
3058 /**
3059 * Check if a window is being managed.
3060 *
3061 * @param {OO.ui.Window} win Window to check
3062 * @return {boolean} Window is being managed
3063 */
3064 OO.ui.WindowManager.prototype.hasWindow = function ( win ) {
3065 var name;
3066
3067 for ( name in this.windows ) {
3068 if ( this.windows[ name ] === win ) {
3069 return true;
3070 }
3071 }
3072
3073 return false;
3074 };
3075
3076 /**
3077 * Get the number of milliseconds to wait after opening begins before executing the ‘setup’ process.
3078 *
3079 * @param {OO.ui.Window} win Window being opened
3080 * @param {Object} [data] Window opening data
3081 * @return {number} Milliseconds to wait
3082 */
3083 OO.ui.WindowManager.prototype.getSetupDelay = function () {
3084 return 0;
3085 };
3086
3087 /**
3088 * Get the number of milliseconds to wait after setup has finished before executing the ‘ready’ process.
3089 *
3090 * @param {OO.ui.Window} win Window being opened
3091 * @param {Object} [data] Window opening data
3092 * @return {number} Milliseconds to wait
3093 */
3094 OO.ui.WindowManager.prototype.getReadyDelay = function () {
3095 return 0;
3096 };
3097
3098 /**
3099 * Get the number of milliseconds to wait after closing has begun before executing the 'hold' process.
3100 *
3101 * @param {OO.ui.Window} win Window being closed
3102 * @param {Object} [data] Window closing data
3103 * @return {number} Milliseconds to wait
3104 */
3105 OO.ui.WindowManager.prototype.getHoldDelay = function () {
3106 return 0;
3107 };
3108
3109 /**
3110 * Get the number of milliseconds to wait after the ‘hold’ process has finished before
3111 * executing the ‘teardown’ process.
3112 *
3113 * @param {OO.ui.Window} win Window being closed
3114 * @param {Object} [data] Window closing data
3115 * @return {number} Milliseconds to wait
3116 */
3117 OO.ui.WindowManager.prototype.getTeardownDelay = function () {
3118 return this.modal ? 250 : 0;
3119 };
3120
3121 /**
3122 * Get a window by its symbolic name.
3123 *
3124 * If the window is not yet instantiated and its symbolic name is recognized by a factory, it will be
3125 * instantiated and added to the window manager automatically. Please see the [OOjs UI documentation on MediaWiki][3]
3126 * for more information about using factories.
3127 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
3128 *
3129 * @param {string} name Symbolic name of the window
3130 * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
3131 * @throws {Error} An error is thrown if the symbolic name is not recognized by the factory.
3132 * @throws {Error} An error is thrown if the named window is not recognized as a managed window.
3133 */
3134 OO.ui.WindowManager.prototype.getWindow = function ( name ) {
3135 var deferred = $.Deferred(),
3136 win = this.windows[ name ];
3137
3138 if ( !( win instanceof OO.ui.Window ) ) {
3139 if ( this.factory ) {
3140 if ( !this.factory.lookup( name ) ) {
3141 deferred.reject( new OO.ui.Error(
3142 'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
3143 ) );
3144 } else {
3145 win = this.factory.create( name );
3146 this.addWindows( [ win ] );
3147 deferred.resolve( win );
3148 }
3149 } else {
3150 deferred.reject( new OO.ui.Error(
3151 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
3152 ) );
3153 }
3154 } else {
3155 deferred.resolve( win );
3156 }
3157
3158 return deferred.promise();
3159 };
3160
3161 /**
3162 * Get current window.
3163 *
3164 * @return {OO.ui.Window|null} Currently opening/opened/closing window
3165 */
3166 OO.ui.WindowManager.prototype.getCurrentWindow = function () {
3167 return this.currentWindow;
3168 };
3169
3170 /**
3171 * Open a window.
3172 *
3173 * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
3174 * @param {Object} [data] Window opening data
3175 * @return {jQuery.Promise} An `opening` promise resolved when the window is done opening.
3176 * See {@link #event-opening 'opening' event} for more information about `opening` promises.
3177 * @fires opening
3178 */
3179 OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
3180 var manager = this,
3181 opening = $.Deferred();
3182
3183 // Argument handling
3184 if ( typeof win === 'string' ) {
3185 return this.getWindow( win ).then( function ( win ) {
3186 return manager.openWindow( win, data );
3187 } );
3188 }
3189
3190 // Error handling
3191 if ( !this.hasWindow( win ) ) {
3192 opening.reject( new OO.ui.Error(
3193 'Cannot open window: window is not attached to manager'
3194 ) );
3195 } else if ( this.preparingToOpen || this.opening || this.opened ) {
3196 opening.reject( new OO.ui.Error(
3197 'Cannot open window: another window is opening or open'
3198 ) );
3199 }
3200
3201 // Window opening
3202 if ( opening.state() !== 'rejected' ) {
3203 // If a window is currently closing, wait for it to complete
3204 this.preparingToOpen = $.when( this.closing );
3205 // Ensure handlers get called after preparingToOpen is set
3206 this.preparingToOpen.done( function () {
3207 if ( manager.modal ) {
3208 manager.toggleGlobalEvents( true );
3209 manager.toggleAriaIsolation( true );
3210 }
3211 manager.currentWindow = win;
3212 manager.opening = opening;
3213 manager.preparingToOpen = null;
3214 manager.emit( 'opening', win, opening, data );
3215 setTimeout( function () {
3216 win.setup( data ).then( function () {
3217 manager.updateWindowSize( win );
3218 manager.opening.notify( { state: 'setup' } );
3219 setTimeout( function () {
3220 win.ready( data ).then( function () {
3221 manager.opening.notify( { state: 'ready' } );
3222 manager.opening = null;
3223 manager.opened = $.Deferred();
3224 opening.resolve( manager.opened.promise(), data );
3225 } );
3226 }, manager.getReadyDelay() );
3227 } );
3228 }, manager.getSetupDelay() );
3229 } );
3230 }
3231
3232 return opening.promise();
3233 };
3234
3235 /**
3236 * Close a window.
3237 *
3238 * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
3239 * @param {Object} [data] Window closing data
3240 * @return {jQuery.Promise} A `closing` promise resolved when the window is done closing.
3241 * See {@link #event-closing 'closing' event} for more information about closing promises.
3242 * @throws {Error} An error is thrown if the window is not managed by the window manager.
3243 * @fires closing
3244 */
3245 OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
3246 var manager = this,
3247 closing = $.Deferred(),
3248 opened;
3249
3250 // Argument handling
3251 if ( typeof win === 'string' ) {
3252 win = this.windows[ win ];
3253 } else if ( !this.hasWindow( win ) ) {
3254 win = null;
3255 }
3256
3257 // Error handling
3258 if ( !win ) {
3259 closing.reject( new OO.ui.Error(
3260 'Cannot close window: window is not attached to manager'
3261 ) );
3262 } else if ( win !== this.currentWindow ) {
3263 closing.reject( new OO.ui.Error(
3264 'Cannot close window: window already closed with different data'
3265 ) );
3266 } else if ( this.preparingToClose || this.closing ) {
3267 closing.reject( new OO.ui.Error(
3268 'Cannot close window: window already closing with different data'
3269 ) );
3270 }
3271
3272 // Window closing
3273 if ( closing.state() !== 'rejected' ) {
3274 // If the window is currently opening, close it when it's done
3275 this.preparingToClose = $.when( this.opening );
3276 // Ensure handlers get called after preparingToClose is set
3277 this.preparingToClose.done( function () {
3278 manager.closing = closing;
3279 manager.preparingToClose = null;
3280 manager.emit( 'closing', win, closing, data );
3281 opened = manager.opened;
3282 manager.opened = null;
3283 opened.resolve( closing.promise(), data );
3284 setTimeout( function () {
3285 win.hold( data ).then( function () {
3286 closing.notify( { state: 'hold' } );
3287 setTimeout( function () {
3288 win.teardown( data ).then( function () {
3289 closing.notify( { state: 'teardown' } );
3290 if ( manager.modal ) {
3291 manager.toggleGlobalEvents( false );
3292 manager.toggleAriaIsolation( false );
3293 }
3294 manager.closing = null;
3295 manager.currentWindow = null;
3296 closing.resolve( data );
3297 } );
3298 }, manager.getTeardownDelay() );
3299 } );
3300 }, manager.getHoldDelay() );
3301 } );
3302 }
3303
3304 return closing.promise();
3305 };
3306
3307 /**
3308 * Add windows to the window manager.
3309 *
3310 * Windows can be added by reference, symbolic name, or explicitly defined symbolic names.
3311 * See the [OOjs ui documentation on MediaWiki] [2] for examples.
3312 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
3313 *
3314 * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows An array of window objects specified
3315 * by reference, symbolic name, or explicitly defined symbolic names.
3316 * @throws {Error} An error is thrown if a window is added by symbolic name, but has neither an
3317 * explicit nor a statically configured symbolic name.
3318 */
3319 OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
3320 var i, len, win, name, list;
3321
3322 if ( Array.isArray( windows ) ) {
3323 // Convert to map of windows by looking up symbolic names from static configuration
3324 list = {};
3325 for ( i = 0, len = windows.length; i < len; i++ ) {
3326 name = windows[ i ].constructor.static.name;
3327 if ( typeof name !== 'string' ) {
3328 throw new Error( 'Cannot add window' );
3329 }
3330 list[ name ] = windows[ i ];
3331 }
3332 } else if ( OO.isPlainObject( windows ) ) {
3333 list = windows;
3334 }
3335
3336 // Add windows
3337 for ( name in list ) {
3338 win = list[ name ];
3339 this.windows[ name ] = win.toggle( false );
3340 this.$element.append( win.$element );
3341 win.setManager( this );
3342 }
3343 };
3344
3345 /**
3346 * Remove the specified windows from the windows manager.
3347 *
3348 * Windows will be closed before they are removed. If you wish to remove all windows, you may wish to use
3349 * the #clearWindows method instead. If you no longer need the window manager and want to ensure that it no
3350 * longer listens to events, use the #destroy method.
3351 *
3352 * @param {string[]} names Symbolic names of windows to remove
3353 * @return {jQuery.Promise} Promise resolved when window is closed and removed
3354 * @throws {Error} An error is thrown if the named windows are not managed by the window manager.
3355 */
3356 OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
3357 var i, len, win, name, cleanupWindow,
3358 manager = this,
3359 promises = [],
3360 cleanup = function ( name, win ) {
3361 delete manager.windows[ name ];
3362 win.$element.detach();
3363 };
3364
3365 for ( i = 0, len = names.length; i < len; i++ ) {
3366 name = names[ i ];
3367 win = this.windows[ name ];
3368 if ( !win ) {
3369 throw new Error( 'Cannot remove window' );
3370 }
3371 cleanupWindow = cleanup.bind( null, name, win );
3372 promises.push( this.closeWindow( name ).then( cleanupWindow, cleanupWindow ) );
3373 }
3374
3375 return $.when.apply( $, promises );
3376 };
3377
3378 /**
3379 * Remove all windows from the window manager.
3380 *
3381 * Windows will be closed before they are removed. Note that the window manager, though not in use, will still
3382 * listen to events. If the window manager will not be used again, you may wish to use the #destroy method instead.
3383 * To remove just a subset of windows, use the #removeWindows method.
3384 *
3385 * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
3386 */
3387 OO.ui.WindowManager.prototype.clearWindows = function () {
3388 return this.removeWindows( Object.keys( this.windows ) );
3389 };
3390
3391 /**
3392 * Set dialog size. In general, this method should not be called directly.
3393 *
3394 * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
3395 *
3396 * @chainable
3397 */
3398 OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) {
3399 // Bypass for non-current, and thus invisible, windows
3400 if ( win !== this.currentWindow ) {
3401 return;
3402 }
3403
3404 var viewport = OO.ui.Element.static.getDimensions( win.getElementWindow() ),
3405 sizes = this.constructor.static.sizes,
3406 size = win.getSize();
3407
3408 if ( !sizes[ size ] ) {
3409 size = this.constructor.static.defaultSize;
3410 }
3411 if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[ size ].width ) {
3412 size = 'full';
3413 }
3414
3415 this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', size === 'full' );
3416 this.$element.toggleClass( 'oo-ui-windowManager-floating', size !== 'full' );
3417 win.setDimensions( sizes[ size ] );
3418
3419 this.emit( 'resize', win );
3420
3421 return this;
3422 };
3423
3424 /**
3425 * Bind or unbind global events for scrolling.
3426 *
3427 * @private
3428 * @param {boolean} [on] Bind global events
3429 * @chainable
3430 */
3431 OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) {
3432 on = on === undefined ? !!this.globalEvents : !!on;
3433
3434 var scrollWidth, bodyMargin,
3435 $body = $( this.getElementDocument().body ),
3436 // We could have multiple window managers open so only modify
3437 // the body css at the bottom of the stack
3438 stackDepth = $body.data( 'windowManagerGlobalEvents' ) || 0 ;
3439
3440 if ( on ) {
3441 if ( !this.globalEvents ) {
3442 $( this.getElementWindow() ).on( {
3443 // Start listening for top-level window dimension changes
3444 'orientationchange resize': this.onWindowResizeHandler
3445 } );
3446 if ( stackDepth === 0 ) {
3447 scrollWidth = window.innerWidth - document.documentElement.clientWidth;
3448 bodyMargin = parseFloat( $body.css( 'margin-right' ) ) || 0;
3449 $body.css( {
3450 overflow: 'hidden',
3451 'margin-right': bodyMargin + scrollWidth
3452 } );
3453 }
3454 stackDepth++;
3455 this.globalEvents = true;
3456 }
3457 } else if ( this.globalEvents ) {
3458 $( this.getElementWindow() ).off( {
3459 // Stop listening for top-level window dimension changes
3460 'orientationchange resize': this.onWindowResizeHandler
3461 } );
3462 stackDepth--;
3463 if ( stackDepth === 0 ) {
3464 $body.css( {
3465 overflow: '',
3466 'margin-right': ''
3467 } );
3468 }
3469 this.globalEvents = false;
3470 }
3471 $body.data( 'windowManagerGlobalEvents', stackDepth );
3472
3473 return this;
3474 };
3475
3476 /**
3477 * Toggle screen reader visibility of content other than the window manager.
3478 *
3479 * @private
3480 * @param {boolean} [isolate] Make only the window manager visible to screen readers
3481 * @chainable
3482 */
3483 OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) {
3484 isolate = isolate === undefined ? !this.$ariaHidden : !!isolate;
3485
3486 if ( isolate ) {
3487 if ( !this.$ariaHidden ) {
3488 // Hide everything other than the window manager from screen readers
3489 this.$ariaHidden = $( 'body' )
3490 .children()
3491 .not( this.$element.parentsUntil( 'body' ).last() )
3492 .attr( 'aria-hidden', '' );
3493 }
3494 } else if ( this.$ariaHidden ) {
3495 // Restore screen reader visibility
3496 this.$ariaHidden.removeAttr( 'aria-hidden' );
3497 this.$ariaHidden = null;
3498 }
3499
3500 return this;
3501 };
3502
3503 /**
3504 * Destroy the window manager.
3505 *
3506 * Destroying the window manager ensures that it will no longer listen to events. If you would like to
3507 * continue using the window manager, but wish to remove all windows from it, use the #clearWindows method
3508 * instead.
3509 */
3510 OO.ui.WindowManager.prototype.destroy = function () {
3511 this.toggleGlobalEvents( false );
3512 this.toggleAriaIsolation( false );
3513 this.clearWindows();
3514 this.$element.remove();
3515 };
3516
3517 /**
3518 * Errors contain a required message (either a string or jQuery selection) that is used to describe what went wrong
3519 * in a {@link OO.ui.Process process}. The error's #recoverable and #warning configurations are used to customize the
3520 * appearance and functionality of the error interface.
3521 *
3522 * The basic error interface contains a formatted error message as well as two buttons: 'Dismiss' and 'Try again' (i.e., the error
3523 * is 'recoverable' by default). If the error is not recoverable, the 'Try again' button will not be rendered and the widget
3524 * that initiated the failed process will be disabled.
3525 *
3526 * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button, which will try the
3527 * process again.
3528 *
3529 * For an example of error interfaces, please see the [OOjs UI documentation on MediaWiki][1].
3530 *
3531 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Processes_and_errors
3532 *
3533 * @class
3534 *
3535 * @constructor
3536 * @param {string|jQuery} message Description of error
3537 * @param {Object} [config] Configuration options
3538 * @cfg {boolean} [recoverable=true] Error is recoverable.
3539 * By default, errors are recoverable, and users can try the process again.
3540 * @cfg {boolean} [warning=false] Error is a warning.
3541 * If the error is a warning, the error interface will include a
3542 * 'Dismiss' and a 'Continue' button. It is the responsibility of the developer to ensure that the warning
3543 * is not triggered a second time if the user chooses to continue.
3544 */
3545 OO.ui.Error = function OoUiError( message, config ) {
3546 // Allow passing positional parameters inside the config object
3547 if ( OO.isPlainObject( message ) && config === undefined ) {
3548 config = message;
3549 message = config.message;
3550 }
3551
3552 // Configuration initialization
3553 config = config || {};
3554
3555 // Properties
3556 this.message = message instanceof jQuery ? message : String( message );
3557 this.recoverable = config.recoverable === undefined || !!config.recoverable;
3558 this.warning = !!config.warning;
3559 };
3560
3561 /* Setup */
3562
3563 OO.initClass( OO.ui.Error );
3564
3565 /* Methods */
3566
3567 /**
3568 * Check if the error is recoverable.
3569 *
3570 * If the error is recoverable, users are able to try the process again.
3571 *
3572 * @return {boolean} Error is recoverable
3573 */
3574 OO.ui.Error.prototype.isRecoverable = function () {
3575 return this.recoverable;
3576 };
3577
3578 /**
3579 * Check if the error is a warning.
3580 *
3581 * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button.
3582 *
3583 * @return {boolean} Error is warning
3584 */
3585 OO.ui.Error.prototype.isWarning = function () {
3586 return this.warning;
3587 };
3588
3589 /**
3590 * Get error message as DOM nodes.
3591 *
3592 * @return {jQuery} Error message in DOM nodes
3593 */
3594 OO.ui.Error.prototype.getMessage = function () {
3595 return this.message instanceof jQuery ?
3596 this.message.clone() :
3597 $( '<div>' ).text( this.message ).contents();
3598 };
3599
3600 /**
3601 * Get the error message text.
3602 *
3603 * @return {string} Error message
3604 */
3605 OO.ui.Error.prototype.getMessageText = function () {
3606 return this.message instanceof jQuery ? this.message.text() : this.message;
3607 };
3608
3609 /**
3610 * Wraps an HTML snippet for use with configuration values which default
3611 * to strings. This bypasses the default html-escaping done to string
3612 * values.
3613 *
3614 * @class
3615 *
3616 * @constructor
3617 * @param {string} [content] HTML content
3618 */
3619 OO.ui.HtmlSnippet = function OoUiHtmlSnippet( content ) {
3620 // Properties
3621 this.content = content;
3622 };
3623
3624 /* Setup */
3625
3626 OO.initClass( OO.ui.HtmlSnippet );
3627
3628 /* Methods */
3629
3630 /**
3631 * Render into HTML.
3632 *
3633 * @return {string} Unchanged HTML snippet.
3634 */
3635 OO.ui.HtmlSnippet.prototype.toString = function () {
3636 return this.content;
3637 };
3638
3639 /**
3640 * A Process is a list of steps that are called in sequence. The step can be a number, a jQuery promise,
3641 * or a function:
3642 *
3643 * - **number**: the process will wait for the specified number of milliseconds before proceeding.
3644 * - **promise**: the process will continue to the next step when the promise is successfully resolved
3645 * or stop if the promise is rejected.
3646 * - **function**: the process will execute the function. The process will stop if the function returns
3647 * either a boolean `false` or a promise that is rejected; if the function returns a number, the process
3648 * will wait for that number of milliseconds before proceeding.
3649 *
3650 * If the process fails, an {@link OO.ui.Error error} is generated. Depending on how the error is
3651 * configured, users can dismiss the error and try the process again, or not. If a process is stopped,
3652 * its remaining steps will not be performed.
3653 *
3654 * @class
3655 *
3656 * @constructor
3657 * @param {number|jQuery.Promise|Function} step Number of miliseconds to wait before proceeding, promise
3658 * that must be resolved before proceeding, or a function to execute. See #createStep for more information. see #createStep for more information
3659 * @param {Object} [context=null] Execution context of the function. The context is ignored if the step is
3660 * a number or promise.
3661 * @return {Object} Step object, with `callback` and `context` properties
3662 */
3663 OO.ui.Process = function ( step, context ) {
3664 // Properties
3665 this.steps = [];
3666
3667 // Initialization
3668 if ( step !== undefined ) {
3669 this.next( step, context );
3670 }
3671 };
3672
3673 /* Setup */
3674
3675 OO.initClass( OO.ui.Process );
3676
3677 /* Methods */
3678
3679 /**
3680 * Start the process.
3681 *
3682 * @return {jQuery.Promise} Promise that is resolved when all steps have successfully completed.
3683 * If any of the steps return a promise that is rejected or a boolean false, this promise is rejected
3684 * and any remaining steps are not performed.
3685 */
3686 OO.ui.Process.prototype.execute = function () {
3687 var i, len, promise;
3688
3689 /**
3690 * Continue execution.
3691 *
3692 * @ignore
3693 * @param {Array} step A function and the context it should be called in
3694 * @return {Function} Function that continues the process
3695 */
3696 function proceed( step ) {
3697 return function () {
3698 // Execute step in the correct context
3699 var deferred,
3700 result = step.callback.call( step.context );
3701
3702 if ( result === false ) {
3703 // Use rejected promise for boolean false results
3704 return $.Deferred().reject( [] ).promise();
3705 }
3706 if ( typeof result === 'number' ) {
3707 if ( result < 0 ) {
3708 throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
3709 }
3710 // Use a delayed promise for numbers, expecting them to be in milliseconds
3711 deferred = $.Deferred();
3712 setTimeout( deferred.resolve, result );
3713 return deferred.promise();
3714 }
3715 if ( result instanceof OO.ui.Error ) {
3716 // Use rejected promise for error
3717 return $.Deferred().reject( [ result ] ).promise();
3718 }
3719 if ( Array.isArray( result ) && result.length && result[ 0 ] instanceof OO.ui.Error ) {
3720 // Use rejected promise for list of errors
3721 return $.Deferred().reject( result ).promise();
3722 }
3723 // Duck-type the object to see if it can produce a promise
3724 if ( result && $.isFunction( result.promise ) ) {
3725 // Use a promise generated from the result
3726 return result.promise();
3727 }
3728 // Use resolved promise for other results
3729 return $.Deferred().resolve().promise();
3730 };
3731 }
3732
3733 if ( this.steps.length ) {
3734 // Generate a chain reaction of promises
3735 promise = proceed( this.steps[ 0 ] )();
3736 for ( i = 1, len = this.steps.length; i < len; i++ ) {
3737 promise = promise.then( proceed( this.steps[ i ] ) );
3738 }
3739 } else {
3740 promise = $.Deferred().resolve().promise();
3741 }
3742
3743 return promise;
3744 };
3745
3746 /**
3747 * Create a process step.
3748 *
3749 * @private
3750 * @param {number|jQuery.Promise|Function} step
3751 *
3752 * - Number of milliseconds to wait before proceeding
3753 * - Promise that must be resolved before proceeding
3754 * - Function to execute
3755 * - If the function returns a boolean false the process will stop
3756 * - If the function returns a promise, the process will continue to the next
3757 * step when the promise is resolved or stop if the promise is rejected
3758 * - If the function returns a number, the process will wait for that number of
3759 * milliseconds before proceeding
3760 * @param {Object} [context=null] Execution context of the function. The context is
3761 * ignored if the step is a number or promise.
3762 * @return {Object} Step object, with `callback` and `context` properties
3763 */
3764 OO.ui.Process.prototype.createStep = function ( step, context ) {
3765 if ( typeof step === 'number' || $.isFunction( step.promise ) ) {
3766 return {
3767 callback: function () {
3768 return step;
3769 },
3770 context: null
3771 };
3772 }
3773 if ( $.isFunction( step ) ) {
3774 return {
3775 callback: step,
3776 context: context
3777 };
3778 }
3779 throw new Error( 'Cannot create process step: number, promise or function expected' );
3780 };
3781
3782 /**
3783 * Add step to the beginning of the process.
3784 *
3785 * @inheritdoc #createStep
3786 * @return {OO.ui.Process} this
3787 * @chainable
3788 */
3789 OO.ui.Process.prototype.first = function ( step, context ) {
3790 this.steps.unshift( this.createStep( step, context ) );
3791 return this;
3792 };
3793
3794 /**
3795 * Add step to the end of the process.
3796 *
3797 * @inheritdoc #createStep
3798 * @return {OO.ui.Process} this
3799 * @chainable
3800 */
3801 OO.ui.Process.prototype.next = function ( step, context ) {
3802 this.steps.push( this.createStep( step, context ) );
3803 return this;
3804 };
3805
3806 /**
3807 * A ToolFactory creates tools on demand. All tools ({@link OO.ui.Tool Tools}, {@link OO.ui.PopupTool PopupTools},
3808 * and {@link OO.ui.ToolGroupTool ToolGroupTools}) must be registered with a tool factory. Tools are
3809 * registered by their symbolic name. See {@link OO.ui.Toolbar toolbars} for an example.
3810 *
3811 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
3812 *
3813 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
3814 *
3815 * @class
3816 * @extends OO.Factory
3817 * @constructor
3818 */
3819 OO.ui.ToolFactory = function OoUiToolFactory() {
3820 // Parent constructor
3821 OO.ui.ToolFactory.parent.call( this );
3822 };
3823
3824 /* Setup */
3825
3826 OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
3827
3828 /* Methods */
3829
3830 /**
3831 * Get tools from the factory
3832 *
3833 * @param {Array} include Included tools
3834 * @param {Array} exclude Excluded tools
3835 * @param {Array} promote Promoted tools
3836 * @param {Array} demote Demoted tools
3837 * @return {string[]} List of tools
3838 */
3839 OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
3840 var i, len, included, promoted, demoted,
3841 auto = [],
3842 used = {};
3843
3844 // Collect included and not excluded tools
3845 included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
3846
3847 // Promotion
3848 promoted = this.extract( promote, used );
3849 demoted = this.extract( demote, used );
3850
3851 // Auto
3852 for ( i = 0, len = included.length; i < len; i++ ) {
3853 if ( !used[ included[ i ] ] ) {
3854 auto.push( included[ i ] );
3855 }
3856 }
3857
3858 return promoted.concat( auto ).concat( demoted );
3859 };
3860
3861 /**
3862 * Get a flat list of names from a list of names or groups.
3863 *
3864 * Tools can be specified in the following ways:
3865 *
3866 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
3867 * - All tools in a group: `{ group: 'group-name' }`
3868 * - All tools: `'*'`
3869 *
3870 * @private
3871 * @param {Array|string} collection List of tools
3872 * @param {Object} [used] Object with names that should be skipped as properties; extracted
3873 * names will be added as properties
3874 * @return {string[]} List of extracted names
3875 */
3876 OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
3877 var i, len, item, name, tool,
3878 names = [];
3879
3880 if ( collection === '*' ) {
3881 for ( name in this.registry ) {
3882 tool = this.registry[ name ];
3883 if (
3884 // Only add tools by group name when auto-add is enabled
3885 tool.static.autoAddToCatchall &&
3886 // Exclude already used tools
3887 ( !used || !used[ name ] )
3888 ) {
3889 names.push( name );
3890 if ( used ) {
3891 used[ name ] = true;
3892 }
3893 }
3894 }
3895 } else if ( Array.isArray( collection ) ) {
3896 for ( i = 0, len = collection.length; i < len; i++ ) {
3897 item = collection[ i ];
3898 // Allow plain strings as shorthand for named tools
3899 if ( typeof item === 'string' ) {
3900 item = { name: item };
3901 }
3902 if ( OO.isPlainObject( item ) ) {
3903 if ( item.group ) {
3904 for ( name in this.registry ) {
3905 tool = this.registry[ name ];
3906 if (
3907 // Include tools with matching group
3908 tool.static.group === item.group &&
3909 // Only add tools by group name when auto-add is enabled
3910 tool.static.autoAddToGroup &&
3911 // Exclude already used tools
3912 ( !used || !used[ name ] )
3913 ) {
3914 names.push( name );
3915 if ( used ) {
3916 used[ name ] = true;
3917 }
3918 }
3919 }
3920 // Include tools with matching name and exclude already used tools
3921 } else if ( item.name && ( !used || !used[ item.name ] ) ) {
3922 names.push( item.name );
3923 if ( used ) {
3924 used[ item.name ] = true;
3925 }
3926 }
3927 }
3928 }
3929 }
3930 return names;
3931 };
3932
3933 /**
3934 * ToolGroupFactories create {@link OO.ui.ToolGroup toolgroups} on demand. The toolgroup classes must
3935 * specify a symbolic name and be registered with the factory. The following classes are registered by
3936 * default:
3937 *
3938 * - {@link OO.ui.BarToolGroup BarToolGroups} (‘bar’)
3939 * - {@link OO.ui.MenuToolGroup MenuToolGroups} (‘menu’)
3940 * - {@link OO.ui.ListToolGroup ListToolGroups} (‘list’)
3941 *
3942 * See {@link OO.ui.Toolbar toolbars} for an example.
3943 *
3944 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
3945 *
3946 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
3947 * @class
3948 * @extends OO.Factory
3949 * @constructor
3950 */
3951 OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() {
3952 // Parent constructor
3953 OO.Factory.call( this );
3954
3955 var i, l,
3956 defaultClasses = this.constructor.static.getDefaultClasses();
3957
3958 // Register default toolgroups
3959 for ( i = 0, l = defaultClasses.length; i < l; i++ ) {
3960 this.register( defaultClasses[ i ] );
3961 }
3962 };
3963
3964 /* Setup */
3965
3966 OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory );
3967
3968 /* Static Methods */
3969
3970 /**
3971 * Get a default set of classes to be registered on construction.
3972 *
3973 * @return {Function[]} Default classes
3974 */
3975 OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
3976 return [
3977 OO.ui.BarToolGroup,
3978 OO.ui.ListToolGroup,
3979 OO.ui.MenuToolGroup
3980 ];
3981 };
3982
3983 /**
3984 * Theme logic.
3985 *
3986 * @abstract
3987 * @class
3988 *
3989 * @constructor
3990 * @param {Object} [config] Configuration options
3991 */
3992 OO.ui.Theme = function OoUiTheme( config ) {
3993 // Configuration initialization
3994 config = config || {};
3995 };
3996
3997 /* Setup */
3998
3999 OO.initClass( OO.ui.Theme );
4000
4001 /* Methods */
4002
4003 /**
4004 * Get a list of classes to be applied to a widget.
4005 *
4006 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
4007 * otherwise state transitions will not work properly.
4008 *
4009 * @param {OO.ui.Element} element Element for which to get classes
4010 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
4011 */
4012 OO.ui.Theme.prototype.getElementClasses = function ( /* element */ ) {
4013 return { on: [], off: [] };
4014 };
4015
4016 /**
4017 * Update CSS classes provided by the theme.
4018 *
4019 * For elements with theme logic hooks, this should be called any time there's a state change.
4020 *
4021 * @param {OO.ui.Element} element Element for which to update classes
4022 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
4023 */
4024 OO.ui.Theme.prototype.updateElementClasses = function ( element ) {
4025 var classes = this.getElementClasses( element );
4026
4027 element.$element
4028 .removeClass( classes.off.join( ' ' ) )
4029 .addClass( classes.on.join( ' ' ) );
4030 };
4031
4032 /**
4033 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
4034 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
4035 * order in which users will navigate through the focusable elements via the "tab" key.
4036 *
4037 * @example
4038 * // TabIndexedElement is mixed into the ButtonWidget class
4039 * // to provide a tabIndex property.
4040 * var button1 = new OO.ui.ButtonWidget( {
4041 * label: 'fourth',
4042 * tabIndex: 4
4043 * } );
4044 * var button2 = new OO.ui.ButtonWidget( {
4045 * label: 'second',
4046 * tabIndex: 2
4047 * } );
4048 * var button3 = new OO.ui.ButtonWidget( {
4049 * label: 'third',
4050 * tabIndex: 3
4051 * } );
4052 * var button4 = new OO.ui.ButtonWidget( {
4053 * label: 'first',
4054 * tabIndex: 1
4055 * } );
4056 * $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
4057 *
4058 * @abstract
4059 * @class
4060 *
4061 * @constructor
4062 * @param {Object} [config] Configuration options
4063 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
4064 * the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
4065 * functionality will be applied to it instead.
4066 * @cfg {number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
4067 * order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
4068 * to remove the element from the tab-navigation flow.
4069 */
4070 OO.ui.mixin.TabIndexedElement = function OoUiMixinTabIndexedElement( config ) {
4071 // Configuration initialization
4072 config = $.extend( { tabIndex: 0 }, config );
4073
4074 // Properties
4075 this.$tabIndexed = null;
4076 this.tabIndex = null;
4077
4078 // Events
4079 this.connect( this, { disable: 'onTabIndexedElementDisable' } );
4080
4081 // Initialization
4082 this.setTabIndex( config.tabIndex );
4083 this.setTabIndexedElement( config.$tabIndexed || this.$element );
4084 };
4085
4086 /* Setup */
4087
4088 OO.initClass( OO.ui.mixin.TabIndexedElement );
4089
4090 /* Methods */
4091
4092 /**
4093 * Set the element that should use the tabindex functionality.
4094 *
4095 * This method is used to retarget a tabindex mixin so that its functionality applies
4096 * to the specified element. If an element is currently using the functionality, the mixin’s
4097 * effect on that element is removed before the new element is set up.
4098 *
4099 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
4100 * @chainable
4101 */
4102 OO.ui.mixin.TabIndexedElement.prototype.setTabIndexedElement = function ( $tabIndexed ) {
4103 var tabIndex = this.tabIndex;
4104 // Remove attributes from old $tabIndexed
4105 this.setTabIndex( null );
4106 // Force update of new $tabIndexed
4107 this.$tabIndexed = $tabIndexed;
4108 this.tabIndex = tabIndex;
4109 return this.updateTabIndex();
4110 };
4111
4112 /**
4113 * Set the value of the tabindex.
4114 *
4115 * @param {number|null} tabIndex Tabindex value, or `null` for no tabindex
4116 * @chainable
4117 */
4118 OO.ui.mixin.TabIndexedElement.prototype.setTabIndex = function ( tabIndex ) {
4119 tabIndex = typeof tabIndex === 'number' ? tabIndex : null;
4120
4121 if ( this.tabIndex !== tabIndex ) {
4122 this.tabIndex = tabIndex;
4123 this.updateTabIndex();
4124 }
4125
4126 return this;
4127 };
4128
4129 /**
4130 * Update the `tabindex` attribute, in case of changes to tab index or
4131 * disabled state.
4132 *
4133 * @private
4134 * @chainable
4135 */
4136 OO.ui.mixin.TabIndexedElement.prototype.updateTabIndex = function () {
4137 if ( this.$tabIndexed ) {
4138 if ( this.tabIndex !== null ) {
4139 // Do not index over disabled elements
4140 this.$tabIndexed.attr( {
4141 tabindex: this.isDisabled() ? -1 : this.tabIndex,
4142 // ChromeVox and NVDA do not seem to inherit this from parent elements
4143 'aria-disabled': this.isDisabled().toString()
4144 } );
4145 } else {
4146 this.$tabIndexed.removeAttr( 'tabindex aria-disabled' );
4147 }
4148 }
4149 return this;
4150 };
4151
4152 /**
4153 * Handle disable events.
4154 *
4155 * @private
4156 * @param {boolean} disabled Element is disabled
4157 */
4158 OO.ui.mixin.TabIndexedElement.prototype.onTabIndexedElementDisable = function () {
4159 this.updateTabIndex();
4160 };
4161
4162 /**
4163 * Get the value of the tabindex.
4164 *
4165 * @return {number|null} Tabindex value
4166 */
4167 OO.ui.mixin.TabIndexedElement.prototype.getTabIndex = function () {
4168 return this.tabIndex;
4169 };
4170
4171 /**
4172 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
4173 * interface element that can be configured with access keys for accessibility.
4174 * See the [OOjs UI documentation on MediaWiki] [1] for examples.
4175 *
4176 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
4177 * @abstract
4178 * @class
4179 *
4180 * @constructor
4181 * @param {Object} [config] Configuration options
4182 * @cfg {jQuery} [$button] The button element created by the class.
4183 * If this configuration is omitted, the button element will use a generated `<a>`.
4184 * @cfg {boolean} [framed=true] Render the button with a frame
4185 * @cfg {string} [accessKey] Button's access key
4186 */
4187 OO.ui.mixin.ButtonElement = function OoUiMixinButtonElement( config ) {
4188 // Configuration initialization
4189 config = config || {};
4190
4191 // Properties
4192 this.$button = null;
4193 this.framed = null;
4194 this.accessKey = null;
4195 this.active = false;
4196 this.onMouseUpHandler = this.onMouseUp.bind( this );
4197 this.onMouseDownHandler = this.onMouseDown.bind( this );
4198 this.onKeyDownHandler = this.onKeyDown.bind( this );
4199 this.onKeyUpHandler = this.onKeyUp.bind( this );
4200 this.onClickHandler = this.onClick.bind( this );
4201 this.onKeyPressHandler = this.onKeyPress.bind( this );
4202
4203 // Initialization
4204 this.$element.addClass( 'oo-ui-buttonElement' );
4205 this.toggleFramed( config.framed === undefined || config.framed );
4206 this.setAccessKey( config.accessKey );
4207 this.setButtonElement( config.$button || $( '<a>' ) );
4208 };
4209
4210 /* Setup */
4211
4212 OO.initClass( OO.ui.mixin.ButtonElement );
4213
4214 /* Static Properties */
4215
4216 /**
4217 * Cancel mouse down events.
4218 *
4219 * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
4220 * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
4221 * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
4222 * parent widget.
4223 *
4224 * @static
4225 * @inheritable
4226 * @property {boolean}
4227 */
4228 OO.ui.mixin.ButtonElement.static.cancelButtonMouseDownEvents = true;
4229
4230 /* Events */
4231
4232 /**
4233 * A 'click' event is emitted when the button element is clicked.
4234 *
4235 * @event click
4236 */
4237
4238 /* Methods */
4239
4240 /**
4241 * Set the button element.
4242 *
4243 * This method is used to retarget a button mixin so that its functionality applies to
4244 * the specified button element instead of the one created by the class. If a button element
4245 * is already set, the method will remove the mixin’s effect on that element.
4246 *
4247 * @param {jQuery} $button Element to use as button
4248 */
4249 OO.ui.mixin.ButtonElement.prototype.setButtonElement = function ( $button ) {
4250 if ( this.$button ) {
4251 this.$button
4252 .removeClass( 'oo-ui-buttonElement-button' )
4253 .removeAttr( 'role accesskey' )
4254 .off( {
4255 mousedown: this.onMouseDownHandler,
4256 keydown: this.onKeyDownHandler,
4257 click: this.onClickHandler,
4258 keypress: this.onKeyPressHandler
4259 } );
4260 }
4261
4262 this.$button = $button
4263 .addClass( 'oo-ui-buttonElement-button' )
4264 .attr( { role: 'button', accesskey: this.accessKey } )
4265 .on( {
4266 mousedown: this.onMouseDownHandler,
4267 keydown: this.onKeyDownHandler,
4268 click: this.onClickHandler,
4269 keypress: this.onKeyPressHandler
4270 } );
4271 };
4272
4273 /**
4274 * Handles mouse down events.
4275 *
4276 * @protected
4277 * @param {jQuery.Event} e Mouse down event
4278 */
4279 OO.ui.mixin.ButtonElement.prototype.onMouseDown = function ( e ) {
4280 if ( this.isDisabled() || e.which !== 1 ) {
4281 return;
4282 }
4283 this.$element.addClass( 'oo-ui-buttonElement-pressed' );
4284 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
4285 // reliably remove the pressed class
4286 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
4287 // Prevent change of focus unless specifically configured otherwise
4288 if ( this.constructor.static.cancelButtonMouseDownEvents ) {
4289 return false;
4290 }
4291 };
4292
4293 /**
4294 * Handles mouse up events.
4295 *
4296 * @protected
4297 * @param {jQuery.Event} e Mouse up event
4298 */
4299 OO.ui.mixin.ButtonElement.prototype.onMouseUp = function ( e ) {
4300 if ( this.isDisabled() || e.which !== 1 ) {
4301 return;
4302 }
4303 this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
4304 // Stop listening for mouseup, since we only needed this once
4305 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
4306 };
4307
4308 /**
4309 * Handles mouse click events.
4310 *
4311 * @protected
4312 * @param {jQuery.Event} e Mouse click event
4313 * @fires click
4314 */
4315 OO.ui.mixin.ButtonElement.prototype.onClick = function ( e ) {
4316 if ( !this.isDisabled() && e.which === 1 ) {
4317 if ( this.emit( 'click' ) ) {
4318 return false;
4319 }
4320 }
4321 };
4322
4323 /**
4324 * Handles key down events.
4325 *
4326 * @protected
4327 * @param {jQuery.Event} e Key down event
4328 */
4329 OO.ui.mixin.ButtonElement.prototype.onKeyDown = function ( e ) {
4330 if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
4331 return;
4332 }
4333 this.$element.addClass( 'oo-ui-buttonElement-pressed' );
4334 // Run the keyup handler no matter where the key is when the button is let go, so we can
4335 // reliably remove the pressed class
4336 this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler, true );
4337 };
4338
4339 /**
4340 * Handles key up events.
4341 *
4342 * @protected
4343 * @param {jQuery.Event} e Key up event
4344 */
4345 OO.ui.mixin.ButtonElement.prototype.onKeyUp = function ( e ) {
4346 if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
4347 return;
4348 }
4349 this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
4350 // Stop listening for keyup, since we only needed this once
4351 this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler, true );
4352 };
4353
4354 /**
4355 * Handles key press events.
4356 *
4357 * @protected
4358 * @param {jQuery.Event} e Key press event
4359 * @fires click
4360 */
4361 OO.ui.mixin.ButtonElement.prototype.onKeyPress = function ( e ) {
4362 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
4363 if ( this.emit( 'click' ) ) {
4364 return false;
4365 }
4366 }
4367 };
4368
4369 /**
4370 * Check if button has a frame.
4371 *
4372 * @return {boolean} Button is framed
4373 */
4374 OO.ui.mixin.ButtonElement.prototype.isFramed = function () {
4375 return this.framed;
4376 };
4377
4378 /**
4379 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
4380 *
4381 * @param {boolean} [framed] Make button framed, omit to toggle
4382 * @chainable
4383 */
4384 OO.ui.mixin.ButtonElement.prototype.toggleFramed = function ( framed ) {
4385 framed = framed === undefined ? !this.framed : !!framed;
4386 if ( framed !== this.framed ) {
4387 this.framed = framed;
4388 this.$element
4389 .toggleClass( 'oo-ui-buttonElement-frameless', !framed )
4390 .toggleClass( 'oo-ui-buttonElement-framed', framed );
4391 this.updateThemeClasses();
4392 }
4393
4394 return this;
4395 };
4396
4397 /**
4398 * Set the button's access key.
4399 *
4400 * @param {string} accessKey Button's access key, use empty string to remove
4401 * @chainable
4402 */
4403 OO.ui.mixin.ButtonElement.prototype.setAccessKey = function ( accessKey ) {
4404 accessKey = typeof accessKey === 'string' && accessKey.length ? accessKey : null;
4405
4406 if ( this.accessKey !== accessKey ) {
4407 if ( this.$button ) {
4408 if ( accessKey !== null ) {
4409 this.$button.attr( 'accesskey', accessKey );
4410 } else {
4411 this.$button.removeAttr( 'accesskey' );
4412 }
4413 }
4414 this.accessKey = accessKey;
4415 }
4416
4417 return this;
4418 };
4419
4420 /**
4421 * Set the button to its 'active' state.
4422 *
4423 * The active state occurs when a {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} or
4424 * a {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} is pressed. This method does nothing
4425 * for other button types.
4426 *
4427 * @param {boolean} [value] Make button active
4428 * @chainable
4429 */
4430 OO.ui.mixin.ButtonElement.prototype.setActive = function ( value ) {
4431 this.$element.toggleClass( 'oo-ui-buttonElement-active', !!value );
4432 return this;
4433 };
4434
4435 /**
4436 * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
4437 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
4438 * items from the group is done through the interface the class provides.
4439 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
4440 *
4441 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
4442 *
4443 * @abstract
4444 * @class
4445 *
4446 * @constructor
4447 * @param {Object} [config] Configuration options
4448 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
4449 * is omitted, the group element will use a generated `<div>`.
4450 */
4451 OO.ui.mixin.GroupElement = function OoUiMixinGroupElement( config ) {
4452 // Configuration initialization
4453 config = config || {};
4454
4455 // Properties
4456 this.$group = null;
4457 this.items = [];
4458 this.aggregateItemEvents = {};
4459
4460 // Initialization
4461 this.setGroupElement( config.$group || $( '<div>' ) );
4462 };
4463
4464 /* Methods */
4465
4466 /**
4467 * Set the group element.
4468 *
4469 * If an element is already set, items will be moved to the new element.
4470 *
4471 * @param {jQuery} $group Element to use as group
4472 */
4473 OO.ui.mixin.GroupElement.prototype.setGroupElement = function ( $group ) {
4474 var i, len;
4475
4476 this.$group = $group;
4477 for ( i = 0, len = this.items.length; i < len; i++ ) {
4478 this.$group.append( this.items[ i ].$element );
4479 }
4480 };
4481
4482 /**
4483 * Check if a group contains no items.
4484 *
4485 * @return {boolean} Group is empty
4486 */
4487 OO.ui.mixin.GroupElement.prototype.isEmpty = function () {
4488 return !this.items.length;
4489 };
4490
4491 /**
4492 * Get all items in the group.
4493 *
4494 * The method returns an array of item references (e.g., [button1, button2, button3]) and is useful
4495 * when synchronizing groups of items, or whenever the references are required (e.g., when removing items
4496 * from a group).
4497 *
4498 * @return {OO.ui.Element[]} An array of items.
4499 */
4500 OO.ui.mixin.GroupElement.prototype.getItems = function () {
4501 return this.items.slice( 0 );
4502 };
4503
4504 /**
4505 * Get an item by its data.
4506 *
4507 * Only the first item with matching data will be returned. To return all matching items,
4508 * use the #getItemsFromData method.
4509 *
4510 * @param {Object} data Item data to search for
4511 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
4512 */
4513 OO.ui.mixin.GroupElement.prototype.getItemFromData = function ( data ) {
4514 var i, len, item,
4515 hash = OO.getHash( data );
4516
4517 for ( i = 0, len = this.items.length; i < len; i++ ) {
4518 item = this.items[ i ];
4519 if ( hash === OO.getHash( item.getData() ) ) {
4520 return item;
4521 }
4522 }
4523
4524 return null;
4525 };
4526
4527 /**
4528 * Get items by their data.
4529 *
4530 * All items with matching data will be returned. To return only the first match, use the #getItemFromData method instead.
4531 *
4532 * @param {Object} data Item data to search for
4533 * @return {OO.ui.Element[]} Items with equivalent data
4534 */
4535 OO.ui.mixin.GroupElement.prototype.getItemsFromData = function ( data ) {
4536 var i, len, item,
4537 hash = OO.getHash( data ),
4538 items = [];
4539
4540 for ( i = 0, len = this.items.length; i < len; i++ ) {
4541 item = this.items[ i ];
4542 if ( hash === OO.getHash( item.getData() ) ) {
4543 items.push( item );
4544 }
4545 }
4546
4547 return items;
4548 };
4549
4550 /**
4551 * Aggregate the events emitted by the group.
4552 *
4553 * When events are aggregated, the group will listen to all contained items for the event,
4554 * and then emit the event under a new name. The new event will contain an additional leading
4555 * parameter containing the item that emitted the original event. Other arguments emitted from
4556 * the original event are passed through.
4557 *
4558 * @param {Object.<string,string|null>} events An object keyed by the name of the event that should be
4559 * aggregated (e.g., ‘click’) and the value of the new name to use (e.g., ‘groupClick’).
4560 * A `null` value will remove aggregated events.
4561
4562 * @throws {Error} An error is thrown if aggregation already exists.
4563 */
4564 OO.ui.mixin.GroupElement.prototype.aggregate = function ( events ) {
4565 var i, len, item, add, remove, itemEvent, groupEvent;
4566
4567 for ( itemEvent in events ) {
4568 groupEvent = events[ itemEvent ];
4569
4570 // Remove existing aggregated event
4571 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4572 // Don't allow duplicate aggregations
4573 if ( groupEvent ) {
4574 throw new Error( 'Duplicate item event aggregation for ' + itemEvent );
4575 }
4576 // Remove event aggregation from existing items
4577 for ( i = 0, len = this.items.length; i < len; i++ ) {
4578 item = this.items[ i ];
4579 if ( item.connect && item.disconnect ) {
4580 remove = {};
4581 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[itemEvent], item ];
4582 item.disconnect( this, remove );
4583 }
4584 }
4585 // Prevent future items from aggregating event
4586 delete this.aggregateItemEvents[ itemEvent ];
4587 }
4588
4589 // Add new aggregate event
4590 if ( groupEvent ) {
4591 // Make future items aggregate event
4592 this.aggregateItemEvents[ itemEvent ] = groupEvent;
4593 // Add event aggregation to existing items
4594 for ( i = 0, len = this.items.length; i < len; i++ ) {
4595 item = this.items[ i ];
4596 if ( item.connect && item.disconnect ) {
4597 add = {};
4598 add[ itemEvent ] = [ 'emit', groupEvent, item ];
4599 item.connect( this, add );
4600 }
4601 }
4602 }
4603 }
4604 };
4605
4606 /**
4607 * Add items to the group.
4608 *
4609 * Items will be added to the end of the group array unless the optional `index` parameter specifies
4610 * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
4611 *
4612 * @param {OO.ui.Element[]} items An array of items to add to the group
4613 * @param {number} [index] Index of the insertion point
4614 * @chainable
4615 */
4616 OO.ui.mixin.GroupElement.prototype.addItems = function ( items, index ) {
4617 var i, len, item, event, events, currentIndex,
4618 itemElements = [];
4619
4620 for ( i = 0, len = items.length; i < len; i++ ) {
4621 item = items[ i ];
4622
4623 // Check if item exists then remove it first, effectively "moving" it
4624 currentIndex = $.inArray( item, this.items );
4625 if ( currentIndex >= 0 ) {
4626 this.removeItems( [ item ] );
4627 // Adjust index to compensate for removal
4628 if ( currentIndex < index ) {
4629 index--;
4630 }
4631 }
4632 // Add the item
4633 if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
4634 events = {};
4635 for ( event in this.aggregateItemEvents ) {
4636 events[ event ] = [ 'emit', this.aggregateItemEvents[ event ], item ];
4637 }
4638 item.connect( this, events );
4639 }
4640 item.setElementGroup( this );
4641 itemElements.push( item.$element.get( 0 ) );
4642 }
4643
4644 if ( index === undefined || index < 0 || index >= this.items.length ) {
4645 this.$group.append( itemElements );
4646 this.items.push.apply( this.items, items );
4647 } else if ( index === 0 ) {
4648 this.$group.prepend( itemElements );
4649 this.items.unshift.apply( this.items, items );
4650 } else {
4651 this.items[ index ].$element.before( itemElements );
4652 this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
4653 }
4654
4655 return this;
4656 };
4657
4658 /**
4659 * Remove the specified items from a group.
4660 *
4661 * Removed items are detached (not removed) from the DOM so that they may be reused.
4662 * To remove all items from a group, you may wish to use the #clearItems method instead.
4663 *
4664 * @param {OO.ui.Element[]} items An array of items to remove
4665 * @chainable
4666 */
4667 OO.ui.mixin.GroupElement.prototype.removeItems = function ( items ) {
4668 var i, len, item, index, remove, itemEvent;
4669
4670 // Remove specific items
4671 for ( i = 0, len = items.length; i < len; i++ ) {
4672 item = items[ i ];
4673 index = $.inArray( item, this.items );
4674 if ( index !== -1 ) {
4675 if (
4676 item.connect && item.disconnect &&
4677 !$.isEmptyObject( this.aggregateItemEvents )
4678 ) {
4679 remove = {};
4680 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4681 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
4682 }
4683 item.disconnect( this, remove );
4684 }
4685 item.setElementGroup( null );
4686 this.items.splice( index, 1 );
4687 item.$element.detach();
4688 }
4689 }
4690
4691 return this;
4692 };
4693
4694 /**
4695 * Clear all items from the group.
4696 *
4697 * Cleared items are detached from the DOM, not removed, so that they may be reused.
4698 * To remove only a subset of items from a group, use the #removeItems method.
4699 *
4700 * @chainable
4701 */
4702 OO.ui.mixin.GroupElement.prototype.clearItems = function () {
4703 var i, len, item, remove, itemEvent;
4704
4705 // Remove all items
4706 for ( i = 0, len = this.items.length; i < len; i++ ) {
4707 item = this.items[ i ];
4708 if (
4709 item.connect && item.disconnect &&
4710 !$.isEmptyObject( this.aggregateItemEvents )
4711 ) {
4712 remove = {};
4713 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4714 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
4715 }
4716 item.disconnect( this, remove );
4717 }
4718 item.setElementGroup( null );
4719 item.$element.detach();
4720 }
4721
4722 this.items = [];
4723 return this;
4724 };
4725
4726 /**
4727 * DraggableElement is a mixin class used to create elements that can be clicked
4728 * and dragged by a mouse to a new position within a group. This class must be used
4729 * in conjunction with OO.ui.mixin.DraggableGroupElement, which provides a container for
4730 * the draggable elements.
4731 *
4732 * @abstract
4733 * @class
4734 *
4735 * @constructor
4736 */
4737 OO.ui.mixin.DraggableElement = function OoUiMixinDraggableElement() {
4738 // Properties
4739 this.index = null;
4740
4741 // Initialize and events
4742 this.$element
4743 .attr( 'draggable', true )
4744 .addClass( 'oo-ui-draggableElement' )
4745 .on( {
4746 dragstart: this.onDragStart.bind( this ),
4747 dragover: this.onDragOver.bind( this ),
4748 dragend: this.onDragEnd.bind( this ),
4749 drop: this.onDrop.bind( this )
4750 } );
4751 };
4752
4753 OO.initClass( OO.ui.mixin.DraggableElement );
4754
4755 /* Events */
4756
4757 /**
4758 * @event dragstart
4759 *
4760 * A dragstart event is emitted when the user clicks and begins dragging an item.
4761 * @param {OO.ui.mixin.DraggableElement} item The item the user has clicked and is dragging with the mouse.
4762 */
4763
4764 /**
4765 * @event dragend
4766 * A dragend event is emitted when the user drags an item and releases the mouse,
4767 * thus terminating the drag operation.
4768 */
4769
4770 /**
4771 * @event drop
4772 * A drop event is emitted when the user drags an item and then releases the mouse button
4773 * over a valid target.
4774 */
4775
4776 /* Static Properties */
4777
4778 /**
4779 * @inheritdoc OO.ui.mixin.ButtonElement
4780 */
4781 OO.ui.mixin.DraggableElement.static.cancelButtonMouseDownEvents = false;
4782
4783 /* Methods */
4784
4785 /**
4786 * Respond to dragstart event.
4787 *
4788 * @private
4789 * @param {jQuery.Event} event jQuery event
4790 * @fires dragstart
4791 */
4792 OO.ui.mixin.DraggableElement.prototype.onDragStart = function ( e ) {
4793 var dataTransfer = e.originalEvent.dataTransfer;
4794 // Define drop effect
4795 dataTransfer.dropEffect = 'none';
4796 dataTransfer.effectAllowed = 'move';
4797 // We must set up a dataTransfer data property or Firefox seems to
4798 // ignore the fact the element is draggable.
4799 try {
4800 dataTransfer.setData( 'application-x/OOjs-UI-draggable', this.getIndex() );
4801 } catch ( err ) {
4802 // The above is only for firefox. No need to set a catch clause
4803 // if it fails, move on.
4804 }
4805 // Add dragging class
4806 this.$element.addClass( 'oo-ui-draggableElement-dragging' );
4807 // Emit event
4808 this.emit( 'dragstart', this );
4809 return true;
4810 };
4811
4812 /**
4813 * Respond to dragend event.
4814 *
4815 * @private
4816 * @fires dragend
4817 */
4818 OO.ui.mixin.DraggableElement.prototype.onDragEnd = function () {
4819 this.$element.removeClass( 'oo-ui-draggableElement-dragging' );
4820 this.emit( 'dragend' );
4821 };
4822
4823 /**
4824 * Handle drop event.
4825 *
4826 * @private
4827 * @param {jQuery.Event} event jQuery event
4828 * @fires drop
4829 */
4830 OO.ui.mixin.DraggableElement.prototype.onDrop = function ( e ) {
4831 e.preventDefault();
4832 this.emit( 'drop', e );
4833 };
4834
4835 /**
4836 * In order for drag/drop to work, the dragover event must
4837 * return false and stop propogation.
4838 *
4839 * @private
4840 */
4841 OO.ui.mixin.DraggableElement.prototype.onDragOver = function ( e ) {
4842 e.preventDefault();
4843 };
4844
4845 /**
4846 * Set item index.
4847 * Store it in the DOM so we can access from the widget drag event
4848 *
4849 * @private
4850 * @param {number} Item index
4851 */
4852 OO.ui.mixin.DraggableElement.prototype.setIndex = function ( index ) {
4853 if ( this.index !== index ) {
4854 this.index = index;
4855 this.$element.data( 'index', index );
4856 }
4857 };
4858
4859 /**
4860 * Get item index
4861 *
4862 * @private
4863 * @return {number} Item index
4864 */
4865 OO.ui.mixin.DraggableElement.prototype.getIndex = function () {
4866 return this.index;
4867 };
4868
4869 /**
4870 * DraggableGroupElement is a mixin class used to create a group element to
4871 * contain draggable elements, which are items that can be clicked and dragged by a mouse.
4872 * The class is used with OO.ui.mixin.DraggableElement.
4873 *
4874 * @abstract
4875 * @class
4876 * @mixins OO.ui.mixin.GroupElement
4877 *
4878 * @constructor
4879 * @param {Object} [config] Configuration options
4880 * @cfg {string} [orientation] Item orientation: 'horizontal' or 'vertical'. The orientation
4881 * should match the layout of the items. Items displayed in a single row
4882 * or in several rows should use horizontal orientation. The vertical orientation should only be
4883 * used when the items are displayed in a single column. Defaults to 'vertical'
4884 */
4885 OO.ui.mixin.DraggableGroupElement = function OoUiMixinDraggableGroupElement( config ) {
4886 // Configuration initialization
4887 config = config || {};
4888
4889 // Parent constructor
4890 OO.ui.mixin.GroupElement.call( this, config );
4891
4892 // Properties
4893 this.orientation = config.orientation || 'vertical';
4894 this.dragItem = null;
4895 this.itemDragOver = null;
4896 this.itemKeys = {};
4897 this.sideInsertion = '';
4898
4899 // Events
4900 this.aggregate( {
4901 dragstart: 'itemDragStart',
4902 dragend: 'itemDragEnd',
4903 drop: 'itemDrop'
4904 } );
4905 this.connect( this, {
4906 itemDragStart: 'onItemDragStart',
4907 itemDrop: 'onItemDrop',
4908 itemDragEnd: 'onItemDragEnd'
4909 } );
4910 this.$element.on( {
4911 dragover: $.proxy( this.onDragOver, this ),
4912 dragleave: $.proxy( this.onDragLeave, this )
4913 } );
4914
4915 // Initialize
4916 if ( Array.isArray( config.items ) ) {
4917 this.addItems( config.items );
4918 }
4919 this.$placeholder = $( '<div>' )
4920 .addClass( 'oo-ui-draggableGroupElement-placeholder' );
4921 this.$element
4922 .addClass( 'oo-ui-draggableGroupElement' )
4923 .append( this.$status )
4924 .toggleClass( 'oo-ui-draggableGroupElement-horizontal', this.orientation === 'horizontal' )
4925 .prepend( this.$placeholder );
4926 };
4927
4928 /* Setup */
4929 OO.mixinClass( OO.ui.mixin.DraggableGroupElement, OO.ui.mixin.GroupElement );
4930
4931 /* Events */
4932
4933 /**
4934 * A 'reorder' event is emitted when the order of items in the group changes.
4935 *
4936 * @event reorder
4937 * @param {OO.ui.mixin.DraggableElement} item Reordered item
4938 * @param {number} [newIndex] New index for the item
4939 */
4940
4941 /* Methods */
4942
4943 /**
4944 * Respond to item drag start event
4945 *
4946 * @private
4947 * @param {OO.ui.mixin.DraggableElement} item Dragged item
4948 */
4949 OO.ui.mixin.DraggableGroupElement.prototype.onItemDragStart = function ( item ) {
4950 var i, len;
4951
4952 // Map the index of each object
4953 for ( i = 0, len = this.items.length; i < len; i++ ) {
4954 this.items[ i ].setIndex( i );
4955 }
4956
4957 if ( this.orientation === 'horizontal' ) {
4958 // Set the height of the indicator
4959 this.$placeholder.css( {
4960 height: item.$element.outerHeight(),
4961 width: 2
4962 } );
4963 } else {
4964 // Set the width of the indicator
4965 this.$placeholder.css( {
4966 height: 2,
4967 width: item.$element.outerWidth()
4968 } );
4969 }
4970 this.setDragItem( item );
4971 };
4972
4973 /**
4974 * Respond to item drag end event
4975 *
4976 * @private
4977 */
4978 OO.ui.mixin.DraggableGroupElement.prototype.onItemDragEnd = function () {
4979 this.unsetDragItem();
4980 return false;
4981 };
4982
4983 /**
4984 * Handle drop event and switch the order of the items accordingly
4985 *
4986 * @private
4987 * @param {OO.ui.mixin.DraggableElement} item Dropped item
4988 * @fires reorder
4989 */
4990 OO.ui.mixin.DraggableGroupElement.prototype.onItemDrop = function ( item ) {
4991 var toIndex = item.getIndex();
4992 // Check if the dropped item is from the current group
4993 // TODO: Figure out a way to configure a list of legally droppable
4994 // elements even if they are not yet in the list
4995 if ( this.getDragItem() ) {
4996 // If the insertion point is 'after', the insertion index
4997 // is shifted to the right (or to the left in RTL, hence 'after')
4998 if ( this.sideInsertion === 'after' ) {
4999 toIndex++;
5000 }
5001 // Emit change event
5002 this.emit( 'reorder', this.getDragItem(), toIndex );
5003 }
5004 this.unsetDragItem();
5005 // Return false to prevent propogation
5006 return false;
5007 };
5008
5009 /**
5010 * Handle dragleave event.
5011 *
5012 * @private
5013 */
5014 OO.ui.mixin.DraggableGroupElement.prototype.onDragLeave = function () {
5015 // This means the item was dragged outside the widget
5016 this.$placeholder
5017 .css( 'left', 0 )
5018 .addClass( 'oo-ui-element-hidden' );
5019 };
5020
5021 /**
5022 * Respond to dragover event
5023 *
5024 * @private
5025 * @param {jQuery.Event} event Event details
5026 */
5027 OO.ui.mixin.DraggableGroupElement.prototype.onDragOver = function ( e ) {
5028 var dragOverObj, $optionWidget, itemOffset, itemMidpoint, itemBoundingRect,
5029 itemSize, cssOutput, dragPosition, itemIndex, itemPosition,
5030 clientX = e.originalEvent.clientX,
5031 clientY = e.originalEvent.clientY;
5032
5033 // Get the OptionWidget item we are dragging over
5034 dragOverObj = this.getElementDocument().elementFromPoint( clientX, clientY );
5035 $optionWidget = $( dragOverObj ).closest( '.oo-ui-draggableElement' );
5036 if ( $optionWidget[ 0 ] ) {
5037 itemOffset = $optionWidget.offset();
5038 itemBoundingRect = $optionWidget[ 0 ].getBoundingClientRect();
5039 itemPosition = $optionWidget.position();
5040 itemIndex = $optionWidget.data( 'index' );
5041 }
5042
5043 if (
5044 itemOffset &&
5045 this.isDragging() &&
5046 itemIndex !== this.getDragItem().getIndex()
5047 ) {
5048 if ( this.orientation === 'horizontal' ) {
5049 // Calculate where the mouse is relative to the item width
5050 itemSize = itemBoundingRect.width;
5051 itemMidpoint = itemBoundingRect.left + itemSize / 2;
5052 dragPosition = clientX;
5053 // Which side of the item we hover over will dictate
5054 // where the placeholder will appear, on the left or
5055 // on the right
5056 cssOutput = {
5057 left: dragPosition < itemMidpoint ? itemPosition.left : itemPosition.left + itemSize,
5058 top: itemPosition.top
5059 };
5060 } else {
5061 // Calculate where the mouse is relative to the item height
5062 itemSize = itemBoundingRect.height;
5063 itemMidpoint = itemBoundingRect.top + itemSize / 2;
5064 dragPosition = clientY;
5065 // Which side of the item we hover over will dictate
5066 // where the placeholder will appear, on the top or
5067 // on the bottom
5068 cssOutput = {
5069 top: dragPosition < itemMidpoint ? itemPosition.top : itemPosition.top + itemSize,
5070 left: itemPosition.left
5071 };
5072 }
5073 // Store whether we are before or after an item to rearrange
5074 // For horizontal layout, we need to account for RTL, as this is flipped
5075 if ( this.orientation === 'horizontal' && this.$element.css( 'direction' ) === 'rtl' ) {
5076 this.sideInsertion = dragPosition < itemMidpoint ? 'after' : 'before';
5077 } else {
5078 this.sideInsertion = dragPosition < itemMidpoint ? 'before' : 'after';
5079 }
5080 // Add drop indicator between objects
5081 this.$placeholder
5082 .css( cssOutput )
5083 .removeClass( 'oo-ui-element-hidden' );
5084 } else {
5085 // This means the item was dragged outside the widget
5086 this.$placeholder
5087 .css( 'left', 0 )
5088 .addClass( 'oo-ui-element-hidden' );
5089 }
5090 // Prevent default
5091 e.preventDefault();
5092 };
5093
5094 /**
5095 * Set a dragged item
5096 *
5097 * @param {OO.ui.mixin.DraggableElement} item Dragged item
5098 */
5099 OO.ui.mixin.DraggableGroupElement.prototype.setDragItem = function ( item ) {
5100 this.dragItem = item;
5101 };
5102
5103 /**
5104 * Unset the current dragged item
5105 */
5106 OO.ui.mixin.DraggableGroupElement.prototype.unsetDragItem = function () {
5107 this.dragItem = null;
5108 this.itemDragOver = null;
5109 this.$placeholder.addClass( 'oo-ui-element-hidden' );
5110 this.sideInsertion = '';
5111 };
5112
5113 /**
5114 * Get the item that is currently being dragged.
5115 *
5116 * @return {OO.ui.mixin.DraggableElement|null} The currently dragged item, or `null` if no item is being dragged
5117 */
5118 OO.ui.mixin.DraggableGroupElement.prototype.getDragItem = function () {
5119 return this.dragItem;
5120 };
5121
5122 /**
5123 * Check if an item in the group is currently being dragged.
5124 *
5125 * @return {Boolean} Item is being dragged
5126 */
5127 OO.ui.mixin.DraggableGroupElement.prototype.isDragging = function () {
5128 return this.getDragItem() !== null;
5129 };
5130
5131 /**
5132 * IconElement is often mixed into other classes to generate an icon.
5133 * Icons are graphics, about the size of normal text. They are used to aid the user
5134 * in locating a control or to convey information in a space-efficient way. See the
5135 * [OOjs UI documentation on MediaWiki] [1] for a list of icons
5136 * included in the library.
5137 *
5138 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
5139 *
5140 * @abstract
5141 * @class
5142 *
5143 * @constructor
5144 * @param {Object} [config] Configuration options
5145 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
5146 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
5147 * the icon element be set to an existing icon instead of the one generated by this class, set a
5148 * value using a jQuery selection. For example:
5149 *
5150 * // Use a <div> tag instead of a <span>
5151 * $icon: $("<div>")
5152 * // Use an existing icon element instead of the one generated by the class
5153 * $icon: this.$element
5154 * // Use an icon element from a child widget
5155 * $icon: this.childwidget.$element
5156 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
5157 * symbolic names. A map is used for i18n purposes and contains a `default` icon
5158 * name and additional names keyed by language code. The `default` name is used when no icon is keyed
5159 * by the user's language.
5160 *
5161 * Example of an i18n map:
5162 *
5163 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
5164 * See the [OOjs UI documentation on MediaWiki] [2] for a list of icons included in the library.
5165 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
5166 * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
5167 * text. The icon title is displayed when users move the mouse over the icon.
5168 */
5169 OO.ui.mixin.IconElement = function OoUiMixinIconElement( config ) {
5170 // Configuration initialization
5171 config = config || {};
5172
5173 // Properties
5174 this.$icon = null;
5175 this.icon = null;
5176 this.iconTitle = null;
5177
5178 // Initialization
5179 this.setIcon( config.icon || this.constructor.static.icon );
5180 this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle );
5181 this.setIconElement( config.$icon || $( '<span>' ) );
5182 };
5183
5184 /* Setup */
5185
5186 OO.initClass( OO.ui.mixin.IconElement );
5187
5188 /* Static Properties */
5189
5190 /**
5191 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
5192 * for i18n purposes and contains a `default` icon name and additional names keyed by
5193 * language code. The `default` name is used when no icon is keyed by the user's language.
5194 *
5195 * Example of an i18n map:
5196 *
5197 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
5198 *
5199 * Note: the static property will be overridden if the #icon configuration is used.
5200 *
5201 * @static
5202 * @inheritable
5203 * @property {Object|string}
5204 */
5205 OO.ui.mixin.IconElement.static.icon = null;
5206
5207 /**
5208 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
5209 * function that returns title text, or `null` for no title.
5210 *
5211 * The static property will be overridden if the #iconTitle configuration is used.
5212 *
5213 * @static
5214 * @inheritable
5215 * @property {string|Function|null}
5216 */
5217 OO.ui.mixin.IconElement.static.iconTitle = null;
5218
5219 /* Methods */
5220
5221 /**
5222 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
5223 * applies to the specified icon element instead of the one created by the class. If an icon
5224 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
5225 * and mixin methods will no longer affect the element.
5226 *
5227 * @param {jQuery} $icon Element to use as icon
5228 */
5229 OO.ui.mixin.IconElement.prototype.setIconElement = function ( $icon ) {
5230 if ( this.$icon ) {
5231 this.$icon
5232 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon )
5233 .removeAttr( 'title' );
5234 }
5235
5236 this.$icon = $icon
5237 .addClass( 'oo-ui-iconElement-icon' )
5238 .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon );
5239 if ( this.iconTitle !== null ) {
5240 this.$icon.attr( 'title', this.iconTitle );
5241 }
5242 };
5243
5244 /**
5245 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
5246 * The icon parameter can also be set to a map of icon names. See the #icon config setting
5247 * for an example.
5248 *
5249 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
5250 * by language code, or `null` to remove the icon.
5251 * @chainable
5252 */
5253 OO.ui.mixin.IconElement.prototype.setIcon = function ( icon ) {
5254 icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
5255 icon = typeof icon === 'string' && icon.trim().length ? icon.trim() : null;
5256
5257 if ( this.icon !== icon ) {
5258 if ( this.$icon ) {
5259 if ( this.icon !== null ) {
5260 this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
5261 }
5262 if ( icon !== null ) {
5263 this.$icon.addClass( 'oo-ui-icon-' + icon );
5264 }
5265 }
5266 this.icon = icon;
5267 }
5268
5269 this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon );
5270 this.updateThemeClasses();
5271
5272 return this;
5273 };
5274
5275 /**
5276 * Set the icon title. Use `null` to remove the title.
5277 *
5278 * @param {string|Function|null} iconTitle A text string used as the icon title,
5279 * a function that returns title text, or `null` for no title.
5280 * @chainable
5281 */
5282 OO.ui.mixin.IconElement.prototype.setIconTitle = function ( iconTitle ) {
5283 iconTitle = typeof iconTitle === 'function' ||
5284 ( typeof iconTitle === 'string' && iconTitle.length ) ?
5285 OO.ui.resolveMsg( iconTitle ) : null;
5286
5287 if ( this.iconTitle !== iconTitle ) {
5288 this.iconTitle = iconTitle;
5289 if ( this.$icon ) {
5290 if ( this.iconTitle !== null ) {
5291 this.$icon.attr( 'title', iconTitle );
5292 } else {
5293 this.$icon.removeAttr( 'title' );
5294 }
5295 }
5296 }
5297
5298 return this;
5299 };
5300
5301 /**
5302 * Get the symbolic name of the icon.
5303 *
5304 * @return {string} Icon name
5305 */
5306 OO.ui.mixin.IconElement.prototype.getIcon = function () {
5307 return this.icon;
5308 };
5309
5310 /**
5311 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
5312 *
5313 * @return {string} Icon title text
5314 */
5315 OO.ui.mixin.IconElement.prototype.getIconTitle = function () {
5316 return this.iconTitle;
5317 };
5318
5319 /**
5320 * IndicatorElement is often mixed into other classes to generate an indicator.
5321 * Indicators are small graphics that are generally used in two ways:
5322 *
5323 * - To draw attention to the status of an item. For example, an indicator might be
5324 * used to show that an item in a list has errors that need to be resolved.
5325 * - To clarify the function of a control that acts in an exceptional way (a button
5326 * that opens a menu instead of performing an action directly, for example).
5327 *
5328 * For a list of indicators included in the library, please see the
5329 * [OOjs UI documentation on MediaWiki] [1].
5330 *
5331 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
5332 *
5333 * @abstract
5334 * @class
5335 *
5336 * @constructor
5337 * @param {Object} [config] Configuration options
5338 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
5339 * configuration is omitted, the indicator element will use a generated `<span>`.
5340 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
5341 * See the [OOjs UI documentation on MediaWiki][2] for a list of indicators included
5342 * in the library.
5343 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
5344 * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
5345 * or a function that returns title text. The indicator title is displayed when users move
5346 * the mouse over the indicator.
5347 */
5348 OO.ui.mixin.IndicatorElement = function OoUiMixinIndicatorElement( config ) {
5349 // Configuration initialization
5350 config = config || {};
5351
5352 // Properties
5353 this.$indicator = null;
5354 this.indicator = null;
5355 this.indicatorTitle = null;
5356
5357 // Initialization
5358 this.setIndicator( config.indicator || this.constructor.static.indicator );
5359 this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle );
5360 this.setIndicatorElement( config.$indicator || $( '<span>' ) );
5361 };
5362
5363 /* Setup */
5364
5365 OO.initClass( OO.ui.mixin.IndicatorElement );
5366
5367 /* Static Properties */
5368
5369 /**
5370 * Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
5371 * The static property will be overridden if the #indicator configuration is used.
5372 *
5373 * @static
5374 * @inheritable
5375 * @property {string|null}
5376 */
5377 OO.ui.mixin.IndicatorElement.static.indicator = null;
5378
5379 /**
5380 * A text string used as the indicator title, a function that returns title text, or `null`
5381 * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
5382 *
5383 * @static
5384 * @inheritable
5385 * @property {string|Function|null}
5386 */
5387 OO.ui.mixin.IndicatorElement.static.indicatorTitle = null;
5388
5389 /* Methods */
5390
5391 /**
5392 * Set the indicator element.
5393 *
5394 * If an element is already set, it will be cleaned up before setting up the new element.
5395 *
5396 * @param {jQuery} $indicator Element to use as indicator
5397 */
5398 OO.ui.mixin.IndicatorElement.prototype.setIndicatorElement = function ( $indicator ) {
5399 if ( this.$indicator ) {
5400 this.$indicator
5401 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator )
5402 .removeAttr( 'title' );
5403 }
5404
5405 this.$indicator = $indicator
5406 .addClass( 'oo-ui-indicatorElement-indicator' )
5407 .toggleClass( 'oo-ui-indicator-' + this.indicator, !!this.indicator );
5408 if ( this.indicatorTitle !== null ) {
5409 this.$indicator.attr( 'title', this.indicatorTitle );
5410 }
5411 };
5412
5413 /**
5414 * Set the indicator by its symbolic name: ‘alert’, ‘down’, ‘next’, ‘previous’, ‘required’, ‘up’. Use `null` to remove the indicator.
5415 *
5416 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
5417 * @chainable
5418 */
5419 OO.ui.mixin.IndicatorElement.prototype.setIndicator = function ( indicator ) {
5420 indicator = typeof indicator === 'string' && indicator.length ? indicator.trim() : null;
5421
5422 if ( this.indicator !== indicator ) {
5423 if ( this.$indicator ) {
5424 if ( this.indicator !== null ) {
5425 this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
5426 }
5427 if ( indicator !== null ) {
5428 this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
5429 }
5430 }
5431 this.indicator = indicator;
5432 }
5433
5434 this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator );
5435 this.updateThemeClasses();
5436
5437 return this;
5438 };
5439
5440 /**
5441 * Set the indicator title.
5442 *
5443 * The title is displayed when a user moves the mouse over the indicator.
5444 *
5445 * @param {string|Function|null} indicator Indicator title text, a function that returns text, or
5446 * `null` for no indicator title
5447 * @chainable
5448 */
5449 OO.ui.mixin.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
5450 indicatorTitle = typeof indicatorTitle === 'function' ||
5451 ( typeof indicatorTitle === 'string' && indicatorTitle.length ) ?
5452 OO.ui.resolveMsg( indicatorTitle ) : null;
5453
5454 if ( this.indicatorTitle !== indicatorTitle ) {
5455 this.indicatorTitle = indicatorTitle;
5456 if ( this.$indicator ) {
5457 if ( this.indicatorTitle !== null ) {
5458 this.$indicator.attr( 'title', indicatorTitle );
5459 } else {
5460 this.$indicator.removeAttr( 'title' );
5461 }
5462 }
5463 }
5464
5465 return this;
5466 };
5467
5468 /**
5469 * Get the symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
5470 *
5471 * @return {string} Symbolic name of indicator
5472 */
5473 OO.ui.mixin.IndicatorElement.prototype.getIndicator = function () {
5474 return this.indicator;
5475 };
5476
5477 /**
5478 * Get the indicator title.
5479 *
5480 * The title is displayed when a user moves the mouse over the indicator.
5481 *
5482 * @return {string} Indicator title text
5483 */
5484 OO.ui.mixin.IndicatorElement.prototype.getIndicatorTitle = function () {
5485 return this.indicatorTitle;
5486 };
5487
5488 /**
5489 * LabelElement is often mixed into other classes to generate a label, which
5490 * helps identify the function of an interface element.
5491 * See the [OOjs UI documentation on MediaWiki] [1] for more information.
5492 *
5493 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
5494 *
5495 * @abstract
5496 * @class
5497 *
5498 * @constructor
5499 * @param {Object} [config] Configuration options
5500 * @cfg {jQuery} [$label] The label element created by the class. If this
5501 * configuration is omitted, the label element will use a generated `<span>`.
5502 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified
5503 * as a plaintext string, a jQuery selection of elements, or a function that will produce a string
5504 * in the future. See the [OOjs UI documentation on MediaWiki] [2] for examples.
5505 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
5506 * @cfg {boolean} [autoFitLabel=true] Fit the label to the width of the parent element.
5507 * The label will be truncated to fit if necessary.
5508 */
5509 OO.ui.mixin.LabelElement = function OoUiMixinLabelElement( config ) {
5510 // Configuration initialization
5511 config = config || {};
5512
5513 // Properties
5514 this.$label = null;
5515 this.label = null;
5516 this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel;
5517
5518 // Initialization
5519 this.setLabel( config.label || this.constructor.static.label );
5520 this.setLabelElement( config.$label || $( '<span>' ) );
5521 };
5522
5523 /* Setup */
5524
5525 OO.initClass( OO.ui.mixin.LabelElement );
5526
5527 /* Events */
5528
5529 /**
5530 * @event labelChange
5531 * @param {string} value
5532 */
5533
5534 /* Static Properties */
5535
5536 /**
5537 * The label text. The label can be specified as a plaintext string, a function that will
5538 * produce a string in the future, or `null` for no label. The static value will
5539 * be overridden if a label is specified with the #label config option.
5540 *
5541 * @static
5542 * @inheritable
5543 * @property {string|Function|null}
5544 */
5545 OO.ui.mixin.LabelElement.static.label = null;
5546
5547 /* Methods */
5548
5549 /**
5550 * Set the label element.
5551 *
5552 * If an element is already set, it will be cleaned up before setting up the new element.
5553 *
5554 * @param {jQuery} $label Element to use as label
5555 */
5556 OO.ui.mixin.LabelElement.prototype.setLabelElement = function ( $label ) {
5557 if ( this.$label ) {
5558 this.$label.removeClass( 'oo-ui-labelElement-label' ).empty();
5559 }
5560
5561 this.$label = $label.addClass( 'oo-ui-labelElement-label' );
5562 this.setLabelContent( this.label );
5563 };
5564
5565 /**
5566 * Set the label.
5567 *
5568 * An empty string will result in the label being hidden. A string containing only whitespace will
5569 * be converted to a single `&nbsp;`.
5570 *
5571 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
5572 * text; or null for no label
5573 * @chainable
5574 */
5575 OO.ui.mixin.LabelElement.prototype.setLabel = function ( label ) {
5576 label = typeof label === 'function' ? OO.ui.resolveMsg( label ) : label;
5577 label = ( ( typeof label === 'string' && label.length ) || label instanceof jQuery || label instanceof OO.ui.HtmlSnippet ) ? label : null;
5578
5579 this.$element.toggleClass( 'oo-ui-labelElement', !!label );
5580
5581 if ( this.label !== label ) {
5582 if ( this.$label ) {
5583 this.setLabelContent( label );
5584 }
5585 this.label = label;
5586 this.emit( 'labelChange' );
5587 }
5588
5589 return this;
5590 };
5591
5592 /**
5593 * Get the label.
5594 *
5595 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
5596 * text; or null for no label
5597 */
5598 OO.ui.mixin.LabelElement.prototype.getLabel = function () {
5599 return this.label;
5600 };
5601
5602 /**
5603 * Fit the label.
5604 *
5605 * @chainable
5606 */
5607 OO.ui.mixin.LabelElement.prototype.fitLabel = function () {
5608 if ( this.$label && this.$label.autoEllipsis && this.autoFitLabel ) {
5609 this.$label.autoEllipsis( { hasSpan: false, tooltip: true } );
5610 }
5611
5612 return this;
5613 };
5614
5615 /**
5616 * Set the content of the label.
5617 *
5618 * Do not call this method until after the label element has been set by #setLabelElement.
5619 *
5620 * @private
5621 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
5622 * text; or null for no label
5623 */
5624 OO.ui.mixin.LabelElement.prototype.setLabelContent = function ( label ) {
5625 if ( typeof label === 'string' ) {
5626 if ( label.match( /^\s*$/ ) ) {
5627 // Convert whitespace only string to a single non-breaking space
5628 this.$label.html( '&nbsp;' );
5629 } else {
5630 this.$label.text( label );
5631 }
5632 } else if ( label instanceof OO.ui.HtmlSnippet ) {
5633 this.$label.html( label.toString() );
5634 } else if ( label instanceof jQuery ) {
5635 this.$label.empty().append( label );
5636 } else {
5637 this.$label.empty();
5638 }
5639 };
5640
5641 /**
5642 * LookupElement is a mixin that creates a {@link OO.ui.TextInputMenuSelectWidget menu} of suggested values for
5643 * a {@link OO.ui.TextInputWidget text input widget}. Suggested values are based on the characters the user types
5644 * into the text input field and, in general, the menu is only displayed when the user types. If a suggested value is chosen
5645 * from the lookup menu, that value becomes the value of the input field.
5646 *
5647 * Note that a new menu of suggested items is displayed when a value is chosen from the lookup menu. If this is
5648 * not the desired behavior, disable lookup menus with the #setLookupsDisabled method, then set the value, then
5649 * re-enable lookups.
5650 *
5651 * See the [OOjs UI demos][1] for an example.
5652 *
5653 * [1]: https://tools.wmflabs.org/oojs-ui/oojs-ui/demos/index.html#widgets-apex-vector-ltr
5654 *
5655 * @class
5656 * @abstract
5657 *
5658 * @constructor
5659 * @param {Object} [config] Configuration options
5660 * @cfg {jQuery} [$overlay] Overlay for the lookup menu; defaults to relative positioning
5661 * @cfg {jQuery} [$container=this.$element] The container element. The lookup menu is rendered beneath the specified element.
5662 * @cfg {boolean} [allowSuggestionsWhenEmpty=false] Request and display a lookup menu when the text input is empty.
5663 * By default, the lookup menu is not generated and displayed until the user begins to type.
5664 */
5665 OO.ui.mixin.LookupElement = function OoUiMixinLookupElement( config ) {
5666 // Configuration initialization
5667 config = config || {};
5668
5669 // Properties
5670 this.$overlay = config.$overlay || this.$element;
5671 this.lookupMenu = new OO.ui.TextInputMenuSelectWidget( this, {
5672 widget: this,
5673 input: this,
5674 $container: config.$container
5675 } );
5676
5677 this.allowSuggestionsWhenEmpty = config.allowSuggestionsWhenEmpty || false;
5678
5679 this.lookupCache = {};
5680 this.lookupQuery = null;
5681 this.lookupRequest = null;
5682 this.lookupsDisabled = false;
5683 this.lookupInputFocused = false;
5684
5685 // Events
5686 this.$input.on( {
5687 focus: this.onLookupInputFocus.bind( this ),
5688 blur: this.onLookupInputBlur.bind( this ),
5689 mousedown: this.onLookupInputMouseDown.bind( this )
5690 } );
5691 this.connect( this, { change: 'onLookupInputChange' } );
5692 this.lookupMenu.connect( this, {
5693 toggle: 'onLookupMenuToggle',
5694 choose: 'onLookupMenuItemChoose'
5695 } );
5696
5697 // Initialization
5698 this.$element.addClass( 'oo-ui-lookupElement' );
5699 this.lookupMenu.$element.addClass( 'oo-ui-lookupElement-menu' );
5700 this.$overlay.append( this.lookupMenu.$element );
5701 };
5702
5703 /* Methods */
5704
5705 /**
5706 * Handle input focus event.
5707 *
5708 * @protected
5709 * @param {jQuery.Event} e Input focus event
5710 */
5711 OO.ui.mixin.LookupElement.prototype.onLookupInputFocus = function () {
5712 this.lookupInputFocused = true;
5713 this.populateLookupMenu();
5714 };
5715
5716 /**
5717 * Handle input blur event.
5718 *
5719 * @protected
5720 * @param {jQuery.Event} e Input blur event
5721 */
5722 OO.ui.mixin.LookupElement.prototype.onLookupInputBlur = function () {
5723 this.closeLookupMenu();
5724 this.lookupInputFocused = false;
5725 };
5726
5727 /**
5728 * Handle input mouse down event.
5729 *
5730 * @protected
5731 * @param {jQuery.Event} e Input mouse down event
5732 */
5733 OO.ui.mixin.LookupElement.prototype.onLookupInputMouseDown = function () {
5734 // Only open the menu if the input was already focused.
5735 // This way we allow the user to open the menu again after closing it with Esc
5736 // by clicking in the input. Opening (and populating) the menu when initially
5737 // clicking into the input is handled by the focus handler.
5738 if ( this.lookupInputFocused && !this.lookupMenu.isVisible() ) {
5739 this.populateLookupMenu();
5740 }
5741 };
5742
5743 /**
5744 * Handle input change event.
5745 *
5746 * @protected
5747 * @param {string} value New input value
5748 */
5749 OO.ui.mixin.LookupElement.prototype.onLookupInputChange = function () {
5750 if ( this.lookupInputFocused ) {
5751 this.populateLookupMenu();
5752 }
5753 };
5754
5755 /**
5756 * Handle the lookup menu being shown/hidden.
5757 *
5758 * @protected
5759 * @param {boolean} visible Whether the lookup menu is now visible.
5760 */
5761 OO.ui.mixin.LookupElement.prototype.onLookupMenuToggle = function ( visible ) {
5762 if ( !visible ) {
5763 // When the menu is hidden, abort any active request and clear the menu.
5764 // This has to be done here in addition to closeLookupMenu(), because
5765 // MenuSelectWidget will close itself when the user presses Esc.
5766 this.abortLookupRequest();
5767 this.lookupMenu.clearItems();
5768 }
5769 };
5770
5771 /**
5772 * Handle menu item 'choose' event, updating the text input value to the value of the clicked item.
5773 *
5774 * @protected
5775 * @param {OO.ui.MenuOptionWidget} item Selected item
5776 */
5777 OO.ui.mixin.LookupElement.prototype.onLookupMenuItemChoose = function ( item ) {
5778 this.setValue( item.getData() );
5779 };
5780
5781 /**
5782 * Get lookup menu.
5783 *
5784 * @private
5785 * @return {OO.ui.TextInputMenuSelectWidget}
5786 */
5787 OO.ui.mixin.LookupElement.prototype.getLookupMenu = function () {
5788 return this.lookupMenu;
5789 };
5790
5791 /**
5792 * Disable or re-enable lookups.
5793 *
5794 * When lookups are disabled, calls to #populateLookupMenu will be ignored.
5795 *
5796 * @param {boolean} disabled Disable lookups
5797 */
5798 OO.ui.mixin.LookupElement.prototype.setLookupsDisabled = function ( disabled ) {
5799 this.lookupsDisabled = !!disabled;
5800 };
5801
5802 /**
5803 * Open the menu. If there are no entries in the menu, this does nothing.
5804 *
5805 * @private
5806 * @chainable
5807 */
5808 OO.ui.mixin.LookupElement.prototype.openLookupMenu = function () {
5809 if ( !this.lookupMenu.isEmpty() ) {
5810 this.lookupMenu.toggle( true );
5811 }
5812 return this;
5813 };
5814
5815 /**
5816 * Close the menu, empty it, and abort any pending request.
5817 *
5818 * @private
5819 * @chainable
5820 */
5821 OO.ui.mixin.LookupElement.prototype.closeLookupMenu = function () {
5822 this.lookupMenu.toggle( false );
5823 this.abortLookupRequest();
5824 this.lookupMenu.clearItems();
5825 return this;
5826 };
5827
5828 /**
5829 * Request menu items based on the input's current value, and when they arrive,
5830 * populate the menu with these items and show the menu.
5831 *
5832 * If lookups have been disabled with #setLookupsDisabled, this function does nothing.
5833 *
5834 * @private
5835 * @chainable
5836 */
5837 OO.ui.mixin.LookupElement.prototype.populateLookupMenu = function () {
5838 var widget = this,
5839 value = this.getValue();
5840
5841 if ( this.lookupsDisabled ) {
5842 return;
5843 }
5844
5845 // If the input is empty, clear the menu, unless suggestions when empty are allowed.
5846 if ( !this.allowSuggestionsWhenEmpty && value === '' ) {
5847 this.closeLookupMenu();
5848 // Skip population if there is already a request pending for the current value
5849 } else if ( value !== this.lookupQuery ) {
5850 this.getLookupMenuItems()
5851 .done( function ( items ) {
5852 widget.lookupMenu.clearItems();
5853 if ( items.length ) {
5854 widget.lookupMenu
5855 .addItems( items )
5856 .toggle( true );
5857 widget.initializeLookupMenuSelection();
5858 } else {
5859 widget.lookupMenu.toggle( false );
5860 }
5861 } )
5862 .fail( function () {
5863 widget.lookupMenu.clearItems();
5864 } );
5865 }
5866
5867 return this;
5868 };
5869
5870 /**
5871 * Highlight the first selectable item in the menu.
5872 *
5873 * @private
5874 * @chainable
5875 */
5876 OO.ui.mixin.LookupElement.prototype.initializeLookupMenuSelection = function () {
5877 if ( !this.lookupMenu.getSelectedItem() ) {
5878 this.lookupMenu.highlightItem( this.lookupMenu.getFirstSelectableItem() );
5879 }
5880 };
5881
5882 /**
5883 * Get lookup menu items for the current query.
5884 *
5885 * @private
5886 * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument of
5887 * the done event. If the request was aborted to make way for a subsequent request, this promise
5888 * will not be rejected: it will remain pending forever.
5889 */
5890 OO.ui.mixin.LookupElement.prototype.getLookupMenuItems = function () {
5891 var widget = this,
5892 value = this.getValue(),
5893 deferred = $.Deferred(),
5894 ourRequest;
5895
5896 this.abortLookupRequest();
5897 if ( Object.prototype.hasOwnProperty.call( this.lookupCache, value ) ) {
5898 deferred.resolve( this.getLookupMenuOptionsFromData( this.lookupCache[ value ] ) );
5899 } else {
5900 this.pushPending();
5901 this.lookupQuery = value;
5902 ourRequest = this.lookupRequest = this.getLookupRequest();
5903 ourRequest
5904 .always( function () {
5905 // We need to pop pending even if this is an old request, otherwise
5906 // the widget will remain pending forever.
5907 // TODO: this assumes that an aborted request will fail or succeed soon after
5908 // being aborted, or at least eventually. It would be nice if we could popPending()
5909 // at abort time, but only if we knew that we hadn't already called popPending()
5910 // for that request.
5911 widget.popPending();
5912 } )
5913 .done( function ( response ) {
5914 // If this is an old request (and aborting it somehow caused it to still succeed),
5915 // ignore its success completely
5916 if ( ourRequest === widget.lookupRequest ) {
5917 widget.lookupQuery = null;
5918 widget.lookupRequest = null;
5919 widget.lookupCache[ value ] = widget.getLookupCacheDataFromResponse( response );
5920 deferred.resolve( widget.getLookupMenuOptionsFromData( widget.lookupCache[ value ] ) );
5921 }
5922 } )
5923 .fail( function () {
5924 // If this is an old request (or a request failing because it's being aborted),
5925 // ignore its failure completely
5926 if ( ourRequest === widget.lookupRequest ) {
5927 widget.lookupQuery = null;
5928 widget.lookupRequest = null;
5929 deferred.reject();
5930 }
5931 } );
5932 }
5933 return deferred.promise();
5934 };
5935
5936 /**
5937 * Abort the currently pending lookup request, if any.
5938 *
5939 * @private
5940 */
5941 OO.ui.mixin.LookupElement.prototype.abortLookupRequest = function () {
5942 var oldRequest = this.lookupRequest;
5943 if ( oldRequest ) {
5944 // First unset this.lookupRequest to the fail handler will notice
5945 // that the request is no longer current
5946 this.lookupRequest = null;
5947 this.lookupQuery = null;
5948 oldRequest.abort();
5949 }
5950 };
5951
5952 /**
5953 * Get a new request object of the current lookup query value.
5954 *
5955 * @protected
5956 * @abstract
5957 * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
5958 */
5959 OO.ui.mixin.LookupElement.prototype.getLookupRequest = function () {
5960 // Stub, implemented in subclass
5961 return null;
5962 };
5963
5964 /**
5965 * Pre-process data returned by the request from #getLookupRequest.
5966 *
5967 * The return value of this function will be cached, and any further queries for the given value
5968 * will use the cache rather than doing API requests.
5969 *
5970 * @protected
5971 * @abstract
5972 * @param {Mixed} response Response from server
5973 * @return {Mixed} Cached result data
5974 */
5975 OO.ui.mixin.LookupElement.prototype.getLookupCacheDataFromResponse = function () {
5976 // Stub, implemented in subclass
5977 return [];
5978 };
5979
5980 /**
5981 * Get a list of menu option widgets from the (possibly cached) data returned by
5982 * #getLookupCacheDataFromResponse.
5983 *
5984 * @protected
5985 * @abstract
5986 * @param {Mixed} data Cached result data, usually an array
5987 * @return {OO.ui.MenuOptionWidget[]} Menu items
5988 */
5989 OO.ui.mixin.LookupElement.prototype.getLookupMenuOptionsFromData = function () {
5990 // Stub, implemented in subclass
5991 return [];
5992 };
5993
5994 /**
5995 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
5996 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
5997 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
5998 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
5999 *
6000 * @abstract
6001 * @class
6002 *
6003 * @constructor
6004 * @param {Object} [config] Configuration options
6005 * @cfg {Object} [popup] Configuration to pass to popup
6006 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
6007 */
6008 OO.ui.mixin.PopupElement = function OoUiMixinPopupElement( config ) {
6009 // Configuration initialization
6010 config = config || {};
6011
6012 // Properties
6013 this.popup = new OO.ui.PopupWidget( $.extend(
6014 { autoClose: true },
6015 config.popup,
6016 { $autoCloseIgnore: this.$element }
6017 ) );
6018 };
6019
6020 /* Methods */
6021
6022 /**
6023 * Get popup.
6024 *
6025 * @return {OO.ui.PopupWidget} Popup widget
6026 */
6027 OO.ui.mixin.PopupElement.prototype.getPopup = function () {
6028 return this.popup;
6029 };
6030
6031 /**
6032 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
6033 * additional functionality to an element created by another class. The class provides
6034 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
6035 * which are used to customize the look and feel of a widget to better describe its
6036 * importance and functionality.
6037 *
6038 * The library currently contains the following styling flags for general use:
6039 *
6040 * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
6041 * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
6042 * - **constructive**: Constructive styling is applied to convey that the widget will create something.
6043 *
6044 * The flags affect the appearance of the buttons:
6045 *
6046 * @example
6047 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
6048 * var button1 = new OO.ui.ButtonWidget( {
6049 * label: 'Constructive',
6050 * flags: 'constructive'
6051 * } );
6052 * var button2 = new OO.ui.ButtonWidget( {
6053 * label: 'Destructive',
6054 * flags: 'destructive'
6055 * } );
6056 * var button3 = new OO.ui.ButtonWidget( {
6057 * label: 'Progressive',
6058 * flags: 'progressive'
6059 * } );
6060 * $( 'body' ).append( button1.$element, button2.$element, button3.$element );
6061 *
6062 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
6063 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
6064 *
6065 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
6066 *
6067 * @abstract
6068 * @class
6069 *
6070 * @constructor
6071 * @param {Object} [config] Configuration options
6072 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'constructive' or 'primary') to apply.
6073 * Please see the [OOjs UI documentation on MediaWiki] [2] for more information about available flags.
6074 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
6075 * @cfg {jQuery} [$flagged] The flagged element. By default,
6076 * the flagged functionality is applied to the element created by the class ($element).
6077 * If a different element is specified, the flagged functionality will be applied to it instead.
6078 */
6079 OO.ui.mixin.FlaggedElement = function OoUiMixinFlaggedElement( config ) {
6080 // Configuration initialization
6081 config = config || {};
6082
6083 // Properties
6084 this.flags = {};
6085 this.$flagged = null;
6086
6087 // Initialization
6088 this.setFlags( config.flags );
6089 this.setFlaggedElement( config.$flagged || this.$element );
6090 };
6091
6092 /* Events */
6093
6094 /**
6095 * @event flag
6096 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
6097 * parameter contains the name of each modified flag and indicates whether it was
6098 * added or removed.
6099 *
6100 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
6101 * that the flag was added, `false` that the flag was removed.
6102 */
6103
6104 /* Methods */
6105
6106 /**
6107 * Set the flagged element.
6108 *
6109 * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
6110 * If an element is already set, the method will remove the mixin’s effect on that element.
6111 *
6112 * @param {jQuery} $flagged Element that should be flagged
6113 */
6114 OO.ui.mixin.FlaggedElement.prototype.setFlaggedElement = function ( $flagged ) {
6115 var classNames = Object.keys( this.flags ).map( function ( flag ) {
6116 return 'oo-ui-flaggedElement-' + flag;
6117 } ).join( ' ' );
6118
6119 if ( this.$flagged ) {
6120 this.$flagged.removeClass( classNames );
6121 }
6122
6123 this.$flagged = $flagged.addClass( classNames );
6124 };
6125
6126 /**
6127 * Check if the specified flag is set.
6128 *
6129 * @param {string} flag Name of flag
6130 * @return {boolean} The flag is set
6131 */
6132 OO.ui.mixin.FlaggedElement.prototype.hasFlag = function ( flag ) {
6133 return flag in this.flags;
6134 };
6135
6136 /**
6137 * Get the names of all flags set.
6138 *
6139 * @return {string[]} Flag names
6140 */
6141 OO.ui.mixin.FlaggedElement.prototype.getFlags = function () {
6142 return Object.keys( this.flags );
6143 };
6144
6145 /**
6146 * Clear all flags.
6147 *
6148 * @chainable
6149 * @fires flag
6150 */
6151 OO.ui.mixin.FlaggedElement.prototype.clearFlags = function () {
6152 var flag, className,
6153 changes = {},
6154 remove = [],
6155 classPrefix = 'oo-ui-flaggedElement-';
6156
6157 for ( flag in this.flags ) {
6158 className = classPrefix + flag;
6159 changes[ flag ] = false;
6160 delete this.flags[ flag ];
6161 remove.push( className );
6162 }
6163
6164 if ( this.$flagged ) {
6165 this.$flagged.removeClass( remove.join( ' ' ) );
6166 }
6167
6168 this.updateThemeClasses();
6169 this.emit( 'flag', changes );
6170
6171 return this;
6172 };
6173
6174 /**
6175 * Add one or more flags.
6176 *
6177 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
6178 * or an object keyed by flag name with a boolean value that indicates whether the flag should
6179 * be added (`true`) or removed (`false`).
6180 * @chainable
6181 * @fires flag
6182 */
6183 OO.ui.mixin.FlaggedElement.prototype.setFlags = function ( flags ) {
6184 var i, len, flag, className,
6185 changes = {},
6186 add = [],
6187 remove = [],
6188 classPrefix = 'oo-ui-flaggedElement-';
6189
6190 if ( typeof flags === 'string' ) {
6191 className = classPrefix + flags;
6192 // Set
6193 if ( !this.flags[ flags ] ) {
6194 this.flags[ flags ] = true;
6195 add.push( className );
6196 }
6197 } else if ( Array.isArray( flags ) ) {
6198 for ( i = 0, len = flags.length; i < len; i++ ) {
6199 flag = flags[ i ];
6200 className = classPrefix + flag;
6201 // Set
6202 if ( !this.flags[ flag ] ) {
6203 changes[ flag ] = true;
6204 this.flags[ flag ] = true;
6205 add.push( className );
6206 }
6207 }
6208 } else if ( OO.isPlainObject( flags ) ) {
6209 for ( flag in flags ) {
6210 className = classPrefix + flag;
6211 if ( flags[ flag ] ) {
6212 // Set
6213 if ( !this.flags[ flag ] ) {
6214 changes[ flag ] = true;
6215 this.flags[ flag ] = true;
6216 add.push( className );
6217 }
6218 } else {
6219 // Remove
6220 if ( this.flags[ flag ] ) {
6221 changes[ flag ] = false;
6222 delete this.flags[ flag ];
6223 remove.push( className );
6224 }
6225 }
6226 }
6227 }
6228
6229 if ( this.$flagged ) {
6230 this.$flagged
6231 .addClass( add.join( ' ' ) )
6232 .removeClass( remove.join( ' ' ) );
6233 }
6234
6235 this.updateThemeClasses();
6236 this.emit( 'flag', changes );
6237
6238 return this;
6239 };
6240
6241 /**
6242 * TitledElement is mixed into other classes to provide a `title` attribute.
6243 * Titles are rendered by the browser and are made visible when the user moves
6244 * the mouse over the element. Titles are not visible on touch devices.
6245 *
6246 * @example
6247 * // TitledElement provides a 'title' attribute to the
6248 * // ButtonWidget class
6249 * var button = new OO.ui.ButtonWidget( {
6250 * label: 'Button with Title',
6251 * title: 'I am a button'
6252 * } );
6253 * $( 'body' ).append( button.$element );
6254 *
6255 * @abstract
6256 * @class
6257 *
6258 * @constructor
6259 * @param {Object} [config] Configuration options
6260 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
6261 * If this config is omitted, the title functionality is applied to $element, the
6262 * element created by the class.
6263 * @cfg {string|Function} [title] The title text or a function that returns text. If
6264 * this config is omitted, the value of the {@link #static-title static title} property is used.
6265 */
6266 OO.ui.mixin.TitledElement = function OoUiMixinTitledElement( config ) {
6267 // Configuration initialization
6268 config = config || {};
6269
6270 // Properties
6271 this.$titled = null;
6272 this.title = null;
6273
6274 // Initialization
6275 this.setTitle( config.title || this.constructor.static.title );
6276 this.setTitledElement( config.$titled || this.$element );
6277 };
6278
6279 /* Setup */
6280
6281 OO.initClass( OO.ui.mixin.TitledElement );
6282
6283 /* Static Properties */
6284
6285 /**
6286 * The title text, a function that returns text, or `null` for no title. The value of the static property
6287 * is overridden if the #title config option is used.
6288 *
6289 * @static
6290 * @inheritable
6291 * @property {string|Function|null}
6292 */
6293 OO.ui.mixin.TitledElement.static.title = null;
6294
6295 /* Methods */
6296
6297 /**
6298 * Set the titled element.
6299 *
6300 * This method is used to retarget a titledElement mixin so that its functionality applies to the specified element.
6301 * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
6302 *
6303 * @param {jQuery} $titled Element that should use the 'titled' functionality
6304 */
6305 OO.ui.mixin.TitledElement.prototype.setTitledElement = function ( $titled ) {
6306 if ( this.$titled ) {
6307 this.$titled.removeAttr( 'title' );
6308 }
6309
6310 this.$titled = $titled;
6311 if ( this.title ) {
6312 this.$titled.attr( 'title', this.title );
6313 }
6314 };
6315
6316 /**
6317 * Set title.
6318 *
6319 * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
6320 * @chainable
6321 */
6322 OO.ui.mixin.TitledElement.prototype.setTitle = function ( title ) {
6323 title = typeof title === 'string' ? OO.ui.resolveMsg( title ) : null;
6324
6325 if ( this.title !== title ) {
6326 if ( this.$titled ) {
6327 if ( title !== null ) {
6328 this.$titled.attr( 'title', title );
6329 } else {
6330 this.$titled.removeAttr( 'title' );
6331 }
6332 }
6333 this.title = title;
6334 }
6335
6336 return this;
6337 };
6338
6339 /**
6340 * Get title.
6341 *
6342 * @return {string} Title string
6343 */
6344 OO.ui.mixin.TitledElement.prototype.getTitle = function () {
6345 return this.title;
6346 };
6347
6348 /**
6349 * Element that can be automatically clipped to visible boundaries.
6350 *
6351 * Whenever the element's natural height changes, you have to call
6352 * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
6353 * clipping correctly.
6354 *
6355 * @abstract
6356 * @class
6357 *
6358 * @constructor
6359 * @param {Object} [config] Configuration options
6360 * @cfg {jQuery} [$clippable] Nodes to clip, assigned to #$clippable, omit to use #$element
6361 */
6362 OO.ui.mixin.ClippableElement = function OoUiMixinClippableElement( config ) {
6363 // Configuration initialization
6364 config = config || {};
6365
6366 // Properties
6367 this.$clippable = null;
6368 this.clipping = false;
6369 this.clippedHorizontally = false;
6370 this.clippedVertically = false;
6371 this.$clippableContainer = null;
6372 this.$clippableScroller = null;
6373 this.$clippableWindow = null;
6374 this.idealWidth = null;
6375 this.idealHeight = null;
6376 this.onClippableContainerScrollHandler = this.clip.bind( this );
6377 this.onClippableWindowResizeHandler = this.clip.bind( this );
6378
6379 // Initialization
6380 this.setClippableElement( config.$clippable || this.$element );
6381 };
6382
6383 /* Methods */
6384
6385 /**
6386 * Set clippable element.
6387 *
6388 * If an element is already set, it will be cleaned up before setting up the new element.
6389 *
6390 * @param {jQuery} $clippable Element to make clippable
6391 */
6392 OO.ui.mixin.ClippableElement.prototype.setClippableElement = function ( $clippable ) {
6393 if ( this.$clippable ) {
6394 this.$clippable.removeClass( 'oo-ui-clippableElement-clippable' );
6395 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
6396 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
6397 }
6398
6399 this.$clippable = $clippable.addClass( 'oo-ui-clippableElement-clippable' );
6400 this.clip();
6401 };
6402
6403 /**
6404 * Toggle clipping.
6405 *
6406 * Do not turn clipping on until after the element is attached to the DOM and visible.
6407 *
6408 * @param {boolean} [clipping] Enable clipping, omit to toggle
6409 * @chainable
6410 */
6411 OO.ui.mixin.ClippableElement.prototype.toggleClipping = function ( clipping ) {
6412 clipping = clipping === undefined ? !this.clipping : !!clipping;
6413
6414 if ( this.clipping !== clipping ) {
6415 this.clipping = clipping;
6416 if ( clipping ) {
6417 this.$clippableContainer = $( this.getClosestScrollableElementContainer() );
6418 // If the clippable container is the root, we have to listen to scroll events and check
6419 // jQuery.scrollTop on the window because of browser inconsistencies
6420 this.$clippableScroller = this.$clippableContainer.is( 'html, body' ) ?
6421 $( OO.ui.Element.static.getWindow( this.$clippableContainer ) ) :
6422 this.$clippableContainer;
6423 this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler );
6424 this.$clippableWindow = $( this.getElementWindow() )
6425 .on( 'resize', this.onClippableWindowResizeHandler );
6426 // Initial clip after visible
6427 this.clip();
6428 } else {
6429 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
6430 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
6431
6432 this.$clippableContainer = null;
6433 this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler );
6434 this.$clippableScroller = null;
6435 this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
6436 this.$clippableWindow = null;
6437 }
6438 }
6439
6440 return this;
6441 };
6442
6443 /**
6444 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
6445 *
6446 * @return {boolean} Element will be clipped to the visible area
6447 */
6448 OO.ui.mixin.ClippableElement.prototype.isClipping = function () {
6449 return this.clipping;
6450 };
6451
6452 /**
6453 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
6454 *
6455 * @return {boolean} Part of the element is being clipped
6456 */
6457 OO.ui.mixin.ClippableElement.prototype.isClipped = function () {
6458 return this.clippedHorizontally || this.clippedVertically;
6459 };
6460
6461 /**
6462 * Check if the right of the element is being clipped by the nearest scrollable container.
6463 *
6464 * @return {boolean} Part of the element is being clipped
6465 */
6466 OO.ui.mixin.ClippableElement.prototype.isClippedHorizontally = function () {
6467 return this.clippedHorizontally;
6468 };
6469
6470 /**
6471 * Check if the bottom of the element is being clipped by the nearest scrollable container.
6472 *
6473 * @return {boolean} Part of the element is being clipped
6474 */
6475 OO.ui.mixin.ClippableElement.prototype.isClippedVertically = function () {
6476 return this.clippedVertically;
6477 };
6478
6479 /**
6480 * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
6481 *
6482 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
6483 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
6484 */
6485 OO.ui.mixin.ClippableElement.prototype.setIdealSize = function ( width, height ) {
6486 this.idealWidth = width;
6487 this.idealHeight = height;
6488
6489 if ( !this.clipping ) {
6490 // Update dimensions
6491 this.$clippable.css( { width: width, height: height } );
6492 }
6493 // While clipping, idealWidth and idealHeight are not considered
6494 };
6495
6496 /**
6497 * Clip element to visible boundaries and allow scrolling when needed. Call this method when
6498 * the element's natural height changes.
6499 *
6500 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
6501 * overlapped by, the visible area of the nearest scrollable container.
6502 *
6503 * @chainable
6504 */
6505 OO.ui.mixin.ClippableElement.prototype.clip = function () {
6506 if ( !this.clipping ) {
6507 // this.$clippableContainer and this.$clippableWindow are null, so the below will fail
6508 return this;
6509 }
6510
6511 var buffer = 7, // Chosen by fair dice roll
6512 cOffset = this.$clippable.offset(),
6513 $container = this.$clippableContainer.is( 'html, body' ) ?
6514 this.$clippableWindow : this.$clippableContainer,
6515 ccOffset = $container.offset() || { top: 0, left: 0 },
6516 ccHeight = $container.innerHeight() - buffer,
6517 ccWidth = $container.innerWidth() - buffer,
6518 cWidth = this.$clippable.outerWidth() + buffer,
6519 scrollTop = this.$clippableScroller[0] === this.$clippableWindow[0] ? this.$clippableScroller.scrollTop() : 0,
6520 scrollLeft = this.$clippableScroller.scrollLeft(),
6521 desiredWidth = cOffset.left < 0 ?
6522 cWidth + cOffset.left :
6523 ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left,
6524 desiredHeight = ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top,
6525 naturalWidth = this.$clippable.prop( 'scrollWidth' ),
6526 naturalHeight = this.$clippable.prop( 'scrollHeight' ),
6527 clipWidth = desiredWidth < naturalWidth,
6528 clipHeight = desiredHeight < naturalHeight;
6529
6530 if ( clipWidth ) {
6531 this.$clippable.css( { overflowX: 'scroll', width: desiredWidth } );
6532 } else {
6533 this.$clippable.css( { width: this.idealWidth || '', overflowX: '' } );
6534 }
6535 if ( clipHeight ) {
6536 this.$clippable.css( { overflowY: 'scroll', height: desiredHeight } );
6537 } else {
6538 this.$clippable.css( { height: this.idealHeight || '', overflowY: '' } );
6539 }
6540
6541 // If we stopped clipping in at least one of the dimensions
6542 if ( !clipWidth || !clipHeight ) {
6543 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
6544 }
6545
6546 this.clippedHorizontally = clipWidth;
6547 this.clippedVertically = clipHeight;
6548
6549 return this;
6550 };
6551
6552 /**
6553 * Tools, together with {@link OO.ui.ToolGroup toolgroups}, constitute {@link OO.ui.Toolbar toolbars}.
6554 * Each tool is configured with a static name, title, and icon and is customized with the command to carry
6555 * out when the tool is selected. Tools must also be registered with a {@link OO.ui.ToolFactory tool factory},
6556 * which creates the tools on demand.
6557 *
6558 * Tools are added to toolgroups ({@link OO.ui.ListToolGroup ListToolGroup},
6559 * {@link OO.ui.BarToolGroup BarToolGroup}, or {@link OO.ui.MenuToolGroup MenuToolGroup}), which determine how
6560 * the tool is displayed in the toolbar. See {@link OO.ui.Toolbar toolbars} for an example.
6561 *
6562 * For more information, please see the [OOjs UI documentation on MediaWiki][1].
6563 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
6564 *
6565 * @abstract
6566 * @class
6567 * @extends OO.ui.Widget
6568 * @mixins OO.ui.mixin.IconElement
6569 * @mixins OO.ui.mixin.FlaggedElement
6570 * @mixins OO.ui.mixin.TabIndexedElement
6571 *
6572 * @constructor
6573 * @param {OO.ui.ToolGroup} toolGroup
6574 * @param {Object} [config] Configuration options
6575 * @cfg {string|Function} [title] Title text or a function that returns text. If this config is omitted, the value of
6576 * the {@link #static-title static title} property is used.
6577 *
6578 * The title is used in different ways depending on the type of toolgroup that contains the tool. The
6579 * title is used as a tooltip if the tool is part of a {@link OO.ui.BarToolGroup bar} toolgroup, or as the label text if the tool is
6580 * part of a {@link OO.ui.ListToolGroup list} or {@link OO.ui.MenuToolGroup menu} toolgroup.
6581 *
6582 * For bar toolgroups, a description of the accelerator key is appended to the title if an accelerator key
6583 * is associated with an action by the same name as the tool and accelerator functionality has been added to the application.
6584 * To add accelerator key functionality, you must subclass OO.ui.Toolbar and override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method.
6585 */
6586 OO.ui.Tool = function OoUiTool( toolGroup, config ) {
6587 // Allow passing positional parameters inside the config object
6588 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
6589 config = toolGroup;
6590 toolGroup = config.toolGroup;
6591 }
6592
6593 // Configuration initialization
6594 config = config || {};
6595
6596 // Parent constructor
6597 OO.ui.Tool.parent.call( this, config );
6598
6599 // Properties
6600 this.toolGroup = toolGroup;
6601 this.toolbar = this.toolGroup.getToolbar();
6602 this.active = false;
6603 this.$title = $( '<span>' );
6604 this.$accel = $( '<span>' );
6605 this.$link = $( '<a>' );
6606 this.title = null;
6607
6608 // Mixin constructors
6609 OO.ui.mixin.IconElement.call( this, config );
6610 OO.ui.mixin.FlaggedElement.call( this, config );
6611 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$link } ) );
6612
6613 // Events
6614 this.toolbar.connect( this, { updateState: 'onUpdateState' } );
6615
6616 // Initialization
6617 this.$title.addClass( 'oo-ui-tool-title' );
6618 this.$accel
6619 .addClass( 'oo-ui-tool-accel' )
6620 .prop( {
6621 // This may need to be changed if the key names are ever localized,
6622 // but for now they are essentially written in English
6623 dir: 'ltr',
6624 lang: 'en'
6625 } );
6626 this.$link
6627 .addClass( 'oo-ui-tool-link' )
6628 .append( this.$icon, this.$title, this.$accel )
6629 .attr( 'role', 'button' );
6630 this.$element
6631 .data( 'oo-ui-tool', this )
6632 .addClass(
6633 'oo-ui-tool ' + 'oo-ui-tool-name-' +
6634 this.constructor.static.name.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' )
6635 )
6636 .toggleClass( 'oo-ui-tool-with-label', this.constructor.static.displayBothIconAndLabel )
6637 .append( this.$link );
6638 this.setTitle( config.title || this.constructor.static.title );
6639 };
6640
6641 /* Setup */
6642
6643 OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
6644 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.IconElement );
6645 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.FlaggedElement );
6646 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.TabIndexedElement );
6647
6648 /* Static Properties */
6649
6650 /**
6651 * @static
6652 * @inheritdoc
6653 */
6654 OO.ui.Tool.static.tagName = 'span';
6655
6656 /**
6657 * Symbolic name of tool.
6658 *
6659 * The symbolic name is used internally to register the tool with a {@link OO.ui.ToolFactory ToolFactory}. It can
6660 * also be used when adding tools to toolgroups.
6661 *
6662 * @abstract
6663 * @static
6664 * @inheritable
6665 * @property {string}
6666 */
6667 OO.ui.Tool.static.name = '';
6668
6669 /**
6670 * Symbolic name of the group.
6671 *
6672 * The group name is used to associate tools with each other so that they can be selected later by
6673 * a {@link OO.ui.ToolGroup toolgroup}.
6674 *
6675 * @abstract
6676 * @static
6677 * @inheritable
6678 * @property {string}
6679 */
6680 OO.ui.Tool.static.group = '';
6681
6682 /**
6683 * Tool title text or a function that returns title text. The value of the static property is overridden if the #title config option is used.
6684 *
6685 * @abstract
6686 * @static
6687 * @inheritable
6688 * @property {string|Function}
6689 */
6690 OO.ui.Tool.static.title = '';
6691
6692 /**
6693 * Display both icon and label when the tool is used in a {@link OO.ui.BarToolGroup bar} toolgroup.
6694 * Normally only the icon is displayed, or only the label if no icon is given.
6695 *
6696 * @static
6697 * @inheritable
6698 * @property {boolean}
6699 */
6700 OO.ui.Tool.static.displayBothIconAndLabel = false;
6701
6702 /**
6703 * Add tool to catch-all groups automatically.
6704 *
6705 * A catch-all group, which contains all tools that do not currently belong to a toolgroup,
6706 * can be included in a toolgroup using the wildcard selector, an asterisk (*).
6707 *
6708 * @static
6709 * @inheritable
6710 * @property {boolean}
6711 */
6712 OO.ui.Tool.static.autoAddToCatchall = true;
6713
6714 /**
6715 * Add tool to named groups automatically.
6716 *
6717 * By default, tools that are configured with a static ‘group’ property are added
6718 * to that group and will be selected when the symbolic name of the group is specified (e.g., when
6719 * toolgroups include tools by group name).
6720 *
6721 * @static
6722 * @property {boolean}
6723 * @inheritable
6724 */
6725 OO.ui.Tool.static.autoAddToGroup = true;
6726
6727 /**
6728 * Check if this tool is compatible with given data.
6729 *
6730 * This is a stub that can be overriden to provide support for filtering tools based on an
6731 * arbitrary piece of information (e.g., where the cursor is in a document). The implementation
6732 * must also call this method so that the compatibility check can be performed.
6733 *
6734 * @static
6735 * @inheritable
6736 * @param {Mixed} data Data to check
6737 * @return {boolean} Tool can be used with data
6738 */
6739 OO.ui.Tool.static.isCompatibleWith = function () {
6740 return false;
6741 };
6742
6743 /* Methods */
6744
6745 /**
6746 * Handle the toolbar state being updated.
6747 *
6748 * This is an abstract method that must be overridden in a concrete subclass.
6749 *
6750 * @protected
6751 * @abstract
6752 */
6753 OO.ui.Tool.prototype.onUpdateState = function () {
6754 throw new Error(
6755 'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor
6756 );
6757 };
6758
6759 /**
6760 * Handle the tool being selected.
6761 *
6762 * This is an abstract method that must be overridden in a concrete subclass.
6763 *
6764 * @protected
6765 * @abstract
6766 */
6767 OO.ui.Tool.prototype.onSelect = function () {
6768 throw new Error(
6769 'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor
6770 );
6771 };
6772
6773 /**
6774 * Check if the tool is active.
6775 *
6776 * Tools become active when their #onSelect or #onUpdateState handlers change them to appear pressed
6777 * with the #setActive method. Additional CSS is applied to the tool to reflect the active state.
6778 *
6779 * @return {boolean} Tool is active
6780 */
6781 OO.ui.Tool.prototype.isActive = function () {
6782 return this.active;
6783 };
6784
6785 /**
6786 * Make the tool appear active or inactive.
6787 *
6788 * This method should be called within #onSelect or #onUpdateState event handlers to make the tool
6789 * appear pressed or not.
6790 *
6791 * @param {boolean} state Make tool appear active
6792 */
6793 OO.ui.Tool.prototype.setActive = function ( state ) {
6794 this.active = !!state;
6795 if ( this.active ) {
6796 this.$element.addClass( 'oo-ui-tool-active' );
6797 } else {
6798 this.$element.removeClass( 'oo-ui-tool-active' );
6799 }
6800 };
6801
6802 /**
6803 * Set the tool #title.
6804 *
6805 * @param {string|Function} title Title text or a function that returns text
6806 * @chainable
6807 */
6808 OO.ui.Tool.prototype.setTitle = function ( title ) {
6809 this.title = OO.ui.resolveMsg( title );
6810 this.updateTitle();
6811 return this;
6812 };
6813
6814 /**
6815 * Get the tool #title.
6816 *
6817 * @return {string} Title text
6818 */
6819 OO.ui.Tool.prototype.getTitle = function () {
6820 return this.title;
6821 };
6822
6823 /**
6824 * Get the tool's symbolic name.
6825 *
6826 * @return {string} Symbolic name of tool
6827 */
6828 OO.ui.Tool.prototype.getName = function () {
6829 return this.constructor.static.name;
6830 };
6831
6832 /**
6833 * Update the title.
6834 */
6835 OO.ui.Tool.prototype.updateTitle = function () {
6836 var titleTooltips = this.toolGroup.constructor.static.titleTooltips,
6837 accelTooltips = this.toolGroup.constructor.static.accelTooltips,
6838 accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
6839 tooltipParts = [];
6840
6841 this.$title.text( this.title );
6842 this.$accel.text( accel );
6843
6844 if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
6845 tooltipParts.push( this.title );
6846 }
6847 if ( accelTooltips && typeof accel === 'string' && accel.length ) {
6848 tooltipParts.push( accel );
6849 }
6850 if ( tooltipParts.length ) {
6851 this.$link.attr( 'title', tooltipParts.join( ' ' ) );
6852 } else {
6853 this.$link.removeAttr( 'title' );
6854 }
6855 };
6856
6857 /**
6858 * Destroy tool.
6859 *
6860 * Destroying the tool removes all event handlers and the tool’s DOM elements.
6861 * Call this method whenever you are done using a tool.
6862 */
6863 OO.ui.Tool.prototype.destroy = function () {
6864 this.toolbar.disconnect( this );
6865 this.$element.remove();
6866 };
6867
6868 /**
6869 * Toolbars are complex interface components that permit users to easily access a variety
6870 * of {@link OO.ui.Tool tools} (e.g., formatting commands) and actions, which are additional commands that are
6871 * part of the toolbar, but not configured as tools.
6872 *
6873 * Individual tools are customized and then registered with a {@link OO.ui.ToolFactory tool factory}, which creates
6874 * the tools on demand. Each tool has a symbolic name (used when registering the tool), a title (e.g., ‘Insert
6875 * picture’), and an icon.
6876 *
6877 * Individual tools are organized in {@link OO.ui.ToolGroup toolgroups}, which can be {@link OO.ui.MenuToolGroup menus}
6878 * of tools, {@link OO.ui.ListToolGroup lists} of tools, or a single {@link OO.ui.BarToolGroup bar} of tools.
6879 * The arrangement and order of the toolgroups is customized when the toolbar is set up. Tools can be presented in
6880 * any order, but each can only appear once in the toolbar.
6881 *
6882 * The following is an example of a basic toolbar.
6883 *
6884 * @example
6885 * // Example of a toolbar
6886 * // Create the toolbar
6887 * var toolFactory = new OO.ui.ToolFactory();
6888 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
6889 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
6890 *
6891 * // We will be placing status text in this element when tools are used
6892 * var $area = $( '<p>' ).text( 'Toolbar example' );
6893 *
6894 * // Define the tools that we're going to place in our toolbar
6895 *
6896 * // Create a class inheriting from OO.ui.Tool
6897 * function PictureTool() {
6898 * PictureTool.parent.apply( this, arguments );
6899 * }
6900 * OO.inheritClass( PictureTool, OO.ui.Tool );
6901 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
6902 * // of 'icon' and 'title' (displayed icon and text).
6903 * PictureTool.static.name = 'picture';
6904 * PictureTool.static.icon = 'picture';
6905 * PictureTool.static.title = 'Insert picture';
6906 * // Defines the action that will happen when this tool is selected (clicked).
6907 * PictureTool.prototype.onSelect = function () {
6908 * $area.text( 'Picture tool clicked!' );
6909 * // Never display this tool as "active" (selected).
6910 * this.setActive( false );
6911 * };
6912 * // Make this tool available in our toolFactory and thus our toolbar
6913 * toolFactory.register( PictureTool );
6914 *
6915 * // Register two more tools, nothing interesting here
6916 * function SettingsTool() {
6917 * SettingsTool.parent.apply( this, arguments );
6918 * }
6919 * OO.inheritClass( SettingsTool, OO.ui.Tool );
6920 * SettingsTool.static.name = 'settings';
6921 * SettingsTool.static.icon = 'settings';
6922 * SettingsTool.static.title = 'Change settings';
6923 * SettingsTool.prototype.onSelect = function () {
6924 * $area.text( 'Settings tool clicked!' );
6925 * this.setActive( false );
6926 * };
6927 * toolFactory.register( SettingsTool );
6928 *
6929 * // Register two more tools, nothing interesting here
6930 * function StuffTool() {
6931 * StuffTool.parent.apply( this, arguments );
6932 * }
6933 * OO.inheritClass( StuffTool, OO.ui.Tool );
6934 * StuffTool.static.name = 'stuff';
6935 * StuffTool.static.icon = 'ellipsis';
6936 * StuffTool.static.title = 'More stuff';
6937 * StuffTool.prototype.onSelect = function () {
6938 * $area.text( 'More stuff tool clicked!' );
6939 * this.setActive( false );
6940 * };
6941 * toolFactory.register( StuffTool );
6942 *
6943 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
6944 * // little popup window (a PopupWidget).
6945 * function HelpTool( toolGroup, config ) {
6946 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
6947 * padded: true,
6948 * label: 'Help',
6949 * head: true
6950 * } }, config ) );
6951 * this.popup.$body.append( '<p>I am helpful!</p>' );
6952 * }
6953 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
6954 * HelpTool.static.name = 'help';
6955 * HelpTool.static.icon = 'help';
6956 * HelpTool.static.title = 'Help';
6957 * toolFactory.register( HelpTool );
6958 *
6959 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
6960 * // used once (but not all defined tools must be used).
6961 * toolbar.setup( [
6962 * {
6963 * // 'bar' tool groups display tools' icons only, side-by-side.
6964 * type: 'bar',
6965 * include: [ 'picture', 'help' ]
6966 * },
6967 * {
6968 * // 'list' tool groups display both the titles and icons, in a dropdown list.
6969 * type: 'list',
6970 * indicator: 'down',
6971 * label: 'More',
6972 * include: [ 'settings', 'stuff' ]
6973 * }
6974 * // Note how the tools themselves are toolgroup-agnostic - the same tool can be displayed
6975 * // either in a 'list' or a 'bar'. There is a 'menu' tool group too, not showcased here,
6976 * // since it's more complicated to use. (See the next example snippet on this page.)
6977 * ] );
6978 *
6979 * // Create some UI around the toolbar and place it in the document
6980 * var frame = new OO.ui.PanelLayout( {
6981 * expanded: false,
6982 * framed: true
6983 * } );
6984 * var contentFrame = new OO.ui.PanelLayout( {
6985 * expanded: false,
6986 * padded: true
6987 * } );
6988 * frame.$element.append(
6989 * toolbar.$element,
6990 * contentFrame.$element.append( $area )
6991 * );
6992 * $( 'body' ).append( frame.$element );
6993 *
6994 * // Here is where the toolbar is actually built. This must be done after inserting it into the
6995 * // document.
6996 * toolbar.initialize();
6997 *
6998 * The following example extends the previous one to illustrate 'menu' toolgroups and the usage of
6999 * 'updateState' event.
7000 *
7001 * @example
7002 * // Create the toolbar
7003 * var toolFactory = new OO.ui.ToolFactory();
7004 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
7005 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
7006 *
7007 * // We will be placing status text in this element when tools are used
7008 * var $area = $( '<p>' ).text( 'Toolbar example' );
7009 *
7010 * // Define the tools that we're going to place in our toolbar
7011 *
7012 * // Create a class inheriting from OO.ui.Tool
7013 * function PictureTool() {
7014 * PictureTool.parent.apply( this, arguments );
7015 * }
7016 * OO.inheritClass( PictureTool, OO.ui.Tool );
7017 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
7018 * // of 'icon' and 'title' (displayed icon and text).
7019 * PictureTool.static.name = 'picture';
7020 * PictureTool.static.icon = 'picture';
7021 * PictureTool.static.title = 'Insert picture';
7022 * // Defines the action that will happen when this tool is selected (clicked).
7023 * PictureTool.prototype.onSelect = function () {
7024 * $area.text( 'Picture tool clicked!' );
7025 * // Never display this tool as "active" (selected).
7026 * this.setActive( false );
7027 * };
7028 * // The toolbar can be synchronized with the state of some external stuff, like a text
7029 * // editor's editing area, highlighting the tools (e.g. a 'bold' tool would be shown as active
7030 * // when the text cursor was inside bolded text). Here we simply disable this feature.
7031 * PictureTool.prototype.onUpdateState = function () {
7032 * };
7033 * // Make this tool available in our toolFactory and thus our toolbar
7034 * toolFactory.register( PictureTool );
7035 *
7036 * // Register two more tools, nothing interesting here
7037 * function SettingsTool() {
7038 * SettingsTool.parent.apply( this, arguments );
7039 * this.reallyActive = false;
7040 * }
7041 * OO.inheritClass( SettingsTool, OO.ui.Tool );
7042 * SettingsTool.static.name = 'settings';
7043 * SettingsTool.static.icon = 'settings';
7044 * SettingsTool.static.title = 'Change settings';
7045 * SettingsTool.prototype.onSelect = function () {
7046 * $area.text( 'Settings tool clicked!' );
7047 * // Toggle the active state on each click
7048 * this.reallyActive = !this.reallyActive;
7049 * this.setActive( this.reallyActive );
7050 * // To update the menu label
7051 * this.toolbar.emit( 'updateState' );
7052 * };
7053 * SettingsTool.prototype.onUpdateState = function () {
7054 * };
7055 * toolFactory.register( SettingsTool );
7056 *
7057 * // Register two more tools, nothing interesting here
7058 * function StuffTool() {
7059 * StuffTool.parent.apply( this, arguments );
7060 * this.reallyActive = false;
7061 * }
7062 * OO.inheritClass( StuffTool, OO.ui.Tool );
7063 * StuffTool.static.name = 'stuff';
7064 * StuffTool.static.icon = 'ellipsis';
7065 * StuffTool.static.title = 'More stuff';
7066 * StuffTool.prototype.onSelect = function () {
7067 * $area.text( 'More stuff tool clicked!' );
7068 * // Toggle the active state on each click
7069 * this.reallyActive = !this.reallyActive;
7070 * this.setActive( this.reallyActive );
7071 * // To update the menu label
7072 * this.toolbar.emit( 'updateState' );
7073 * };
7074 * StuffTool.prototype.onUpdateState = function () {
7075 * };
7076 * toolFactory.register( StuffTool );
7077 *
7078 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
7079 * // little popup window (a PopupWidget). 'onUpdateState' is also already implemented.
7080 * function HelpTool( toolGroup, config ) {
7081 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
7082 * padded: true,
7083 * label: 'Help',
7084 * head: true
7085 * } }, config ) );
7086 * this.popup.$body.append( '<p>I am helpful!</p>' );
7087 * }
7088 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
7089 * HelpTool.static.name = 'help';
7090 * HelpTool.static.icon = 'help';
7091 * HelpTool.static.title = 'Help';
7092 * toolFactory.register( HelpTool );
7093 *
7094 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
7095 * // used once (but not all defined tools must be used).
7096 * toolbar.setup( [
7097 * {
7098 * // 'bar' tool groups display tools' icons only, side-by-side.
7099 * type: 'bar',
7100 * include: [ 'picture', 'help' ]
7101 * },
7102 * {
7103 * // 'menu' tool groups display both the titles and icons, in a dropdown menu.
7104 * // Menu label indicates which items are selected.
7105 * type: 'menu',
7106 * indicator: 'down',
7107 * include: [ 'settings', 'stuff' ]
7108 * }
7109 * ] );
7110 *
7111 * // Create some UI around the toolbar and place it in the document
7112 * var frame = new OO.ui.PanelLayout( {
7113 * expanded: false,
7114 * framed: true
7115 * } );
7116 * var contentFrame = new OO.ui.PanelLayout( {
7117 * expanded: false,
7118 * padded: true
7119 * } );
7120 * frame.$element.append(
7121 * toolbar.$element,
7122 * contentFrame.$element.append( $area )
7123 * );
7124 * $( 'body' ).append( frame.$element );
7125 *
7126 * // Here is where the toolbar is actually built. This must be done after inserting it into the
7127 * // document.
7128 * toolbar.initialize();
7129 * toolbar.emit( 'updateState' );
7130 *
7131 * @class
7132 * @extends OO.ui.Element
7133 * @mixins OO.EventEmitter
7134 * @mixins OO.ui.mixin.GroupElement
7135 *
7136 * @constructor
7137 * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
7138 * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating toolgroups
7139 * @param {Object} [config] Configuration options
7140 * @cfg {boolean} [actions] Add an actions section to the toolbar. Actions are commands that are included
7141 * in the toolbar, but are not configured as tools. By default, actions are displayed on the right side of
7142 * the toolbar.
7143 * @cfg {boolean} [shadow] Add a shadow below the toolbar.
7144 */
7145 OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
7146 // Allow passing positional parameters inside the config object
7147 if ( OO.isPlainObject( toolFactory ) && config === undefined ) {
7148 config = toolFactory;
7149 toolFactory = config.toolFactory;
7150 toolGroupFactory = config.toolGroupFactory;
7151 }
7152
7153 // Configuration initialization
7154 config = config || {};
7155
7156 // Parent constructor
7157 OO.ui.Toolbar.parent.call( this, config );
7158
7159 // Mixin constructors
7160 OO.EventEmitter.call( this );
7161 OO.ui.mixin.GroupElement.call( this, config );
7162
7163 // Properties
7164 this.toolFactory = toolFactory;
7165 this.toolGroupFactory = toolGroupFactory;
7166 this.groups = [];
7167 this.tools = {};
7168 this.$bar = $( '<div>' );
7169 this.$actions = $( '<div>' );
7170 this.initialized = false;
7171 this.onWindowResizeHandler = this.onWindowResize.bind( this );
7172
7173 // Events
7174 this.$element
7175 .add( this.$bar ).add( this.$group ).add( this.$actions )
7176 .on( 'mousedown keydown', this.onPointerDown.bind( this ) );
7177
7178 // Initialization
7179 this.$group.addClass( 'oo-ui-toolbar-tools' );
7180 if ( config.actions ) {
7181 this.$bar.append( this.$actions.addClass( 'oo-ui-toolbar-actions' ) );
7182 }
7183 this.$bar
7184 .addClass( 'oo-ui-toolbar-bar' )
7185 .append( this.$group, '<div style="clear:both"></div>' );
7186 if ( config.shadow ) {
7187 this.$bar.append( '<div class="oo-ui-toolbar-shadow"></div>' );
7188 }
7189 this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar );
7190 };
7191
7192 /* Setup */
7193
7194 OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
7195 OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
7196 OO.mixinClass( OO.ui.Toolbar, OO.ui.mixin.GroupElement );
7197
7198 /* Methods */
7199
7200 /**
7201 * Get the tool factory.
7202 *
7203 * @return {OO.ui.ToolFactory} Tool factory
7204 */
7205 OO.ui.Toolbar.prototype.getToolFactory = function () {
7206 return this.toolFactory;
7207 };
7208
7209 /**
7210 * Get the toolgroup factory.
7211 *
7212 * @return {OO.Factory} Toolgroup factory
7213 */
7214 OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
7215 return this.toolGroupFactory;
7216 };
7217
7218 /**
7219 * Handles mouse down events.
7220 *
7221 * @private
7222 * @param {jQuery.Event} e Mouse down event
7223 */
7224 OO.ui.Toolbar.prototype.onPointerDown = function ( e ) {
7225 var $closestWidgetToEvent = $( e.target ).closest( '.oo-ui-widget' ),
7226 $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
7227 if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[ 0 ] === $closestWidgetToToolbar[ 0 ] ) {
7228 return false;
7229 }
7230 };
7231
7232 /**
7233 * Handle window resize event.
7234 *
7235 * @private
7236 * @param {jQuery.Event} e Window resize event
7237 */
7238 OO.ui.Toolbar.prototype.onWindowResize = function () {
7239 this.$element.toggleClass(
7240 'oo-ui-toolbar-narrow',
7241 this.$bar.width() <= this.narrowThreshold
7242 );
7243 };
7244
7245 /**
7246 * Sets up handles and preloads required information for the toolbar to work.
7247 * This must be called after it is attached to a visible document and before doing anything else.
7248 */
7249 OO.ui.Toolbar.prototype.initialize = function () {
7250 this.initialized = true;
7251 this.narrowThreshold = this.$group.width() + this.$actions.width();
7252 $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
7253 this.onWindowResize();
7254 };
7255
7256 /**
7257 * Set up the toolbar.
7258 *
7259 * The toolbar is set up with a list of toolgroup configurations that specify the type of
7260 * toolgroup ({@link OO.ui.BarToolGroup bar}, {@link OO.ui.MenuToolGroup menu}, or {@link OO.ui.ListToolGroup list})
7261 * to add and which tools to include, exclude, promote, or demote within that toolgroup. Please
7262 * see {@link OO.ui.ToolGroup toolgroups} for more information about including tools in toolgroups.
7263 *
7264 * @param {Object.<string,Array>} groups List of toolgroup configurations
7265 * @param {Array|string} [groups.include] Tools to include in the toolgroup
7266 * @param {Array|string} [groups.exclude] Tools to exclude from the toolgroup
7267 * @param {Array|string} [groups.promote] Tools to promote to the beginning of the toolgroup
7268 * @param {Array|string} [groups.demote] Tools to demote to the end of the toolgroup
7269 */
7270 OO.ui.Toolbar.prototype.setup = function ( groups ) {
7271 var i, len, type, group,
7272 items = [],
7273 defaultType = 'bar';
7274
7275 // Cleanup previous groups
7276 this.reset();
7277
7278 // Build out new groups
7279 for ( i = 0, len = groups.length; i < len; i++ ) {
7280 group = groups[ i ];
7281 if ( group.include === '*' ) {
7282 // Apply defaults to catch-all groups
7283 if ( group.type === undefined ) {
7284 group.type = 'list';
7285 }
7286 if ( group.label === undefined ) {
7287 group.label = OO.ui.msg( 'ooui-toolbar-more' );
7288 }
7289 }
7290 // Check type has been registered
7291 type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType;
7292 items.push(
7293 this.getToolGroupFactory().create( type, this, group )
7294 );
7295 }
7296 this.addItems( items );
7297 };
7298
7299 /**
7300 * Remove all tools and toolgroups from the toolbar.
7301 */
7302 OO.ui.Toolbar.prototype.reset = function () {
7303 var i, len;
7304
7305 this.groups = [];
7306 this.tools = {};
7307 for ( i = 0, len = this.items.length; i < len; i++ ) {
7308 this.items[ i ].destroy();
7309 }
7310 this.clearItems();
7311 };
7312
7313 /**
7314 * Destroy the toolbar.
7315 *
7316 * Destroying the toolbar removes all event handlers and DOM elements that constitute the toolbar. Call
7317 * this method whenever you are done using a toolbar.
7318 */
7319 OO.ui.Toolbar.prototype.destroy = function () {
7320 $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
7321 this.reset();
7322 this.$element.remove();
7323 };
7324
7325 /**
7326 * Check if the tool is available.
7327 *
7328 * Available tools are ones that have not yet been added to the toolbar.
7329 *
7330 * @param {string} name Symbolic name of tool
7331 * @return {boolean} Tool is available
7332 */
7333 OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
7334 return !this.tools[ name ];
7335 };
7336
7337 /**
7338 * Prevent tool from being used again.
7339 *
7340 * @param {OO.ui.Tool} tool Tool to reserve
7341 */
7342 OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
7343 this.tools[ tool.getName() ] = tool;
7344 };
7345
7346 /**
7347 * Allow tool to be used again.
7348 *
7349 * @param {OO.ui.Tool} tool Tool to release
7350 */
7351 OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
7352 delete this.tools[ tool.getName() ];
7353 };
7354
7355 /**
7356 * Get accelerator label for tool.
7357 *
7358 * The OOjs UI library does not contain an accelerator system, but this is the hook for one. To
7359 * use an accelerator system, subclass the toolbar and override this method, which is meant to return a label
7360 * that describes the accelerator keys for the tool passed (by symbolic name) to the method.
7361 *
7362 * @param {string} name Symbolic name of tool
7363 * @return {string|undefined} Tool accelerator label if available
7364 */
7365 OO.ui.Toolbar.prototype.getToolAccelerator = function () {
7366 return undefined;
7367 };
7368
7369 /**
7370 * ToolGroups are collections of {@link OO.ui.Tool tools} that are used in a {@link OO.ui.Toolbar toolbar}.
7371 * The type of toolgroup ({@link OO.ui.ListToolGroup list}, {@link OO.ui.BarToolGroup bar}, or {@link OO.ui.MenuToolGroup menu})
7372 * to which a tool belongs determines how the tool is arranged and displayed in the toolbar. Toolgroups
7373 * themselves are created on demand with a {@link OO.ui.ToolGroupFactory toolgroup factory}.
7374 *
7375 * Toolgroups can contain individual tools, groups of tools, or all available tools:
7376 *
7377 * To include an individual tool (or array of individual tools), specify tools by symbolic name:
7378 *
7379 * include: [ 'tool-name' ] or [ { name: 'tool-name' }]
7380 *
7381 * To include a group of tools, specify the group name. (The tool's static ‘group’ config is used to assign the tool to a group.)
7382 *
7383 * include: [ { group: 'group-name' } ]
7384 *
7385 * To include all tools that are not yet assigned to a toolgroup, use the catch-all selector, an asterisk (*):
7386 *
7387 * include: '*'
7388 *
7389 * See {@link OO.ui.Toolbar toolbars} for a full example. For more information about toolbars in general,
7390 * please see the [OOjs UI documentation on MediaWiki][1].
7391 *
7392 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
7393 *
7394 * @abstract
7395 * @class
7396 * @extends OO.ui.Widget
7397 * @mixins OO.ui.mixin.GroupElement
7398 *
7399 * @constructor
7400 * @param {OO.ui.Toolbar} toolbar
7401 * @param {Object} [config] Configuration options
7402 * @cfg {Array|string} [include=[]] List of tools to include in the toolgroup.
7403 * @cfg {Array|string} [exclude=[]] List of tools to exclude from the toolgroup.
7404 * @cfg {Array|string} [promote=[]] List of tools to promote to the beginning of the toolgroup.
7405 * @cfg {Array|string} [demote=[]] List of tools to demote to the end of the toolgroup.
7406 * This setting is particularly useful when tools have been added to the toolgroup
7407 * en masse (e.g., via the catch-all selector).
7408 */
7409 OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
7410 // Allow passing positional parameters inside the config object
7411 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
7412 config = toolbar;
7413 toolbar = config.toolbar;
7414 }
7415
7416 // Configuration initialization
7417 config = config || {};
7418
7419 // Parent constructor
7420 OO.ui.ToolGroup.parent.call( this, config );
7421
7422 // Mixin constructors
7423 OO.ui.mixin.GroupElement.call( this, config );
7424
7425 // Properties
7426 this.toolbar = toolbar;
7427 this.tools = {};
7428 this.pressed = null;
7429 this.autoDisabled = false;
7430 this.include = config.include || [];
7431 this.exclude = config.exclude || [];
7432 this.promote = config.promote || [];
7433 this.demote = config.demote || [];
7434 this.onCapturedMouseKeyUpHandler = this.onCapturedMouseKeyUp.bind( this );
7435
7436 // Events
7437 this.$element.on( {
7438 mousedown: this.onMouseKeyDown.bind( this ),
7439 mouseup: this.onMouseKeyUp.bind( this ),
7440 keydown: this.onMouseKeyDown.bind( this ),
7441 keyup: this.onMouseKeyUp.bind( this ),
7442 focus: this.onMouseOverFocus.bind( this ),
7443 blur: this.onMouseOutBlur.bind( this ),
7444 mouseover: this.onMouseOverFocus.bind( this ),
7445 mouseout: this.onMouseOutBlur.bind( this )
7446 } );
7447 this.toolbar.getToolFactory().connect( this, { register: 'onToolFactoryRegister' } );
7448 this.aggregate( { disable: 'itemDisable' } );
7449 this.connect( this, { itemDisable: 'updateDisabled' } );
7450
7451 // Initialization
7452 this.$group.addClass( 'oo-ui-toolGroup-tools' );
7453 this.$element
7454 .addClass( 'oo-ui-toolGroup' )
7455 .append( this.$group );
7456 this.populate();
7457 };
7458
7459 /* Setup */
7460
7461 OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget );
7462 OO.mixinClass( OO.ui.ToolGroup, OO.ui.mixin.GroupElement );
7463
7464 /* Events */
7465
7466 /**
7467 * @event update
7468 */
7469
7470 /* Static Properties */
7471
7472 /**
7473 * Show labels in tooltips.
7474 *
7475 * @static
7476 * @inheritable
7477 * @property {boolean}
7478 */
7479 OO.ui.ToolGroup.static.titleTooltips = false;
7480
7481 /**
7482 * Show acceleration labels in tooltips.
7483 *
7484 * Note: The OOjs UI library does not include an accelerator system, but does contain
7485 * a hook for one. To use an accelerator system, subclass the {@link OO.ui.Toolbar toolbar} and
7486 * override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method, which is
7487 * meant to return a label that describes the accelerator keys for a given tool (e.g., 'Ctrl + M').
7488 *
7489 * @static
7490 * @inheritable
7491 * @property {boolean}
7492 */
7493 OO.ui.ToolGroup.static.accelTooltips = false;
7494
7495 /**
7496 * Automatically disable the toolgroup when all tools are disabled
7497 *
7498 * @static
7499 * @inheritable
7500 * @property {boolean}
7501 */
7502 OO.ui.ToolGroup.static.autoDisable = true;
7503
7504 /* Methods */
7505
7506 /**
7507 * @inheritdoc
7508 */
7509 OO.ui.ToolGroup.prototype.isDisabled = function () {
7510 return this.autoDisabled || OO.ui.ToolGroup.parent.prototype.isDisabled.apply( this, arguments );
7511 };
7512
7513 /**
7514 * @inheritdoc
7515 */
7516 OO.ui.ToolGroup.prototype.updateDisabled = function () {
7517 var i, item, allDisabled = true;
7518
7519 if ( this.constructor.static.autoDisable ) {
7520 for ( i = this.items.length - 1; i >= 0; i-- ) {
7521 item = this.items[ i ];
7522 if ( !item.isDisabled() ) {
7523 allDisabled = false;
7524 break;
7525 }
7526 }
7527 this.autoDisabled = allDisabled;
7528 }
7529 OO.ui.ToolGroup.parent.prototype.updateDisabled.apply( this, arguments );
7530 };
7531
7532 /**
7533 * Handle mouse down and key down events.
7534 *
7535 * @protected
7536 * @param {jQuery.Event} e Mouse down or key down event
7537 */
7538 OO.ui.ToolGroup.prototype.onMouseKeyDown = function ( e ) {
7539 if (
7540 !this.isDisabled() &&
7541 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
7542 ) {
7543 this.pressed = this.getTargetTool( e );
7544 if ( this.pressed ) {
7545 this.pressed.setActive( true );
7546 this.getElementDocument().addEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler, true );
7547 this.getElementDocument().addEventListener( 'keyup', this.onCapturedMouseKeyUpHandler, true );
7548 }
7549 return false;
7550 }
7551 };
7552
7553 /**
7554 * Handle captured mouse up and key up events.
7555 *
7556 * @protected
7557 * @param {Event} e Mouse up or key up event
7558 */
7559 OO.ui.ToolGroup.prototype.onCapturedMouseKeyUp = function ( e ) {
7560 this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler, true );
7561 this.getElementDocument().removeEventListener( 'keyup', this.onCapturedMouseKeyUpHandler, true );
7562 // onMouseKeyUp may be called a second time, depending on where the mouse is when the button is
7563 // released, but since `this.pressed` will no longer be true, the second call will be ignored.
7564 this.onMouseKeyUp( e );
7565 };
7566
7567 /**
7568 * Handle mouse up and key up events.
7569 *
7570 * @protected
7571 * @param {jQuery.Event} e Mouse up or key up event
7572 */
7573 OO.ui.ToolGroup.prototype.onMouseKeyUp = function ( e ) {
7574 var tool = this.getTargetTool( e );
7575
7576 if (
7577 !this.isDisabled() && this.pressed && this.pressed === tool &&
7578 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
7579 ) {
7580 this.pressed.onSelect();
7581 this.pressed = null;
7582 return false;
7583 }
7584
7585 this.pressed = null;
7586 };
7587
7588 /**
7589 * Handle mouse over and focus events.
7590 *
7591 * @protected
7592 * @param {jQuery.Event} e Mouse over or focus event
7593 */
7594 OO.ui.ToolGroup.prototype.onMouseOverFocus = function ( e ) {
7595 var tool = this.getTargetTool( e );
7596
7597 if ( this.pressed && this.pressed === tool ) {
7598 this.pressed.setActive( true );
7599 }
7600 };
7601
7602 /**
7603 * Handle mouse out and blur events.
7604 *
7605 * @protected
7606 * @param {jQuery.Event} e Mouse out or blur event
7607 */
7608 OO.ui.ToolGroup.prototype.onMouseOutBlur = function ( e ) {
7609 var tool = this.getTargetTool( e );
7610
7611 if ( this.pressed && this.pressed === tool ) {
7612 this.pressed.setActive( false );
7613 }
7614 };
7615
7616 /**
7617 * Get the closest tool to a jQuery.Event.
7618 *
7619 * Only tool links are considered, which prevents other elements in the tool such as popups from
7620 * triggering tool group interactions.
7621 *
7622 * @private
7623 * @param {jQuery.Event} e
7624 * @return {OO.ui.Tool|null} Tool, `null` if none was found
7625 */
7626 OO.ui.ToolGroup.prototype.getTargetTool = function ( e ) {
7627 var tool,
7628 $item = $( e.target ).closest( '.oo-ui-tool-link' );
7629
7630 if ( $item.length ) {
7631 tool = $item.parent().data( 'oo-ui-tool' );
7632 }
7633
7634 return tool && !tool.isDisabled() ? tool : null;
7635 };
7636
7637 /**
7638 * Handle tool registry register events.
7639 *
7640 * If a tool is registered after the group is created, we must repopulate the list to account for:
7641 *
7642 * - a tool being added that may be included
7643 * - a tool already included being overridden
7644 *
7645 * @protected
7646 * @param {string} name Symbolic name of tool
7647 */
7648 OO.ui.ToolGroup.prototype.onToolFactoryRegister = function () {
7649 this.populate();
7650 };
7651
7652 /**
7653 * Get the toolbar that contains the toolgroup.
7654 *
7655 * @return {OO.ui.Toolbar} Toolbar that contains the toolgroup
7656 */
7657 OO.ui.ToolGroup.prototype.getToolbar = function () {
7658 return this.toolbar;
7659 };
7660
7661 /**
7662 * Add and remove tools based on configuration.
7663 */
7664 OO.ui.ToolGroup.prototype.populate = function () {
7665 var i, len, name, tool,
7666 toolFactory = this.toolbar.getToolFactory(),
7667 names = {},
7668 add = [],
7669 remove = [],
7670 list = this.toolbar.getToolFactory().getTools(
7671 this.include, this.exclude, this.promote, this.demote
7672 );
7673
7674 // Build a list of needed tools
7675 for ( i = 0, len = list.length; i < len; i++ ) {
7676 name = list[ i ];
7677 if (
7678 // Tool exists
7679 toolFactory.lookup( name ) &&
7680 // Tool is available or is already in this group
7681 ( this.toolbar.isToolAvailable( name ) || this.tools[ name ] )
7682 ) {
7683 // Hack to prevent infinite recursion via ToolGroupTool. We need to reserve the tool before
7684 // creating it, but we can't call reserveTool() yet because we haven't created the tool.
7685 this.toolbar.tools[ name ] = true;
7686 tool = this.tools[ name ];
7687 if ( !tool ) {
7688 // Auto-initialize tools on first use
7689 this.tools[ name ] = tool = toolFactory.create( name, this );
7690 tool.updateTitle();
7691 }
7692 this.toolbar.reserveTool( tool );
7693 add.push( tool );
7694 names[ name ] = true;
7695 }
7696 }
7697 // Remove tools that are no longer needed
7698 for ( name in this.tools ) {
7699 if ( !names[ name ] ) {
7700 this.tools[ name ].destroy();
7701 this.toolbar.releaseTool( this.tools[ name ] );
7702 remove.push( this.tools[ name ] );
7703 delete this.tools[ name ];
7704 }
7705 }
7706 if ( remove.length ) {
7707 this.removeItems( remove );
7708 }
7709 // Update emptiness state
7710 if ( add.length ) {
7711 this.$element.removeClass( 'oo-ui-toolGroup-empty' );
7712 } else {
7713 this.$element.addClass( 'oo-ui-toolGroup-empty' );
7714 }
7715 // Re-add tools (moving existing ones to new locations)
7716 this.addItems( add );
7717 // Disabled state may depend on items
7718 this.updateDisabled();
7719 };
7720
7721 /**
7722 * Destroy toolgroup.
7723 */
7724 OO.ui.ToolGroup.prototype.destroy = function () {
7725 var name;
7726
7727 this.clearItems();
7728 this.toolbar.getToolFactory().disconnect( this );
7729 for ( name in this.tools ) {
7730 this.toolbar.releaseTool( this.tools[ name ] );
7731 this.tools[ name ].disconnect( this ).destroy();
7732 delete this.tools[ name ];
7733 }
7734 this.$element.remove();
7735 };
7736
7737 /**
7738 * MessageDialogs display a confirmation or alert message. By default, the rendered dialog box
7739 * consists of a header that contains the dialog title, a body with the message, and a footer that
7740 * contains any {@link OO.ui.ActionWidget action widgets}. The MessageDialog class is the only type
7741 * of {@link OO.ui.Dialog dialog} that is usually instantiated directly.
7742 *
7743 * There are two basic types of message dialogs, confirmation and alert:
7744 *
7745 * - **confirmation**: the dialog title describes what a progressive action will do and the message provides
7746 * more details about the consequences.
7747 * - **alert**: the dialog title describes which event occurred and the message provides more information
7748 * about why the event occurred.
7749 *
7750 * The MessageDialog class specifies two actions: ‘accept’, the primary
7751 * action (e.g., ‘ok’) and ‘reject,’ the safe action (e.g., ‘cancel’). Both will close the window,
7752 * passing along the selected action.
7753 *
7754 * For more information and examples, please see the [OOjs UI documentation on MediaWiki][1].
7755 *
7756 * @example
7757 * // Example: Creating and opening a message dialog window.
7758 * var messageDialog = new OO.ui.MessageDialog();
7759 *
7760 * // Create and append a window manager.
7761 * var windowManager = new OO.ui.WindowManager();
7762 * $( 'body' ).append( windowManager.$element );
7763 * windowManager.addWindows( [ messageDialog ] );
7764 * // Open the window.
7765 * windowManager.openWindow( messageDialog, {
7766 * title: 'Basic message dialog',
7767 * message: 'This is the message'
7768 * } );
7769 *
7770 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Message_Dialogs
7771 *
7772 * @class
7773 * @extends OO.ui.Dialog
7774 *
7775 * @constructor
7776 * @param {Object} [config] Configuration options
7777 */
7778 OO.ui.MessageDialog = function OoUiMessageDialog( config ) {
7779 // Parent constructor
7780 OO.ui.MessageDialog.parent.call( this, config );
7781
7782 // Properties
7783 this.verticalActionLayout = null;
7784
7785 // Initialization
7786 this.$element.addClass( 'oo-ui-messageDialog' );
7787 };
7788
7789 /* Inheritance */
7790
7791 OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog );
7792
7793 /* Static Properties */
7794
7795 OO.ui.MessageDialog.static.name = 'message';
7796
7797 OO.ui.MessageDialog.static.size = 'small';
7798
7799 OO.ui.MessageDialog.static.verbose = false;
7800
7801 /**
7802 * Dialog title.
7803 *
7804 * The title of a confirmation dialog describes what a progressive action will do. The
7805 * title of an alert dialog describes which event occurred.
7806 *
7807 * @static
7808 * @inheritable
7809 * @property {jQuery|string|Function|null}
7810 */
7811 OO.ui.MessageDialog.static.title = null;
7812
7813 /**
7814 * The message displayed in the dialog body.
7815 *
7816 * A confirmation message describes the consequences of a progressive action. An alert
7817 * message describes why an event occurred.
7818 *
7819 * @static
7820 * @inheritable
7821 * @property {jQuery|string|Function|null}
7822 */
7823 OO.ui.MessageDialog.static.message = null;
7824
7825 OO.ui.MessageDialog.static.actions = [
7826 { action: 'accept', label: OO.ui.deferMsg( 'ooui-dialog-message-accept' ), flags: 'primary' },
7827 { action: 'reject', label: OO.ui.deferMsg( 'ooui-dialog-message-reject' ), flags: 'safe' }
7828 ];
7829
7830 /* Methods */
7831
7832 /**
7833 * @inheritdoc
7834 */
7835 OO.ui.MessageDialog.prototype.setManager = function ( manager ) {
7836 OO.ui.MessageDialog.parent.prototype.setManager.call( this, manager );
7837
7838 // Events
7839 this.manager.connect( this, {
7840 resize: 'onResize'
7841 } );
7842
7843 return this;
7844 };
7845
7846 /**
7847 * @inheritdoc
7848 */
7849 OO.ui.MessageDialog.prototype.onActionResize = function ( action ) {
7850 this.fitActions();
7851 return OO.ui.MessageDialog.parent.prototype.onActionResize.call( this, action );
7852 };
7853
7854 /**
7855 * Handle window resized events.
7856 *
7857 * @private
7858 */
7859 OO.ui.MessageDialog.prototype.onResize = function () {
7860 var dialog = this;
7861 dialog.fitActions();
7862 // Wait for CSS transition to finish and do it again :(
7863 setTimeout( function () {
7864 dialog.fitActions();
7865 }, 300 );
7866 };
7867
7868 /**
7869 * Toggle action layout between vertical and horizontal.
7870 *
7871 *
7872 * @private
7873 * @param {boolean} [value] Layout actions vertically, omit to toggle
7874 * @chainable
7875 */
7876 OO.ui.MessageDialog.prototype.toggleVerticalActionLayout = function ( value ) {
7877 value = value === undefined ? !this.verticalActionLayout : !!value;
7878
7879 if ( value !== this.verticalActionLayout ) {
7880 this.verticalActionLayout = value;
7881 this.$actions
7882 .toggleClass( 'oo-ui-messageDialog-actions-vertical', value )
7883 .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value );
7884 }
7885
7886 return this;
7887 };
7888
7889 /**
7890 * @inheritdoc
7891 */
7892 OO.ui.MessageDialog.prototype.getActionProcess = function ( action ) {
7893 if ( action ) {
7894 return new OO.ui.Process( function () {
7895 this.close( { action: action } );
7896 }, this );
7897 }
7898 return OO.ui.MessageDialog.parent.prototype.getActionProcess.call( this, action );
7899 };
7900
7901 /**
7902 * @inheritdoc
7903 *
7904 * @param {Object} [data] Dialog opening data
7905 * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
7906 * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
7907 * @param {boolean} [data.verbose] Message is verbose and should be styled as a long message
7908 * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each
7909 * action item
7910 */
7911 OO.ui.MessageDialog.prototype.getSetupProcess = function ( data ) {
7912 data = data || {};
7913
7914 // Parent method
7915 return OO.ui.MessageDialog.parent.prototype.getSetupProcess.call( this, data )
7916 .next( function () {
7917 this.title.setLabel(
7918 data.title !== undefined ? data.title : this.constructor.static.title
7919 );
7920 this.message.setLabel(
7921 data.message !== undefined ? data.message : this.constructor.static.message
7922 );
7923 this.message.$element.toggleClass(
7924 'oo-ui-messageDialog-message-verbose',
7925 data.verbose !== undefined ? data.verbose : this.constructor.static.verbose
7926 );
7927 }, this );
7928 };
7929
7930 /**
7931 * @inheritdoc
7932 */
7933 OO.ui.MessageDialog.prototype.getBodyHeight = function () {
7934 var bodyHeight, oldOverflow,
7935 $scrollable = this.container.$element;
7936
7937 oldOverflow = $scrollable[ 0 ].style.overflow;
7938 $scrollable[ 0 ].style.overflow = 'hidden';
7939
7940 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
7941
7942 bodyHeight = this.text.$element.outerHeight( true );
7943 $scrollable[ 0 ].style.overflow = oldOverflow;
7944
7945 return bodyHeight;
7946 };
7947
7948 /**
7949 * @inheritdoc
7950 */
7951 OO.ui.MessageDialog.prototype.setDimensions = function ( dim ) {
7952 var $scrollable = this.container.$element;
7953 OO.ui.MessageDialog.parent.prototype.setDimensions.call( this, dim );
7954
7955 // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
7956 // Need to do it after transition completes (250ms), add 50ms just in case.
7957 setTimeout( function () {
7958 var oldOverflow = $scrollable[ 0 ].style.overflow;
7959 $scrollable[ 0 ].style.overflow = 'hidden';
7960
7961 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
7962
7963 $scrollable[ 0 ].style.overflow = oldOverflow;
7964 }, 300 );
7965
7966 return this;
7967 };
7968
7969 /**
7970 * @inheritdoc
7971 */
7972 OO.ui.MessageDialog.prototype.initialize = function () {
7973 // Parent method
7974 OO.ui.MessageDialog.parent.prototype.initialize.call( this );
7975
7976 // Properties
7977 this.$actions = $( '<div>' );
7978 this.container = new OO.ui.PanelLayout( {
7979 scrollable: true, classes: [ 'oo-ui-messageDialog-container' ]
7980 } );
7981 this.text = new OO.ui.PanelLayout( {
7982 padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ]
7983 } );
7984 this.message = new OO.ui.LabelWidget( {
7985 classes: [ 'oo-ui-messageDialog-message' ]
7986 } );
7987
7988 // Initialization
7989 this.title.$element.addClass( 'oo-ui-messageDialog-title' );
7990 this.$content.addClass( 'oo-ui-messageDialog-content' );
7991 this.container.$element.append( this.text.$element );
7992 this.text.$element.append( this.title.$element, this.message.$element );
7993 this.$body.append( this.container.$element );
7994 this.$actions.addClass( 'oo-ui-messageDialog-actions' );
7995 this.$foot.append( this.$actions );
7996 };
7997
7998 /**
7999 * @inheritdoc
8000 */
8001 OO.ui.MessageDialog.prototype.attachActions = function () {
8002 var i, len, other, special, others;
8003
8004 // Parent method
8005 OO.ui.MessageDialog.parent.prototype.attachActions.call( this );
8006
8007 special = this.actions.getSpecial();
8008 others = this.actions.getOthers();
8009 if ( special.safe ) {
8010 this.$actions.append( special.safe.$element );
8011 special.safe.toggleFramed( false );
8012 }
8013 if ( others.length ) {
8014 for ( i = 0, len = others.length; i < len; i++ ) {
8015 other = others[ i ];
8016 this.$actions.append( other.$element );
8017 other.toggleFramed( false );
8018 }
8019 }
8020 if ( special.primary ) {
8021 this.$actions.append( special.primary.$element );
8022 special.primary.toggleFramed( false );
8023 }
8024
8025 if ( !this.isOpening() ) {
8026 // If the dialog is currently opening, this will be called automatically soon.
8027 // This also calls #fitActions.
8028 this.updateSize();
8029 }
8030 };
8031
8032 /**
8033 * Fit action actions into columns or rows.
8034 *
8035 * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
8036 *
8037 * @private
8038 */
8039 OO.ui.MessageDialog.prototype.fitActions = function () {
8040 var i, len, action,
8041 previous = this.verticalActionLayout,
8042 actions = this.actions.get();
8043
8044 // Detect clipping
8045 this.toggleVerticalActionLayout( false );
8046 for ( i = 0, len = actions.length; i < len; i++ ) {
8047 action = actions[ i ];
8048 if ( action.$element.innerWidth() < action.$label.outerWidth( true ) ) {
8049 this.toggleVerticalActionLayout( true );
8050 break;
8051 }
8052 }
8053
8054 // Move the body out of the way of the foot
8055 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
8056
8057 if ( this.verticalActionLayout !== previous ) {
8058 // We changed the layout, window height might need to be updated.
8059 this.updateSize();
8060 }
8061 };
8062
8063 /**
8064 * ProcessDialog windows encapsulate a {@link OO.ui.Process process} and all of the code necessary
8065 * to complete it. If the process terminates with an error, a customizable {@link OO.ui.Error error
8066 * interface} alerts users to the trouble, permitting the user to dismiss the error and try again when
8067 * relevant. The ProcessDialog class is always extended and customized with the actions and content
8068 * required for each process.
8069 *
8070 * The process dialog box consists of a header that visually represents the ‘working’ state of long
8071 * processes with an animation. The header contains the dialog title as well as
8072 * two {@link OO.ui.ActionWidget action widgets}: a ‘safe’ action on the left (e.g., ‘Cancel’) and
8073 * a ‘primary’ action on the right (e.g., ‘Done’).
8074 *
8075 * Like other windows, the process dialog is managed by a {@link OO.ui.WindowManager window manager}.
8076 * Please see the [OOjs UI documentation on MediaWiki][1] for more information and examples.
8077 *
8078 * @example
8079 * // Example: Creating and opening a process dialog window.
8080 * function MyProcessDialog( config ) {
8081 * MyProcessDialog.parent.call( this, config );
8082 * }
8083 * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
8084 *
8085 * MyProcessDialog.static.title = 'Process dialog';
8086 * MyProcessDialog.static.actions = [
8087 * { action: 'save', label: 'Done', flags: 'primary' },
8088 * { label: 'Cancel', flags: 'safe' }
8089 * ];
8090 *
8091 * MyProcessDialog.prototype.initialize = function () {
8092 * MyProcessDialog.parent.prototype.initialize.apply( this, arguments );
8093 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
8094 * 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>' );
8095 * this.$body.append( this.content.$element );
8096 * };
8097 * MyProcessDialog.prototype.getActionProcess = function ( action ) {
8098 * var dialog = this;
8099 * if ( action ) {
8100 * return new OO.ui.Process( function () {
8101 * dialog.close( { action: action } );
8102 * } );
8103 * }
8104 * return MyProcessDialog.parent.prototype.getActionProcess.call( this, action );
8105 * };
8106 *
8107 * var windowManager = new OO.ui.WindowManager();
8108 * $( 'body' ).append( windowManager.$element );
8109 *
8110 * var dialog = new MyProcessDialog();
8111 * windowManager.addWindows( [ dialog ] );
8112 * windowManager.openWindow( dialog );
8113 *
8114 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
8115 *
8116 * @abstract
8117 * @class
8118 * @extends OO.ui.Dialog
8119 *
8120 * @constructor
8121 * @param {Object} [config] Configuration options
8122 */
8123 OO.ui.ProcessDialog = function OoUiProcessDialog( config ) {
8124 // Parent constructor
8125 OO.ui.ProcessDialog.parent.call( this, config );
8126
8127 // Initialization
8128 this.$element.addClass( 'oo-ui-processDialog' );
8129 };
8130
8131 /* Setup */
8132
8133 OO.inheritClass( OO.ui.ProcessDialog, OO.ui.Dialog );
8134
8135 /* Methods */
8136
8137 /**
8138 * Handle dismiss button click events.
8139 *
8140 * Hides errors.
8141 *
8142 * @private
8143 */
8144 OO.ui.ProcessDialog.prototype.onDismissErrorButtonClick = function () {
8145 this.hideErrors();
8146 };
8147
8148 /**
8149 * Handle retry button click events.
8150 *
8151 * Hides errors and then tries again.
8152 *
8153 * @private
8154 */
8155 OO.ui.ProcessDialog.prototype.onRetryButtonClick = function () {
8156 this.hideErrors();
8157 this.executeAction( this.currentAction );
8158 };
8159
8160 /**
8161 * @inheritdoc
8162 */
8163 OO.ui.ProcessDialog.prototype.onActionResize = function ( action ) {
8164 if ( this.actions.isSpecial( action ) ) {
8165 this.fitLabel();
8166 }
8167 return OO.ui.ProcessDialog.parent.prototype.onActionResize.call( this, action );
8168 };
8169
8170 /**
8171 * @inheritdoc
8172 */
8173 OO.ui.ProcessDialog.prototype.initialize = function () {
8174 // Parent method
8175 OO.ui.ProcessDialog.parent.prototype.initialize.call( this );
8176
8177 // Properties
8178 this.$navigation = $( '<div>' );
8179 this.$location = $( '<div>' );
8180 this.$safeActions = $( '<div>' );
8181 this.$primaryActions = $( '<div>' );
8182 this.$otherActions = $( '<div>' );
8183 this.dismissButton = new OO.ui.ButtonWidget( {
8184 label: OO.ui.msg( 'ooui-dialog-process-dismiss' )
8185 } );
8186 this.retryButton = new OO.ui.ButtonWidget();
8187 this.$errors = $( '<div>' );
8188 this.$errorsTitle = $( '<div>' );
8189
8190 // Events
8191 this.dismissButton.connect( this, { click: 'onDismissErrorButtonClick' } );
8192 this.retryButton.connect( this, { click: 'onRetryButtonClick' } );
8193
8194 // Initialization
8195 this.title.$element.addClass( 'oo-ui-processDialog-title' );
8196 this.$location
8197 .append( this.title.$element )
8198 .addClass( 'oo-ui-processDialog-location' );
8199 this.$safeActions.addClass( 'oo-ui-processDialog-actions-safe' );
8200 this.$primaryActions.addClass( 'oo-ui-processDialog-actions-primary' );
8201 this.$otherActions.addClass( 'oo-ui-processDialog-actions-other' );
8202 this.$errorsTitle
8203 .addClass( 'oo-ui-processDialog-errors-title' )
8204 .text( OO.ui.msg( 'ooui-dialog-process-error' ) );
8205 this.$errors
8206 .addClass( 'oo-ui-processDialog-errors oo-ui-element-hidden' )
8207 .append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element );
8208 this.$content
8209 .addClass( 'oo-ui-processDialog-content' )
8210 .append( this.$errors );
8211 this.$navigation
8212 .addClass( 'oo-ui-processDialog-navigation' )
8213 .append( this.$safeActions, this.$location, this.$primaryActions );
8214 this.$head.append( this.$navigation );
8215 this.$foot.append( this.$otherActions );
8216 };
8217
8218 /**
8219 * @inheritdoc
8220 */
8221 OO.ui.ProcessDialog.prototype.getActionWidgets = function ( actions ) {
8222 var i, len, widgets = [];
8223 for ( i = 0, len = actions.length; i < len; i++ ) {
8224 widgets.push(
8225 new OO.ui.ActionWidget( $.extend( { framed: true }, actions[ i ] ) )
8226 );
8227 }
8228 return widgets;
8229 };
8230
8231 /**
8232 * @inheritdoc
8233 */
8234 OO.ui.ProcessDialog.prototype.attachActions = function () {
8235 var i, len, other, special, others;
8236
8237 // Parent method
8238 OO.ui.ProcessDialog.parent.prototype.attachActions.call( this );
8239
8240 special = this.actions.getSpecial();
8241 others = this.actions.getOthers();
8242 if ( special.primary ) {
8243 this.$primaryActions.append( special.primary.$element );
8244 }
8245 for ( i = 0, len = others.length; i < len; i++ ) {
8246 other = others[ i ];
8247 this.$otherActions.append( other.$element );
8248 }
8249 if ( special.safe ) {
8250 this.$safeActions.append( special.safe.$element );
8251 }
8252
8253 this.fitLabel();
8254 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
8255 };
8256
8257 /**
8258 * @inheritdoc
8259 */
8260 OO.ui.ProcessDialog.prototype.executeAction = function ( action ) {
8261 var process = this;
8262 return OO.ui.ProcessDialog.parent.prototype.executeAction.call( this, action )
8263 .fail( function ( errors ) {
8264 process.showErrors( errors || [] );
8265 } );
8266 };
8267
8268 /**
8269 * Fit label between actions.
8270 *
8271 * @private
8272 * @chainable
8273 */
8274 OO.ui.ProcessDialog.prototype.fitLabel = function () {
8275 var safeWidth, primaryWidth, biggerWidth, labelWidth, navigationWidth, leftWidth, rightWidth;
8276
8277 safeWidth = this.$safeActions.is( ':visible' ) ? this.$safeActions.width() : 0;
8278 primaryWidth = this.$primaryActions.is( ':visible' ) ? this.$primaryActions.width() : 0;
8279 biggerWidth = Math.max( safeWidth, primaryWidth );
8280
8281 labelWidth = this.title.$element.width();
8282 // Is there a better way to calculate this?
8283 navigationWidth = OO.ui.WindowManager.static.sizes[ this.getSize() ].width - 20;
8284
8285 if ( 2 * biggerWidth + labelWidth < navigationWidth ) {
8286 // We have enough space to center the label
8287 leftWidth = rightWidth = biggerWidth;
8288 } else {
8289 // Let's hope we at least have enough space not to overlap, because we can't wrap the label…
8290 if ( this.getDir() === 'ltr' ) {
8291 leftWidth = safeWidth;
8292 rightWidth = primaryWidth;
8293 } else {
8294 leftWidth = primaryWidth;
8295 rightWidth = safeWidth;
8296 }
8297 }
8298
8299 this.$location.css( { paddingLeft: leftWidth, paddingRight: rightWidth } );
8300
8301 return this;
8302 };
8303
8304 /**
8305 * Handle errors that occurred during accept or reject processes.
8306 *
8307 * @private
8308 * @param {OO.ui.Error[]|OO.ui.Error} errors Errors to be handled
8309 */
8310 OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
8311 var i, len, $item, actions,
8312 items = [],
8313 abilities = {},
8314 recoverable = true,
8315 warning = false;
8316
8317 if ( errors instanceof OO.ui.Error ) {
8318 errors = [ errors ];
8319 }
8320
8321 for ( i = 0, len = errors.length; i < len; i++ ) {
8322 if ( !errors[ i ].isRecoverable() ) {
8323 recoverable = false;
8324 }
8325 if ( errors[ i ].isWarning() ) {
8326 warning = true;
8327 }
8328 $item = $( '<div>' )
8329 .addClass( 'oo-ui-processDialog-error' )
8330 .append( errors[ i ].getMessage() );
8331 items.push( $item[ 0 ] );
8332 }
8333 this.$errorItems = $( items );
8334 if ( recoverable ) {
8335 abilities[this.currentAction] = true;
8336 // Copy the flags from the first matching action
8337 actions = this.actions.get( { actions: this.currentAction } );
8338 if ( actions.length ) {
8339 this.retryButton.clearFlags().setFlags( actions[0].getFlags() );
8340 }
8341 } else {
8342 abilities[this.currentAction] = false;
8343 this.actions.setAbilities( abilities );
8344 }
8345 if ( warning ) {
8346 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-continue' ) );
8347 } else {
8348 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-retry' ) );
8349 }
8350 this.retryButton.toggle( recoverable );
8351 this.$errorsTitle.after( this.$errorItems );
8352 this.$errors.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 );
8353 };
8354
8355 /**
8356 * Hide errors.
8357 *
8358 * @private
8359 */
8360 OO.ui.ProcessDialog.prototype.hideErrors = function () {
8361 this.$errors.addClass( 'oo-ui-element-hidden' );
8362 if ( this.$errorItems ) {
8363 this.$errorItems.remove();
8364 this.$errorItems = null;
8365 }
8366 };
8367
8368 /**
8369 * @inheritdoc
8370 */
8371 OO.ui.ProcessDialog.prototype.getTeardownProcess = function ( data ) {
8372 // Parent method
8373 return OO.ui.ProcessDialog.parent.prototype.getTeardownProcess.call( this, data )
8374 .first( function () {
8375 // Make sure to hide errors
8376 this.hideErrors();
8377 }, this );
8378 };
8379
8380 /**
8381 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
8382 * which is a widget that is specified by reference before any optional configuration settings.
8383 *
8384 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
8385 *
8386 * - **left**: The label is placed before the field-widget and aligned with the left margin.
8387 * A left-alignment is used for forms with many fields.
8388 * - **right**: The label is placed before the field-widget and aligned to the right margin.
8389 * A right-alignment is used for long but familiar forms which users tab through,
8390 * verifying the current field with a quick glance at the label.
8391 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
8392 * that users fill out from top to bottom.
8393 * - **inline**: The label is placed after the field-widget and aligned to the left.
8394 * An inline-alignment is best used with checkboxes or radio buttons.
8395 *
8396 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
8397 * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
8398 *
8399 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
8400 * @class
8401 * @extends OO.ui.Layout
8402 * @mixins OO.ui.mixin.LabelElement
8403 *
8404 * @constructor
8405 * @param {OO.ui.Widget} fieldWidget Field widget
8406 * @param {Object} [config] Configuration options
8407 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
8408 * @cfg {string} [help] Help text. When help text is specified, a help icon will appear
8409 * in the upper-right corner of the rendered field.
8410 */
8411 OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
8412 // Allow passing positional parameters inside the config object
8413 if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
8414 config = fieldWidget;
8415 fieldWidget = config.fieldWidget;
8416 }
8417
8418 var hasInputWidget = fieldWidget.constructor.static.supportsSimpleLabel;
8419
8420 // Configuration initialization
8421 config = $.extend( { align: 'left' }, config );
8422
8423 // Parent constructor
8424 OO.ui.FieldLayout.parent.call( this, config );
8425
8426 // Mixin constructors
8427 OO.ui.mixin.LabelElement.call( this, config );
8428
8429 // Properties
8430 this.fieldWidget = fieldWidget;
8431 this.$field = $( '<div>' );
8432 this.$body = $( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
8433 this.align = null;
8434 if ( config.help ) {
8435 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
8436 classes: [ 'oo-ui-fieldLayout-help' ],
8437 framed: false,
8438 icon: 'info'
8439 } );
8440
8441 this.popupButtonWidget.getPopup().$body.append(
8442 $( '<div>' )
8443 .text( config.help )
8444 .addClass( 'oo-ui-fieldLayout-help-content' )
8445 );
8446 this.$help = this.popupButtonWidget.$element;
8447 } else {
8448 this.$help = $( [] );
8449 }
8450
8451 // Events
8452 if ( hasInputWidget ) {
8453 this.$label.on( 'click', this.onLabelClick.bind( this ) );
8454 }
8455 this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
8456
8457 // Initialization
8458 this.$element
8459 .addClass( 'oo-ui-fieldLayout' )
8460 .append( this.$help, this.$body );
8461 this.$body.addClass( 'oo-ui-fieldLayout-body' );
8462 this.$field
8463 .addClass( 'oo-ui-fieldLayout-field' )
8464 .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget.isDisabled() )
8465 .append( this.fieldWidget.$element );
8466
8467 this.setAlignment( config.align );
8468 };
8469
8470 /* Setup */
8471
8472 OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
8473 OO.mixinClass( OO.ui.FieldLayout, OO.ui.mixin.LabelElement );
8474
8475 /* Methods */
8476
8477 /**
8478 * Handle field disable events.
8479 *
8480 * @private
8481 * @param {boolean} value Field is disabled
8482 */
8483 OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
8484 this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
8485 };
8486
8487 /**
8488 * Handle label mouse click events.
8489 *
8490 * @private
8491 * @param {jQuery.Event} e Mouse click event
8492 */
8493 OO.ui.FieldLayout.prototype.onLabelClick = function () {
8494 this.fieldWidget.simulateLabelClick();
8495 return false;
8496 };
8497
8498 /**
8499 * Get the widget contained by the field.
8500 *
8501 * @return {OO.ui.Widget} Field widget
8502 */
8503 OO.ui.FieldLayout.prototype.getField = function () {
8504 return this.fieldWidget;
8505 };
8506
8507 /**
8508 * Set the field alignment mode.
8509 *
8510 * @private
8511 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
8512 * @chainable
8513 */
8514 OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
8515 if ( value !== this.align ) {
8516 // Default to 'left'
8517 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) {
8518 value = 'left';
8519 }
8520 // Reorder elements
8521 if ( value === 'inline' ) {
8522 this.$body.append( this.$field, this.$label );
8523 } else {
8524 this.$body.append( this.$label, this.$field );
8525 }
8526 // Set classes. The following classes can be used here:
8527 // * oo-ui-fieldLayout-align-left
8528 // * oo-ui-fieldLayout-align-right
8529 // * oo-ui-fieldLayout-align-top
8530 // * oo-ui-fieldLayout-align-inline
8531 if ( this.align ) {
8532 this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
8533 }
8534 this.$element.addClass( 'oo-ui-fieldLayout-align-' + value );
8535 this.align = value;
8536 }
8537
8538 return this;
8539 };
8540
8541 /**
8542 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
8543 * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
8544 * is required and is specified before any optional configuration settings.
8545 *
8546 * Labels can be aligned in one of four ways:
8547 *
8548 * - **left**: The label is placed before the field-widget and aligned with the left margin.
8549 * A left-alignment is used for forms with many fields.
8550 * - **right**: The label is placed before the field-widget and aligned to the right margin.
8551 * A right-alignment is used for long but familiar forms which users tab through,
8552 * verifying the current field with a quick glance at the label.
8553 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
8554 * that users fill out from top to bottom.
8555 * - **inline**: The label is placed after the field-widget and aligned to the left.
8556 * An inline-alignment is best used with checkboxes or radio buttons.
8557 *
8558 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
8559 * text is specified.
8560 *
8561 * @example
8562 * // Example of an ActionFieldLayout
8563 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
8564 * new OO.ui.TextInputWidget( {
8565 * placeholder: 'Field widget'
8566 * } ),
8567 * new OO.ui.ButtonWidget( {
8568 * label: 'Button'
8569 * } ),
8570 * {
8571 * label: 'An ActionFieldLayout. This label is aligned top',
8572 * align: 'top',
8573 * help: 'This is help text'
8574 * }
8575 * );
8576 *
8577 * $( 'body' ).append( actionFieldLayout.$element );
8578 *
8579 *
8580 * @class
8581 * @extends OO.ui.FieldLayout
8582 *
8583 * @constructor
8584 * @param {OO.ui.Widget} fieldWidget Field widget
8585 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
8586 */
8587 OO.ui.ActionFieldLayout = function OoUiActionFieldLayout( fieldWidget, buttonWidget, config ) {
8588 // Allow passing positional parameters inside the config object
8589 if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
8590 config = fieldWidget;
8591 fieldWidget = config.fieldWidget;
8592 buttonWidget = config.buttonWidget;
8593 }
8594
8595 // Parent constructor
8596 OO.ui.ActionFieldLayout.parent.call( this, fieldWidget, config );
8597
8598 // Properties
8599 this.buttonWidget = buttonWidget;
8600 this.$button = $( '<div>' );
8601 this.$input = $( '<div>' );
8602
8603 // Initialization
8604 this.$element
8605 .addClass( 'oo-ui-actionFieldLayout' );
8606 this.$button
8607 .addClass( 'oo-ui-actionFieldLayout-button' )
8608 .append( this.buttonWidget.$element );
8609 this.$input
8610 .addClass( 'oo-ui-actionFieldLayout-input' )
8611 .append( this.fieldWidget.$element );
8612 this.$field
8613 .append( this.$input, this.$button );
8614 };
8615
8616 /* Setup */
8617
8618 OO.inheritClass( OO.ui.ActionFieldLayout, OO.ui.FieldLayout );
8619
8620 /**
8621 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
8622 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
8623 * configured with a label as well. For more information and examples,
8624 * please see the [OOjs UI documentation on MediaWiki][1].
8625 *
8626 * @example
8627 * // Example of a fieldset layout
8628 * var input1 = new OO.ui.TextInputWidget( {
8629 * placeholder: 'A text input field'
8630 * } );
8631 *
8632 * var input2 = new OO.ui.TextInputWidget( {
8633 * placeholder: 'A text input field'
8634 * } );
8635 *
8636 * var fieldset = new OO.ui.FieldsetLayout( {
8637 * label: 'Example of a fieldset layout'
8638 * } );
8639 *
8640 * fieldset.addItems( [
8641 * new OO.ui.FieldLayout( input1, {
8642 * label: 'Field One'
8643 * } ),
8644 * new OO.ui.FieldLayout( input2, {
8645 * label: 'Field Two'
8646 * } )
8647 * ] );
8648 * $( 'body' ).append( fieldset.$element );
8649 *
8650 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
8651 *
8652 * @class
8653 * @extends OO.ui.Layout
8654 * @mixins OO.ui.mixin.IconElement
8655 * @mixins OO.ui.mixin.LabelElement
8656 * @mixins OO.ui.mixin.GroupElement
8657 *
8658 * @constructor
8659 * @param {Object} [config] Configuration options
8660 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
8661 */
8662 OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
8663 // Configuration initialization
8664 config = config || {};
8665
8666 // Parent constructor
8667 OO.ui.FieldsetLayout.parent.call( this, config );
8668
8669 // Mixin constructors
8670 OO.ui.mixin.IconElement.call( this, config );
8671 OO.ui.mixin.LabelElement.call( this, config );
8672 OO.ui.mixin.GroupElement.call( this, config );
8673
8674 if ( config.help ) {
8675 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
8676 classes: [ 'oo-ui-fieldsetLayout-help' ],
8677 framed: false,
8678 icon: 'info'
8679 } );
8680
8681 this.popupButtonWidget.getPopup().$body.append(
8682 $( '<div>' )
8683 .text( config.help )
8684 .addClass( 'oo-ui-fieldsetLayout-help-content' )
8685 );
8686 this.$help = this.popupButtonWidget.$element;
8687 } else {
8688 this.$help = $( [] );
8689 }
8690
8691 // Initialization
8692 this.$element
8693 .addClass( 'oo-ui-fieldsetLayout' )
8694 .prepend( this.$help, this.$icon, this.$label, this.$group );
8695 if ( Array.isArray( config.items ) ) {
8696 this.addItems( config.items );
8697 }
8698 };
8699
8700 /* Setup */
8701
8702 OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
8703 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.IconElement );
8704 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.LabelElement );
8705 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.GroupElement );
8706
8707 /**
8708 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
8709 * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
8710 * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
8711 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
8712 *
8713 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
8714 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
8715 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
8716 * some fancier controls. Some controls have both regular and InputWidget variants, for example
8717 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
8718 * often have simplified APIs to match the capabilities of HTML forms.
8719 * See the [OOjs UI Inputs documentation on MediaWiki] [2] for more information about InputWidgets.
8720 *
8721 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Forms
8722 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8723 *
8724 * @example
8725 * // Example of a form layout that wraps a fieldset layout
8726 * var input1 = new OO.ui.TextInputWidget( {
8727 * placeholder: 'Username'
8728 * } );
8729 * var input2 = new OO.ui.TextInputWidget( {
8730 * placeholder: 'Password',
8731 * type: 'password'
8732 * } );
8733 * var submit = new OO.ui.ButtonInputWidget( {
8734 * label: 'Submit'
8735 * } );
8736 *
8737 * var fieldset = new OO.ui.FieldsetLayout( {
8738 * label: 'A form layout'
8739 * } );
8740 * fieldset.addItems( [
8741 * new OO.ui.FieldLayout( input1, {
8742 * label: 'Username',
8743 * align: 'top'
8744 * } ),
8745 * new OO.ui.FieldLayout( input2, {
8746 * label: 'Password',
8747 * align: 'top'
8748 * } ),
8749 * new OO.ui.FieldLayout( submit )
8750 * ] );
8751 * var form = new OO.ui.FormLayout( {
8752 * items: [ fieldset ],
8753 * action: '/api/formhandler',
8754 * method: 'get'
8755 * } )
8756 * $( 'body' ).append( form.$element );
8757 *
8758 * @class
8759 * @extends OO.ui.Layout
8760 * @mixins OO.ui.mixin.GroupElement
8761 *
8762 * @constructor
8763 * @param {Object} [config] Configuration options
8764 * @cfg {string} [method] HTML form `method` attribute
8765 * @cfg {string} [action] HTML form `action` attribute
8766 * @cfg {string} [enctype] HTML form `enctype` attribute
8767 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
8768 */
8769 OO.ui.FormLayout = function OoUiFormLayout( config ) {
8770 // Configuration initialization
8771 config = config || {};
8772
8773 // Parent constructor
8774 OO.ui.FormLayout.parent.call( this, config );
8775
8776 // Mixin constructors
8777 OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
8778
8779 // Events
8780 this.$element.on( 'submit', this.onFormSubmit.bind( this ) );
8781
8782 // Initialization
8783 this.$element
8784 .addClass( 'oo-ui-formLayout' )
8785 .attr( {
8786 method: config.method,
8787 action: config.action,
8788 enctype: config.enctype
8789 } );
8790 if ( Array.isArray( config.items ) ) {
8791 this.addItems( config.items );
8792 }
8793 };
8794
8795 /* Setup */
8796
8797 OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout );
8798 OO.mixinClass( OO.ui.FormLayout, OO.ui.mixin.GroupElement );
8799
8800 /* Events */
8801
8802 /**
8803 * A 'submit' event is emitted when the form is submitted.
8804 *
8805 * @event submit
8806 */
8807
8808 /* Static Properties */
8809
8810 OO.ui.FormLayout.static.tagName = 'form';
8811
8812 /* Methods */
8813
8814 /**
8815 * Handle form submit events.
8816 *
8817 * @private
8818 * @param {jQuery.Event} e Submit event
8819 * @fires submit
8820 */
8821 OO.ui.FormLayout.prototype.onFormSubmit = function () {
8822 if ( this.emit( 'submit' ) ) {
8823 return false;
8824 }
8825 };
8826
8827 /**
8828 * 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)
8829 * and its size is customized with the #menuSize config. The content area will fill all remaining space.
8830 *
8831 * @example
8832 * var menuLayout = new OO.ui.MenuLayout( {
8833 * position: 'top'
8834 * } ),
8835 * menuPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
8836 * contentPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
8837 * select = new OO.ui.SelectWidget( {
8838 * items: [
8839 * new OO.ui.OptionWidget( {
8840 * data: 'before',
8841 * label: 'Before',
8842 * } ),
8843 * new OO.ui.OptionWidget( {
8844 * data: 'after',
8845 * label: 'After',
8846 * } ),
8847 * new OO.ui.OptionWidget( {
8848 * data: 'top',
8849 * label: 'Top',
8850 * } ),
8851 * new OO.ui.OptionWidget( {
8852 * data: 'bottom',
8853 * label: 'Bottom',
8854 * } )
8855 * ]
8856 * } ).on( 'select', function ( item ) {
8857 * menuLayout.setMenuPosition( item.getData() );
8858 * } );
8859 *
8860 * menuLayout.$menu.append(
8861 * menuPanel.$element.append( '<b>Menu panel</b>', select.$element )
8862 * );
8863 * menuLayout.$content.append(
8864 * contentPanel.$element.append( '<b>Content panel</b>', '<p>Note that the menu is positioned relative to the content panel: top, bottom, after, before.</p>')
8865 * );
8866 * $( 'body' ).append( menuLayout.$element );
8867 *
8868 * If menu size needs to be overridden, it can be accomplished using CSS similar to the snippet
8869 * below. MenuLayout's CSS will override the appropriate values with 'auto' or '0' to display the
8870 * menu correctly. If `menuPosition` is known beforehand, CSS rules corresponding to other positions
8871 * may be omitted.
8872 *
8873 * .oo-ui-menuLayout-menu {
8874 * height: 200px;
8875 * width: 200px;
8876 * }
8877 * .oo-ui-menuLayout-content {
8878 * top: 200px;
8879 * left: 200px;
8880 * right: 200px;
8881 * bottom: 200px;
8882 * }
8883 *
8884 * @class
8885 * @extends OO.ui.Layout
8886 *
8887 * @constructor
8888 * @param {Object} [config] Configuration options
8889 * @cfg {boolean} [showMenu=true] Show menu
8890 * @cfg {string} [menuPosition='before'] Position of menu: `top`, `after`, `bottom` or `before`
8891 */
8892 OO.ui.MenuLayout = function OoUiMenuLayout( config ) {
8893 // Configuration initialization
8894 config = $.extend( {
8895 showMenu: true,
8896 menuPosition: 'before'
8897 }, config );
8898
8899 // Parent constructor
8900 OO.ui.MenuLayout.parent.call( this, config );
8901
8902 /**
8903 * Menu DOM node
8904 *
8905 * @property {jQuery}
8906 */
8907 this.$menu = $( '<div>' );
8908 /**
8909 * Content DOM node
8910 *
8911 * @property {jQuery}
8912 */
8913 this.$content = $( '<div>' );
8914
8915 // Initialization
8916 this.$menu
8917 .addClass( 'oo-ui-menuLayout-menu' );
8918 this.$content.addClass( 'oo-ui-menuLayout-content' );
8919 this.$element
8920 .addClass( 'oo-ui-menuLayout' )
8921 .append( this.$content, this.$menu );
8922 this.setMenuPosition( config.menuPosition );
8923 this.toggleMenu( config.showMenu );
8924 };
8925
8926 /* Setup */
8927
8928 OO.inheritClass( OO.ui.MenuLayout, OO.ui.Layout );
8929
8930 /* Methods */
8931
8932 /**
8933 * Toggle menu.
8934 *
8935 * @param {boolean} showMenu Show menu, omit to toggle
8936 * @chainable
8937 */
8938 OO.ui.MenuLayout.prototype.toggleMenu = function ( showMenu ) {
8939 showMenu = showMenu === undefined ? !this.showMenu : !!showMenu;
8940
8941 if ( this.showMenu !== showMenu ) {
8942 this.showMenu = showMenu;
8943 this.$element
8944 .toggleClass( 'oo-ui-menuLayout-showMenu', this.showMenu )
8945 .toggleClass( 'oo-ui-menuLayout-hideMenu', !this.showMenu );
8946 }
8947
8948 return this;
8949 };
8950
8951 /**
8952 * Check if menu is visible
8953 *
8954 * @return {boolean} Menu is visible
8955 */
8956 OO.ui.MenuLayout.prototype.isMenuVisible = function () {
8957 return this.showMenu;
8958 };
8959
8960 /**
8961 * Set menu position.
8962 *
8963 * @param {string} position Position of menu, either `top`, `after`, `bottom` or `before`
8964 * @throws {Error} If position value is not supported
8965 * @chainable
8966 */
8967 OO.ui.MenuLayout.prototype.setMenuPosition = function ( position ) {
8968 this.$element.removeClass( 'oo-ui-menuLayout-' + this.menuPosition );
8969 this.menuPosition = position;
8970 this.$element.addClass( 'oo-ui-menuLayout-' + position );
8971
8972 return this;
8973 };
8974
8975 /**
8976 * Get menu position.
8977 *
8978 * @return {string} Menu position
8979 */
8980 OO.ui.MenuLayout.prototype.getMenuPosition = function () {
8981 return this.menuPosition;
8982 };
8983
8984 /**
8985 * BookletLayouts contain {@link OO.ui.PageLayout page layouts} as well as
8986 * an {@link OO.ui.OutlineSelectWidget outline} that allows users to easily navigate
8987 * through the pages and select which one to display. By default, only one page is
8988 * displayed at a time and the outline is hidden. When a user navigates to a new page,
8989 * the booklet layout automatically focuses on the first focusable element, unless the
8990 * default setting is changed. Optionally, booklets can be configured to show
8991 * {@link OO.ui.OutlineControlsWidget controls} for adding, moving, and removing items.
8992 *
8993 * @example
8994 * // Example of a BookletLayout that contains two PageLayouts.
8995 *
8996 * function PageOneLayout( name, config ) {
8997 * PageOneLayout.parent.call( this, name, config );
8998 * this.$element.append( '<p>First page</p><p>(This booklet has an outline, displayed on the left)</p>' );
8999 * }
9000 * OO.inheritClass( PageOneLayout, OO.ui.PageLayout );
9001 * PageOneLayout.prototype.setupOutlineItem = function () {
9002 * this.outlineItem.setLabel( 'Page One' );
9003 * };
9004 *
9005 * function PageTwoLayout( name, config ) {
9006 * PageTwoLayout.parent.call( this, name, config );
9007 * this.$element.append( '<p>Second page</p>' );
9008 * }
9009 * OO.inheritClass( PageTwoLayout, OO.ui.PageLayout );
9010 * PageTwoLayout.prototype.setupOutlineItem = function () {
9011 * this.outlineItem.setLabel( 'Page Two' );
9012 * };
9013 *
9014 * var page1 = new PageOneLayout( 'one' ),
9015 * page2 = new PageTwoLayout( 'two' );
9016 *
9017 * var booklet = new OO.ui.BookletLayout( {
9018 * outlined: true
9019 * } );
9020 *
9021 * booklet.addPages ( [ page1, page2 ] );
9022 * $( 'body' ).append( booklet.$element );
9023 *
9024 * @class
9025 * @extends OO.ui.MenuLayout
9026 *
9027 * @constructor
9028 * @param {Object} [config] Configuration options
9029 * @cfg {boolean} [continuous=false] Show all pages, one after another
9030 * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new page is displayed.
9031 * @cfg {boolean} [outlined=false] Show the outline. The outline is used to navigate through the pages of the booklet.
9032 * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
9033 */
9034 OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
9035 // Configuration initialization
9036 config = config || {};
9037
9038 // Parent constructor
9039 OO.ui.BookletLayout.parent.call( this, config );
9040
9041 // Properties
9042 this.currentPageName = null;
9043 this.pages = {};
9044 this.ignoreFocus = false;
9045 this.stackLayout = new OO.ui.StackLayout( { continuous: !!config.continuous } );
9046 this.$content.append( this.stackLayout.$element );
9047 this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
9048 this.outlineVisible = false;
9049 this.outlined = !!config.outlined;
9050 if ( this.outlined ) {
9051 this.editable = !!config.editable;
9052 this.outlineControlsWidget = null;
9053 this.outlineSelectWidget = new OO.ui.OutlineSelectWidget();
9054 this.outlinePanel = new OO.ui.PanelLayout( { scrollable: true } );
9055 this.$menu.append( this.outlinePanel.$element );
9056 this.outlineVisible = true;
9057 if ( this.editable ) {
9058 this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
9059 this.outlineSelectWidget
9060 );
9061 }
9062 }
9063 this.toggleMenu( this.outlined );
9064
9065 // Events
9066 this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
9067 if ( this.outlined ) {
9068 this.outlineSelectWidget.connect( this, { select: 'onOutlineSelectWidgetSelect' } );
9069 }
9070 if ( this.autoFocus ) {
9071 // Event 'focus' does not bubble, but 'focusin' does
9072 this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
9073 }
9074
9075 // Initialization
9076 this.$element.addClass( 'oo-ui-bookletLayout' );
9077 this.stackLayout.$element.addClass( 'oo-ui-bookletLayout-stackLayout' );
9078 if ( this.outlined ) {
9079 this.outlinePanel.$element
9080 .addClass( 'oo-ui-bookletLayout-outlinePanel' )
9081 .append( this.outlineSelectWidget.$element );
9082 if ( this.editable ) {
9083 this.outlinePanel.$element
9084 .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
9085 .append( this.outlineControlsWidget.$element );
9086 }
9087 }
9088 };
9089
9090 /* Setup */
9091
9092 OO.inheritClass( OO.ui.BookletLayout, OO.ui.MenuLayout );
9093
9094 /* Events */
9095
9096 /**
9097 * A 'set' event is emitted when a page is {@link #setPage set} to be displayed by the booklet layout.
9098 * @event set
9099 * @param {OO.ui.PageLayout} page Current page
9100 */
9101
9102 /**
9103 * An 'add' event is emitted when pages are {@link #addPages added} to the booklet layout.
9104 *
9105 * @event add
9106 * @param {OO.ui.PageLayout[]} page Added pages
9107 * @param {number} index Index pages were added at
9108 */
9109
9110 /**
9111 * A 'remove' event is emitted when pages are {@link #clearPages cleared} or
9112 * {@link #removePages removed} from the booklet.
9113 *
9114 * @event remove
9115 * @param {OO.ui.PageLayout[]} pages Removed pages
9116 */
9117
9118 /* Methods */
9119
9120 /**
9121 * Handle stack layout focus.
9122 *
9123 * @private
9124 * @param {jQuery.Event} e Focusin event
9125 */
9126 OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) {
9127 var name, $target;
9128
9129 // Find the page that an element was focused within
9130 $target = $( e.target ).closest( '.oo-ui-pageLayout' );
9131 for ( name in this.pages ) {
9132 // Check for page match, exclude current page to find only page changes
9133 if ( this.pages[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentPageName ) {
9134 this.setPage( name );
9135 break;
9136 }
9137 }
9138 };
9139
9140 /**
9141 * Handle stack layout set events.
9142 *
9143 * @private
9144 * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
9145 */
9146 OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
9147 var layout = this;
9148 if ( page ) {
9149 page.scrollElementIntoView( { complete: function () {
9150 if ( layout.autoFocus ) {
9151 layout.focus();
9152 }
9153 } } );
9154 }
9155 };
9156
9157 /**
9158 * Focus the first input in the current page.
9159 *
9160 * If no page is selected, the first selectable page will be selected.
9161 * If the focus is already in an element on the current page, nothing will happen.
9162 * @param {number} [itemIndex] A specific item to focus on
9163 */
9164 OO.ui.BookletLayout.prototype.focus = function ( itemIndex ) {
9165 var $input, page,
9166 items = this.stackLayout.getItems();
9167
9168 if ( itemIndex !== undefined && items[ itemIndex ] ) {
9169 page = items[ itemIndex ];
9170 } else {
9171 page = this.stackLayout.getCurrentItem();
9172 }
9173
9174 if ( !page && this.outlined ) {
9175 this.selectFirstSelectablePage();
9176 page = this.stackLayout.getCurrentItem();
9177 }
9178 if ( !page ) {
9179 return;
9180 }
9181 // Only change the focus if is not already in the current page
9182 if ( !page.$element.find( ':focus' ).length ) {
9183 $input = page.$element.find( ':input:first' );
9184 if ( $input.length ) {
9185 $input[ 0 ].focus();
9186 }
9187 }
9188 };
9189
9190 /**
9191 * Find the first focusable input in the booklet layout and focus
9192 * on it.
9193 */
9194 OO.ui.BookletLayout.prototype.focusFirstFocusable = function () {
9195 var i, len,
9196 found = false,
9197 items = this.stackLayout.getItems(),
9198 checkAndFocus = function () {
9199 if ( OO.ui.isFocusableElement( $( this ) ) ) {
9200 $( this ).focus();
9201 found = true;
9202 return false;
9203 }
9204 };
9205
9206 for ( i = 0, len = items.length; i < len; i++ ) {
9207 if ( found ) {
9208 break;
9209 }
9210 // Find all potentially focusable elements in the item
9211 // and check if they are focusable
9212 items[i].$element
9213 .find( 'input, select, textarea, button, object' )
9214 /* jshint loopfunc:true */
9215 .each( checkAndFocus );
9216 }
9217 };
9218
9219 /**
9220 * Handle outline widget select events.
9221 *
9222 * @private
9223 * @param {OO.ui.OptionWidget|null} item Selected item
9224 */
9225 OO.ui.BookletLayout.prototype.onOutlineSelectWidgetSelect = function ( item ) {
9226 if ( item ) {
9227 this.setPage( item.getData() );
9228 }
9229 };
9230
9231 /**
9232 * Check if booklet has an outline.
9233 *
9234 * @return {boolean} Booklet has an outline
9235 */
9236 OO.ui.BookletLayout.prototype.isOutlined = function () {
9237 return this.outlined;
9238 };
9239
9240 /**
9241 * Check if booklet has editing controls.
9242 *
9243 * @return {boolean} Booklet is editable
9244 */
9245 OO.ui.BookletLayout.prototype.isEditable = function () {
9246 return this.editable;
9247 };
9248
9249 /**
9250 * Check if booklet has a visible outline.
9251 *
9252 * @return {boolean} Outline is visible
9253 */
9254 OO.ui.BookletLayout.prototype.isOutlineVisible = function () {
9255 return this.outlined && this.outlineVisible;
9256 };
9257
9258 /**
9259 * Hide or show the outline.
9260 *
9261 * @param {boolean} [show] Show outline, omit to invert current state
9262 * @chainable
9263 */
9264 OO.ui.BookletLayout.prototype.toggleOutline = function ( show ) {
9265 if ( this.outlined ) {
9266 show = show === undefined ? !this.outlineVisible : !!show;
9267 this.outlineVisible = show;
9268 this.toggleMenu( show );
9269 }
9270
9271 return this;
9272 };
9273
9274 /**
9275 * Get the page closest to the specified page.
9276 *
9277 * @param {OO.ui.PageLayout} page Page to use as a reference point
9278 * @return {OO.ui.PageLayout|null} Page closest to the specified page
9279 */
9280 OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) {
9281 var next, prev, level,
9282 pages = this.stackLayout.getItems(),
9283 index = $.inArray( page, pages );
9284
9285 if ( index !== -1 ) {
9286 next = pages[ index + 1 ];
9287 prev = pages[ index - 1 ];
9288 // Prefer adjacent pages at the same level
9289 if ( this.outlined ) {
9290 level = this.outlineSelectWidget.getItemFromData( page.getName() ).getLevel();
9291 if (
9292 prev &&
9293 level === this.outlineSelectWidget.getItemFromData( prev.getName() ).getLevel()
9294 ) {
9295 return prev;
9296 }
9297 if (
9298 next &&
9299 level === this.outlineSelectWidget.getItemFromData( next.getName() ).getLevel()
9300 ) {
9301 return next;
9302 }
9303 }
9304 }
9305 return prev || next || null;
9306 };
9307
9308 /**
9309 * Get the outline widget.
9310 *
9311 * If the booklet is not outlined, the method will return `null`.
9312 *
9313 * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if the booklet is not outlined
9314 */
9315 OO.ui.BookletLayout.prototype.getOutline = function () {
9316 return this.outlineSelectWidget;
9317 };
9318
9319 /**
9320 * Get the outline controls widget.
9321 *
9322 * If the outline is not editable, the method will return `null`.
9323 *
9324 * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
9325 */
9326 OO.ui.BookletLayout.prototype.getOutlineControls = function () {
9327 return this.outlineControlsWidget;
9328 };
9329
9330 /**
9331 * Get a page by its symbolic name.
9332 *
9333 * @param {string} name Symbolic name of page
9334 * @return {OO.ui.PageLayout|undefined} Page, if found
9335 */
9336 OO.ui.BookletLayout.prototype.getPage = function ( name ) {
9337 return this.pages[ name ];
9338 };
9339
9340 /**
9341 * Get the current page.
9342 *
9343 * @return {OO.ui.PageLayout|undefined} Current page, if found
9344 */
9345 OO.ui.BookletLayout.prototype.getCurrentPage = function () {
9346 var name = this.getCurrentPageName();
9347 return name ? this.getPage( name ) : undefined;
9348 };
9349
9350 /**
9351 * Get the symbolic name of the current page.
9352 *
9353 * @return {string|null} Symbolic name of the current page
9354 */
9355 OO.ui.BookletLayout.prototype.getCurrentPageName = function () {
9356 return this.currentPageName;
9357 };
9358
9359 /**
9360 * Add pages to the booklet layout
9361 *
9362 * When pages are added with the same names as existing pages, the existing pages will be
9363 * automatically removed before the new pages are added.
9364 *
9365 * @param {OO.ui.PageLayout[]} pages Pages to add
9366 * @param {number} index Index of the insertion point
9367 * @fires add
9368 * @chainable
9369 */
9370 OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) {
9371 var i, len, name, page, item, currentIndex,
9372 stackLayoutPages = this.stackLayout.getItems(),
9373 remove = [],
9374 items = [];
9375
9376 // Remove pages with same names
9377 for ( i = 0, len = pages.length; i < len; i++ ) {
9378 page = pages[ i ];
9379 name = page.getName();
9380
9381 if ( Object.prototype.hasOwnProperty.call( this.pages, name ) ) {
9382 // Correct the insertion index
9383 currentIndex = $.inArray( this.pages[ name ], stackLayoutPages );
9384 if ( currentIndex !== -1 && currentIndex + 1 < index ) {
9385 index--;
9386 }
9387 remove.push( this.pages[ name ] );
9388 }
9389 }
9390 if ( remove.length ) {
9391 this.removePages( remove );
9392 }
9393
9394 // Add new pages
9395 for ( i = 0, len = pages.length; i < len; i++ ) {
9396 page = pages[ i ];
9397 name = page.getName();
9398 this.pages[ page.getName() ] = page;
9399 if ( this.outlined ) {
9400 item = new OO.ui.OutlineOptionWidget( { data: name } );
9401 page.setOutlineItem( item );
9402 items.push( item );
9403 }
9404 }
9405
9406 if ( this.outlined && items.length ) {
9407 this.outlineSelectWidget.addItems( items, index );
9408 this.selectFirstSelectablePage();
9409 }
9410 this.stackLayout.addItems( pages, index );
9411 this.emit( 'add', pages, index );
9412
9413 return this;
9414 };
9415
9416 /**
9417 * Remove the specified pages from the booklet layout.
9418 *
9419 * To remove all pages from the booklet, you may wish to use the #clearPages method instead.
9420 *
9421 * @param {OO.ui.PageLayout[]} pages An array of pages to remove
9422 * @fires remove
9423 * @chainable
9424 */
9425 OO.ui.BookletLayout.prototype.removePages = function ( pages ) {
9426 var i, len, name, page,
9427 items = [];
9428
9429 for ( i = 0, len = pages.length; i < len; i++ ) {
9430 page = pages[ i ];
9431 name = page.getName();
9432 delete this.pages[ name ];
9433 if ( this.outlined ) {
9434 items.push( this.outlineSelectWidget.getItemFromData( name ) );
9435 page.setOutlineItem( null );
9436 }
9437 }
9438 if ( this.outlined && items.length ) {
9439 this.outlineSelectWidget.removeItems( items );
9440 this.selectFirstSelectablePage();
9441 }
9442 this.stackLayout.removeItems( pages );
9443 this.emit( 'remove', pages );
9444
9445 return this;
9446 };
9447
9448 /**
9449 * Clear all pages from the booklet layout.
9450 *
9451 * To remove only a subset of pages from the booklet, use the #removePages method.
9452 *
9453 * @fires remove
9454 * @chainable
9455 */
9456 OO.ui.BookletLayout.prototype.clearPages = function () {
9457 var i, len,
9458 pages = this.stackLayout.getItems();
9459
9460 this.pages = {};
9461 this.currentPageName = null;
9462 if ( this.outlined ) {
9463 this.outlineSelectWidget.clearItems();
9464 for ( i = 0, len = pages.length; i < len; i++ ) {
9465 pages[ i ].setOutlineItem( null );
9466 }
9467 }
9468 this.stackLayout.clearItems();
9469
9470 this.emit( 'remove', pages );
9471
9472 return this;
9473 };
9474
9475 /**
9476 * Set the current page by symbolic name.
9477 *
9478 * @fires set
9479 * @param {string} name Symbolic name of page
9480 */
9481 OO.ui.BookletLayout.prototype.setPage = function ( name ) {
9482 var selectedItem,
9483 $focused,
9484 page = this.pages[ name ];
9485
9486 if ( name !== this.currentPageName ) {
9487 if ( this.outlined ) {
9488 selectedItem = this.outlineSelectWidget.getSelectedItem();
9489 if ( selectedItem && selectedItem.getData() !== name ) {
9490 this.outlineSelectWidget.selectItemByData( name );
9491 }
9492 }
9493 if ( page ) {
9494 if ( this.currentPageName && this.pages[ this.currentPageName ] ) {
9495 this.pages[ this.currentPageName ].setActive( false );
9496 // Blur anything focused if the next page doesn't have anything focusable - this
9497 // is not needed if the next page has something focusable because once it is focused
9498 // this blur happens automatically
9499 if ( this.autoFocus && !page.$element.find( ':input' ).length ) {
9500 $focused = this.pages[ this.currentPageName ].$element.find( ':focus' );
9501 if ( $focused.length ) {
9502 $focused[ 0 ].blur();
9503 }
9504 }
9505 }
9506 this.currentPageName = name;
9507 this.stackLayout.setItem( page );
9508 page.setActive( true );
9509 this.emit( 'set', page );
9510 }
9511 }
9512 };
9513
9514 /**
9515 * Select the first selectable page.
9516 *
9517 * @chainable
9518 */
9519 OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () {
9520 if ( !this.outlineSelectWidget.getSelectedItem() ) {
9521 this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getFirstSelectableItem() );
9522 }
9523
9524 return this;
9525 };
9526
9527 /**
9528 * IndexLayouts contain {@link OO.ui.CardLayout card layouts} as well as
9529 * {@link OO.ui.TabSelectWidget tabs} that allow users to easily navigate through the cards and
9530 * select which one to display. By default, only one card is displayed at a time. When a user
9531 * navigates to a new card, the index layout automatically focuses on the first focusable element,
9532 * unless the default setting is changed.
9533 *
9534 * TODO: This class is similar to BookletLayout, we may want to refactor to reduce duplication
9535 *
9536 * @example
9537 * // Example of a IndexLayout that contains two CardLayouts.
9538 *
9539 * function CardOneLayout( name, config ) {
9540 * CardOneLayout.parent.call( this, name, config );
9541 * this.$element.append( '<p>First card</p>' );
9542 * }
9543 * OO.inheritClass( CardOneLayout, OO.ui.CardLayout );
9544 * CardOneLayout.prototype.setupTabItem = function () {
9545 * this.tabItem.setLabel( 'Card One' );
9546 * };
9547 *
9548 * function CardTwoLayout( name, config ) {
9549 * CardTwoLayout.parent.call( this, name, config );
9550 * this.$element.append( '<p>Second card</p>' );
9551 * }
9552 * OO.inheritClass( CardTwoLayout, OO.ui.CardLayout );
9553 * CardTwoLayout.prototype.setupTabItem = function () {
9554 * this.tabItem.setLabel( 'Card Two' );
9555 * };
9556 *
9557 * var card1 = new CardOneLayout( 'one' ),
9558 * card2 = new CardTwoLayout( 'two' );
9559 *
9560 * var index = new OO.ui.IndexLayout();
9561 *
9562 * index.addCards ( [ card1, card2 ] );
9563 * $( 'body' ).append( index.$element );
9564 *
9565 * @class
9566 * @extends OO.ui.MenuLayout
9567 *
9568 * @constructor
9569 * @param {Object} [config] Configuration options
9570 * @cfg {boolean} [continuous=false] Show all cards, one after another
9571 * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new card is displayed.
9572 */
9573 OO.ui.IndexLayout = function OoUiIndexLayout( config ) {
9574 // Configuration initialization
9575 config = $.extend( {}, config, { menuPosition: 'top' } );
9576
9577 // Parent constructor
9578 OO.ui.IndexLayout.parent.call( this, config );
9579
9580 // Properties
9581 this.currentCardName = null;
9582 this.cards = {};
9583 this.ignoreFocus = false;
9584 this.stackLayout = new OO.ui.StackLayout( { continuous: !!config.continuous } );
9585 this.$content.append( this.stackLayout.$element );
9586 this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
9587
9588 this.tabSelectWidget = new OO.ui.TabSelectWidget();
9589 this.tabPanel = new OO.ui.PanelLayout();
9590 this.$menu.append( this.tabPanel.$element );
9591
9592 this.toggleMenu( true );
9593
9594 // Events
9595 this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
9596 this.tabSelectWidget.connect( this, { select: 'onTabSelectWidgetSelect' } );
9597 if ( this.autoFocus ) {
9598 // Event 'focus' does not bubble, but 'focusin' does
9599 this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
9600 }
9601
9602 // Initialization
9603 this.$element.addClass( 'oo-ui-indexLayout' );
9604 this.stackLayout.$element.addClass( 'oo-ui-indexLayout-stackLayout' );
9605 this.tabPanel.$element
9606 .addClass( 'oo-ui-indexLayout-tabPanel' )
9607 .append( this.tabSelectWidget.$element );
9608 };
9609
9610 /* Setup */
9611
9612 OO.inheritClass( OO.ui.IndexLayout, OO.ui.MenuLayout );
9613
9614 /* Events */
9615
9616 /**
9617 * A 'set' event is emitted when a card is {@link #setCard set} to be displayed by the index layout.
9618 * @event set
9619 * @param {OO.ui.CardLayout} card Current card
9620 */
9621
9622 /**
9623 * An 'add' event is emitted when cards are {@link #addCards added} to the index layout.
9624 *
9625 * @event add
9626 * @param {OO.ui.CardLayout[]} card Added cards
9627 * @param {number} index Index cards were added at
9628 */
9629
9630 /**
9631 * A 'remove' event is emitted when cards are {@link #clearCards cleared} or
9632 * {@link #removeCards removed} from the index.
9633 *
9634 * @event remove
9635 * @param {OO.ui.CardLayout[]} cards Removed cards
9636 */
9637
9638 /* Methods */
9639
9640 /**
9641 * Handle stack layout focus.
9642 *
9643 * @private
9644 * @param {jQuery.Event} e Focusin event
9645 */
9646 OO.ui.IndexLayout.prototype.onStackLayoutFocus = function ( e ) {
9647 var name, $target;
9648
9649 // Find the card that an element was focused within
9650 $target = $( e.target ).closest( '.oo-ui-cardLayout' );
9651 for ( name in this.cards ) {
9652 // Check for card match, exclude current card to find only card changes
9653 if ( this.cards[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentCardName ) {
9654 this.setCard( name );
9655 break;
9656 }
9657 }
9658 };
9659
9660 /**
9661 * Handle stack layout set events.
9662 *
9663 * @private
9664 * @param {OO.ui.PanelLayout|null} card The card panel that is now the current panel
9665 */
9666 OO.ui.IndexLayout.prototype.onStackLayoutSet = function ( card ) {
9667 var layout = this;
9668 if ( card ) {
9669 card.scrollElementIntoView( { complete: function () {
9670 if ( layout.autoFocus ) {
9671 layout.focus();
9672 }
9673 } } );
9674 }
9675 };
9676
9677 /**
9678 * Focus the first input in the current card.
9679 *
9680 * If no card is selected, the first selectable card will be selected.
9681 * If the focus is already in an element on the current card, nothing will happen.
9682 * @param {number} [itemIndex] A specific item to focus on
9683 */
9684 OO.ui.IndexLayout.prototype.focus = function ( itemIndex ) {
9685 var $input, card,
9686 items = this.stackLayout.getItems();
9687
9688 if ( itemIndex !== undefined && items[ itemIndex ] ) {
9689 card = items[ itemIndex ];
9690 } else {
9691 card = this.stackLayout.getCurrentItem();
9692 }
9693
9694 if ( !card ) {
9695 this.selectFirstSelectableCard();
9696 card = this.stackLayout.getCurrentItem();
9697 }
9698 if ( !card ) {
9699 return;
9700 }
9701 // Only change the focus if is not already in the current card
9702 if ( !card.$element.find( ':focus' ).length ) {
9703 $input = card.$element.find( ':input:first' );
9704 if ( $input.length ) {
9705 $input[ 0 ].focus();
9706 }
9707 }
9708 };
9709
9710 /**
9711 * Find the first focusable input in the index layout and focus
9712 * on it.
9713 */
9714 OO.ui.IndexLayout.prototype.focusFirstFocusable = function () {
9715 var i, len,
9716 found = false,
9717 items = this.stackLayout.getItems(),
9718 checkAndFocus = function () {
9719 if ( OO.ui.isFocusableElement( $( this ) ) ) {
9720 $( this ).focus();
9721 found = true;
9722 return false;
9723 }
9724 };
9725
9726 for ( i = 0, len = items.length; i < len; i++ ) {
9727 if ( found ) {
9728 break;
9729 }
9730 // Find all potentially focusable elements in the item
9731 // and check if they are focusable
9732 items[i].$element
9733 .find( 'input, select, textarea, button, object' )
9734 .each( checkAndFocus );
9735 }
9736 };
9737
9738 /**
9739 * Handle tab widget select events.
9740 *
9741 * @private
9742 * @param {OO.ui.OptionWidget|null} item Selected item
9743 */
9744 OO.ui.IndexLayout.prototype.onTabSelectWidgetSelect = function ( item ) {
9745 if ( item ) {
9746 this.setCard( item.getData() );
9747 }
9748 };
9749
9750 /**
9751 * Get the card closest to the specified card.
9752 *
9753 * @param {OO.ui.CardLayout} card Card to use as a reference point
9754 * @return {OO.ui.CardLayout|null} Card closest to the specified card
9755 */
9756 OO.ui.IndexLayout.prototype.getClosestCard = function ( card ) {
9757 var next, prev, level,
9758 cards = this.stackLayout.getItems(),
9759 index = $.inArray( card, cards );
9760
9761 if ( index !== -1 ) {
9762 next = cards[ index + 1 ];
9763 prev = cards[ index - 1 ];
9764 // Prefer adjacent cards at the same level
9765 level = this.tabSelectWidget.getItemFromData( card.getName() ).getLevel();
9766 if (
9767 prev &&
9768 level === this.tabSelectWidget.getItemFromData( prev.getName() ).getLevel()
9769 ) {
9770 return prev;
9771 }
9772 if (
9773 next &&
9774 level === this.tabSelectWidget.getItemFromData( next.getName() ).getLevel()
9775 ) {
9776 return next;
9777 }
9778 }
9779 return prev || next || null;
9780 };
9781
9782 /**
9783 * Get the tabs widget.
9784 *
9785 * @return {OO.ui.TabSelectWidget} Tabs widget
9786 */
9787 OO.ui.IndexLayout.prototype.getTabs = function () {
9788 return this.tabSelectWidget;
9789 };
9790
9791 /**
9792 * Get a card by its symbolic name.
9793 *
9794 * @param {string} name Symbolic name of card
9795 * @return {OO.ui.CardLayout|undefined} Card, if found
9796 */
9797 OO.ui.IndexLayout.prototype.getCard = function ( name ) {
9798 return this.cards[ name ];
9799 };
9800
9801 /**
9802 * Get the current card.
9803 *
9804 * @return {OO.ui.CardLayout|undefined} Current card, if found
9805 */
9806 OO.ui.IndexLayout.prototype.getCurrentCard = function () {
9807 var name = this.getCurrentCardName();
9808 return name ? this.getCard( name ) : undefined;
9809 };
9810
9811 /**
9812 * Get the symbolic name of the current card.
9813 *
9814 * @return {string|null} Symbolic name of the current card
9815 */
9816 OO.ui.IndexLayout.prototype.getCurrentCardName = function () {
9817 return this.currentCardName;
9818 };
9819
9820 /**
9821 * Add cards to the index layout
9822 *
9823 * When cards are added with the same names as existing cards, the existing cards will be
9824 * automatically removed before the new cards are added.
9825 *
9826 * @param {OO.ui.CardLayout[]} cards Cards to add
9827 * @param {number} index Index of the insertion point
9828 * @fires add
9829 * @chainable
9830 */
9831 OO.ui.IndexLayout.prototype.addCards = function ( cards, index ) {
9832 var i, len, name, card, item, currentIndex,
9833 stackLayoutCards = this.stackLayout.getItems(),
9834 remove = [],
9835 items = [];
9836
9837 // Remove cards with same names
9838 for ( i = 0, len = cards.length; i < len; i++ ) {
9839 card = cards[ i ];
9840 name = card.getName();
9841
9842 if ( Object.prototype.hasOwnProperty.call( this.cards, name ) ) {
9843 // Correct the insertion index
9844 currentIndex = $.inArray( this.cards[ name ], stackLayoutCards );
9845 if ( currentIndex !== -1 && currentIndex + 1 < index ) {
9846 index--;
9847 }
9848 remove.push( this.cards[ name ] );
9849 }
9850 }
9851 if ( remove.length ) {
9852 this.removeCards( remove );
9853 }
9854
9855 // Add new cards
9856 for ( i = 0, len = cards.length; i < len; i++ ) {
9857 card = cards[ i ];
9858 name = card.getName();
9859 this.cards[ card.getName() ] = card;
9860 item = new OO.ui.TabOptionWidget( { data: name } );
9861 card.setTabItem( item );
9862 items.push( item );
9863 }
9864
9865 if ( items.length ) {
9866 this.tabSelectWidget.addItems( items, index );
9867 this.selectFirstSelectableCard();
9868 }
9869 this.stackLayout.addItems( cards, index );
9870 this.emit( 'add', cards, index );
9871
9872 return this;
9873 };
9874
9875 /**
9876 * Remove the specified cards from the index layout.
9877 *
9878 * To remove all cards from the index, you may wish to use the #clearCards method instead.
9879 *
9880 * @param {OO.ui.CardLayout[]} cards An array of cards to remove
9881 * @fires remove
9882 * @chainable
9883 */
9884 OO.ui.IndexLayout.prototype.removeCards = function ( cards ) {
9885 var i, len, name, card,
9886 items = [];
9887
9888 for ( i = 0, len = cards.length; i < len; i++ ) {
9889 card = cards[ i ];
9890 name = card.getName();
9891 delete this.cards[ name ];
9892 items.push( this.tabSelectWidget.getItemFromData( name ) );
9893 card.setTabItem( null );
9894 }
9895 if ( items.length ) {
9896 this.tabSelectWidget.removeItems( items );
9897 this.selectFirstSelectableCard();
9898 }
9899 this.stackLayout.removeItems( cards );
9900 this.emit( 'remove', cards );
9901
9902 return this;
9903 };
9904
9905 /**
9906 * Clear all cards from the index layout.
9907 *
9908 * To remove only a subset of cards from the index, use the #removeCards method.
9909 *
9910 * @fires remove
9911 * @chainable
9912 */
9913 OO.ui.IndexLayout.prototype.clearCards = function () {
9914 var i, len,
9915 cards = this.stackLayout.getItems();
9916
9917 this.cards = {};
9918 this.currentCardName = null;
9919 this.tabSelectWidget.clearItems();
9920 for ( i = 0, len = cards.length; i < len; i++ ) {
9921 cards[ i ].setTabItem( null );
9922 }
9923 this.stackLayout.clearItems();
9924
9925 this.emit( 'remove', cards );
9926
9927 return this;
9928 };
9929
9930 /**
9931 * Set the current card by symbolic name.
9932 *
9933 * @fires set
9934 * @param {string} name Symbolic name of card
9935 */
9936 OO.ui.IndexLayout.prototype.setCard = function ( name ) {
9937 var selectedItem,
9938 $focused,
9939 card = this.cards[ name ];
9940
9941 if ( name !== this.currentCardName ) {
9942 selectedItem = this.tabSelectWidget.getSelectedItem();
9943 if ( selectedItem && selectedItem.getData() !== name ) {
9944 this.tabSelectWidget.selectItemByData( name );
9945 }
9946 if ( card ) {
9947 if ( this.currentCardName && this.cards[ this.currentCardName ] ) {
9948 this.cards[ this.currentCardName ].setActive( false );
9949 // Blur anything focused if the next card doesn't have anything focusable - this
9950 // is not needed if the next card has something focusable because once it is focused
9951 // this blur happens automatically
9952 if ( this.autoFocus && !card.$element.find( ':input' ).length ) {
9953 $focused = this.cards[ this.currentCardName ].$element.find( ':focus' );
9954 if ( $focused.length ) {
9955 $focused[ 0 ].blur();
9956 }
9957 }
9958 }
9959 this.currentCardName = name;
9960 this.stackLayout.setItem( card );
9961 card.setActive( true );
9962 this.emit( 'set', card );
9963 }
9964 }
9965 };
9966
9967 /**
9968 * Select the first selectable card.
9969 *
9970 * @chainable
9971 */
9972 OO.ui.IndexLayout.prototype.selectFirstSelectableCard = function () {
9973 if ( !this.tabSelectWidget.getSelectedItem() ) {
9974 this.tabSelectWidget.selectItem( this.tabSelectWidget.getFirstSelectableItem() );
9975 }
9976
9977 return this;
9978 };
9979
9980 /**
9981 * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
9982 * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
9983 *
9984 * @example
9985 * // Example of a panel layout
9986 * var panel = new OO.ui.PanelLayout( {
9987 * expanded: false,
9988 * framed: true,
9989 * padded: true,
9990 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
9991 * } );
9992 * $( 'body' ).append( panel.$element );
9993 *
9994 * @class
9995 * @extends OO.ui.Layout
9996 *
9997 * @constructor
9998 * @param {Object} [config] Configuration options
9999 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
10000 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
10001 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
10002 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
10003 */
10004 OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
10005 // Configuration initialization
10006 config = $.extend( {
10007 scrollable: false,
10008 padded: false,
10009 expanded: true,
10010 framed: false
10011 }, config );
10012
10013 // Parent constructor
10014 OO.ui.PanelLayout.parent.call( this, config );
10015
10016 // Initialization
10017 this.$element.addClass( 'oo-ui-panelLayout' );
10018 if ( config.scrollable ) {
10019 this.$element.addClass( 'oo-ui-panelLayout-scrollable' );
10020 }
10021 if ( config.padded ) {
10022 this.$element.addClass( 'oo-ui-panelLayout-padded' );
10023 }
10024 if ( config.expanded ) {
10025 this.$element.addClass( 'oo-ui-panelLayout-expanded' );
10026 }
10027 if ( config.framed ) {
10028 this.$element.addClass( 'oo-ui-panelLayout-framed' );
10029 }
10030 };
10031
10032 /* Setup */
10033
10034 OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
10035
10036 /**
10037 * CardLayouts are used within {@link OO.ui.IndexLayout index layouts} to create cards that users can select and display
10038 * from the index's optional {@link OO.ui.TabSelectWidget tab} navigation. Cards are usually not instantiated directly,
10039 * rather extended to include the required content and functionality.
10040 *
10041 * Each card must have a unique symbolic name, which is passed to the constructor. In addition, the card's tab
10042 * item is customized (with a label) using the #setupTabItem method. See
10043 * {@link OO.ui.IndexLayout IndexLayout} for an example.
10044 *
10045 * @class
10046 * @extends OO.ui.PanelLayout
10047 *
10048 * @constructor
10049 * @param {string} name Unique symbolic name of card
10050 * @param {Object} [config] Configuration options
10051 */
10052 OO.ui.CardLayout = function OoUiCardLayout( name, config ) {
10053 // Allow passing positional parameters inside the config object
10054 if ( OO.isPlainObject( name ) && config === undefined ) {
10055 config = name;
10056 name = config.name;
10057 }
10058
10059 // Configuration initialization
10060 config = $.extend( { scrollable: true }, config );
10061
10062 // Parent constructor
10063 OO.ui.CardLayout.parent.call( this, config );
10064
10065 // Properties
10066 this.name = name;
10067 this.tabItem = null;
10068 this.active = false;
10069
10070 // Initialization
10071 this.$element.addClass( 'oo-ui-cardLayout' );
10072 };
10073
10074 /* Setup */
10075
10076 OO.inheritClass( OO.ui.CardLayout, OO.ui.PanelLayout );
10077
10078 /* Events */
10079
10080 /**
10081 * An 'active' event is emitted when the card becomes active. Cards become active when they are
10082 * shown in a index layout that is configured to display only one card at a time.
10083 *
10084 * @event active
10085 * @param {boolean} active Card is active
10086 */
10087
10088 /* Methods */
10089
10090 /**
10091 * Get the symbolic name of the card.
10092 *
10093 * @return {string} Symbolic name of card
10094 */
10095 OO.ui.CardLayout.prototype.getName = function () {
10096 return this.name;
10097 };
10098
10099 /**
10100 * Check if card is active.
10101 *
10102 * Cards become active when they are shown in a {@link OO.ui.IndexLayout index layout} that is configured to display
10103 * only one card at a time. Additional CSS is applied to the card's tab item to reflect the active state.
10104 *
10105 * @return {boolean} Card is active
10106 */
10107 OO.ui.CardLayout.prototype.isActive = function () {
10108 return this.active;
10109 };
10110
10111 /**
10112 * Get tab item.
10113 *
10114 * The tab item allows users to access the card from the index's tab
10115 * navigation. The tab item itself can be customized (with a label, level, etc.) using the #setupTabItem method.
10116 *
10117 * @return {OO.ui.TabOptionWidget|null} Tab option widget
10118 */
10119 OO.ui.CardLayout.prototype.getTabItem = function () {
10120 return this.tabItem;
10121 };
10122
10123 /**
10124 * Set or unset the tab item.
10125 *
10126 * Specify a {@link OO.ui.TabOptionWidget tab option} to set it,
10127 * or `null` to clear the tab item. To customize the tab item itself (e.g., to set a label or tab
10128 * level), use #setupTabItem instead of this method.
10129 *
10130 * @param {OO.ui.TabOptionWidget|null} tabItem Tab option widget, null to clear
10131 * @chainable
10132 */
10133 OO.ui.CardLayout.prototype.setTabItem = function ( tabItem ) {
10134 this.tabItem = tabItem || null;
10135 if ( tabItem ) {
10136 this.setupTabItem();
10137 }
10138 return this;
10139 };
10140
10141 /**
10142 * Set up the tab item.
10143 *
10144 * Use this method to customize the tab item (e.g., to add a label or tab level). To set or unset
10145 * the tab item itself (with a {@link OO.ui.TabOptionWidget tab option} or `null`), use
10146 * the #setTabItem method instead.
10147 *
10148 * @param {OO.ui.TabOptionWidget} tabItem Tab option widget to set up
10149 * @chainable
10150 */
10151 OO.ui.CardLayout.prototype.setupTabItem = function () {
10152 return this;
10153 };
10154
10155 /**
10156 * Set the card to its 'active' state.
10157 *
10158 * Cards become active when they are shown in a index layout that is configured to display only one card at a time. Additional
10159 * CSS is applied to the tab item to reflect the card's active state. Outside of the index
10160 * context, setting the active state on a card does nothing.
10161 *
10162 * @param {boolean} value Card is active
10163 * @fires active
10164 */
10165 OO.ui.CardLayout.prototype.setActive = function ( active ) {
10166 active = !!active;
10167
10168 if ( active !== this.active ) {
10169 this.active = active;
10170 this.$element.toggleClass( 'oo-ui-cardLayout-active', this.active );
10171 this.emit( 'active', this.active );
10172 }
10173 };
10174
10175 /**
10176 * PageLayouts are used within {@link OO.ui.BookletLayout booklet layouts} to create pages that users can select and display
10177 * from the booklet's optional {@link OO.ui.OutlineSelectWidget outline} navigation. Pages are usually not instantiated directly,
10178 * rather extended to include the required content and functionality.
10179 *
10180 * Each page must have a unique symbolic name, which is passed to the constructor. In addition, the page's outline
10181 * item is customized (with a label, outline level, etc.) using the #setupOutlineItem method. See
10182 * {@link OO.ui.BookletLayout BookletLayout} for an example.
10183 *
10184 * @class
10185 * @extends OO.ui.PanelLayout
10186 *
10187 * @constructor
10188 * @param {string} name Unique symbolic name of page
10189 * @param {Object} [config] Configuration options
10190 */
10191 OO.ui.PageLayout = function OoUiPageLayout( name, config ) {
10192 // Allow passing positional parameters inside the config object
10193 if ( OO.isPlainObject( name ) && config === undefined ) {
10194 config = name;
10195 name = config.name;
10196 }
10197
10198 // Configuration initialization
10199 config = $.extend( { scrollable: true }, config );
10200
10201 // Parent constructor
10202 OO.ui.PageLayout.parent.call( this, config );
10203
10204 // Properties
10205 this.name = name;
10206 this.outlineItem = null;
10207 this.active = false;
10208
10209 // Initialization
10210 this.$element.addClass( 'oo-ui-pageLayout' );
10211 };
10212
10213 /* Setup */
10214
10215 OO.inheritClass( OO.ui.PageLayout, OO.ui.PanelLayout );
10216
10217 /* Events */
10218
10219 /**
10220 * An 'active' event is emitted when the page becomes active. Pages become active when they are
10221 * shown in a booklet layout that is configured to display only one page at a time.
10222 *
10223 * @event active
10224 * @param {boolean} active Page is active
10225 */
10226
10227 /* Methods */
10228
10229 /**
10230 * Get the symbolic name of the page.
10231 *
10232 * @return {string} Symbolic name of page
10233 */
10234 OO.ui.PageLayout.prototype.getName = function () {
10235 return this.name;
10236 };
10237
10238 /**
10239 * Check if page is active.
10240 *
10241 * Pages become active when they are shown in a {@link OO.ui.BookletLayout booklet layout} that is configured to display
10242 * only one page at a time. Additional CSS is applied to the page's outline item to reflect the active state.
10243 *
10244 * @return {boolean} Page is active
10245 */
10246 OO.ui.PageLayout.prototype.isActive = function () {
10247 return this.active;
10248 };
10249
10250 /**
10251 * Get outline item.
10252 *
10253 * The outline item allows users to access the page from the booklet's outline
10254 * navigation. The outline item itself can be customized (with a label, level, etc.) using the #setupOutlineItem method.
10255 *
10256 * @return {OO.ui.OutlineOptionWidget|null} Outline option widget
10257 */
10258 OO.ui.PageLayout.prototype.getOutlineItem = function () {
10259 return this.outlineItem;
10260 };
10261
10262 /**
10263 * Set or unset the outline item.
10264 *
10265 * Specify an {@link OO.ui.OutlineOptionWidget outline option} to set it,
10266 * or `null` to clear the outline item. To customize the outline item itself (e.g., to set a label or outline
10267 * level), use #setupOutlineItem instead of this method.
10268 *
10269 * @param {OO.ui.OutlineOptionWidget|null} outlineItem Outline option widget, null to clear
10270 * @chainable
10271 */
10272 OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) {
10273 this.outlineItem = outlineItem || null;
10274 if ( outlineItem ) {
10275 this.setupOutlineItem();
10276 }
10277 return this;
10278 };
10279
10280 /**
10281 * Set up the outline item.
10282 *
10283 * Use this method to customize the outline item (e.g., to add a label or outline level). To set or unset
10284 * the outline item itself (with an {@link OO.ui.OutlineOptionWidget outline option} or `null`), use
10285 * the #setOutlineItem method instead.
10286 *
10287 * @param {OO.ui.OutlineOptionWidget} outlineItem Outline option widget to set up
10288 * @chainable
10289 */
10290 OO.ui.PageLayout.prototype.setupOutlineItem = function () {
10291 return this;
10292 };
10293
10294 /**
10295 * Set the page to its 'active' state.
10296 *
10297 * Pages become active when they are shown in a booklet layout that is configured to display only one page at a time. Additional
10298 * CSS is applied to the outline item to reflect the page's active state. Outside of the booklet
10299 * context, setting the active state on a page does nothing.
10300 *
10301 * @param {boolean} value Page is active
10302 * @fires active
10303 */
10304 OO.ui.PageLayout.prototype.setActive = function ( active ) {
10305 active = !!active;
10306
10307 if ( active !== this.active ) {
10308 this.active = active;
10309 this.$element.toggleClass( 'oo-ui-pageLayout-active', active );
10310 this.emit( 'active', this.active );
10311 }
10312 };
10313
10314 /**
10315 * StackLayouts contain a series of {@link OO.ui.PanelLayout panel layouts}. By default, only one panel is displayed
10316 * at a time, though the stack layout can also be configured to show all contained panels, one after another,
10317 * by setting the #continuous option to 'true'.
10318 *
10319 * @example
10320 * // A stack layout with two panels, configured to be displayed continously
10321 * var myStack = new OO.ui.StackLayout( {
10322 * items: [
10323 * new OO.ui.PanelLayout( {
10324 * $content: $( '<p>Panel One</p>' ),
10325 * padded: true,
10326 * framed: true
10327 * } ),
10328 * new OO.ui.PanelLayout( {
10329 * $content: $( '<p>Panel Two</p>' ),
10330 * padded: true,
10331 * framed: true
10332 * } )
10333 * ],
10334 * continuous: true
10335 * } );
10336 * $( 'body' ).append( myStack.$element );
10337 *
10338 * @class
10339 * @extends OO.ui.PanelLayout
10340 * @mixins OO.ui.mixin.GroupElement
10341 *
10342 * @constructor
10343 * @param {Object} [config] Configuration options
10344 * @cfg {boolean} [continuous=false] Show all panels, one after another. By default, only one panel is displayed at a time.
10345 * @cfg {OO.ui.Layout[]} [items] Panel layouts to add to the stack layout.
10346 */
10347 OO.ui.StackLayout = function OoUiStackLayout( config ) {
10348 // Configuration initialization
10349 config = $.extend( { scrollable: true }, config );
10350
10351 // Parent constructor
10352 OO.ui.StackLayout.parent.call( this, config );
10353
10354 // Mixin constructors
10355 OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
10356
10357 // Properties
10358 this.currentItem = null;
10359 this.continuous = !!config.continuous;
10360
10361 // Initialization
10362 this.$element.addClass( 'oo-ui-stackLayout' );
10363 if ( this.continuous ) {
10364 this.$element.addClass( 'oo-ui-stackLayout-continuous' );
10365 }
10366 if ( Array.isArray( config.items ) ) {
10367 this.addItems( config.items );
10368 }
10369 };
10370
10371 /* Setup */
10372
10373 OO.inheritClass( OO.ui.StackLayout, OO.ui.PanelLayout );
10374 OO.mixinClass( OO.ui.StackLayout, OO.ui.mixin.GroupElement );
10375
10376 /* Events */
10377
10378 /**
10379 * A 'set' event is emitted when panels are {@link #addItems added}, {@link #removeItems removed},
10380 * {@link #clearItems cleared} or {@link #setItem displayed}.
10381 *
10382 * @event set
10383 * @param {OO.ui.Layout|null} item Current panel or `null` if no panel is shown
10384 */
10385
10386 /* Methods */
10387
10388 /**
10389 * Get the current panel.
10390 *
10391 * @return {OO.ui.Layout|null}
10392 */
10393 OO.ui.StackLayout.prototype.getCurrentItem = function () {
10394 return this.currentItem;
10395 };
10396
10397 /**
10398 * Unset the current item.
10399 *
10400 * @private
10401 * @param {OO.ui.StackLayout} layout
10402 * @fires set
10403 */
10404 OO.ui.StackLayout.prototype.unsetCurrentItem = function () {
10405 var prevItem = this.currentItem;
10406 if ( prevItem === null ) {
10407 return;
10408 }
10409
10410 this.currentItem = null;
10411 this.emit( 'set', null );
10412 };
10413
10414 /**
10415 * Add panel layouts to the stack layout.
10416 *
10417 * Panels will be added to the end of the stack layout array unless the optional index parameter specifies a different
10418 * insertion point. Adding a panel that is already in the stack will move it to the end of the array or the point specified
10419 * by the index.
10420 *
10421 * @param {OO.ui.Layout[]} items Panels to add
10422 * @param {number} [index] Index of the insertion point
10423 * @chainable
10424 */
10425 OO.ui.StackLayout.prototype.addItems = function ( items, index ) {
10426 // Update the visibility
10427 this.updateHiddenState( items, this.currentItem );
10428
10429 // Mixin method
10430 OO.ui.mixin.GroupElement.prototype.addItems.call( this, items, index );
10431
10432 if ( !this.currentItem && items.length ) {
10433 this.setItem( items[ 0 ] );
10434 }
10435
10436 return this;
10437 };
10438
10439 /**
10440 * Remove the specified panels from the stack layout.
10441 *
10442 * Removed panels are detached from the DOM, not removed, so that they may be reused. To remove all panels,
10443 * you may wish to use the #clearItems method instead.
10444 *
10445 * @param {OO.ui.Layout[]} items Panels to remove
10446 * @chainable
10447 * @fires set
10448 */
10449 OO.ui.StackLayout.prototype.removeItems = function ( items ) {
10450 // Mixin method
10451 OO.ui.mixin.GroupElement.prototype.removeItems.call( this, items );
10452
10453 if ( $.inArray( this.currentItem, items ) !== -1 ) {
10454 if ( this.items.length ) {
10455 this.setItem( this.items[ 0 ] );
10456 } else {
10457 this.unsetCurrentItem();
10458 }
10459 }
10460
10461 return this;
10462 };
10463
10464 /**
10465 * Clear all panels from the stack layout.
10466 *
10467 * Cleared panels are detached from the DOM, not removed, so that they may be reused. To remove only
10468 * a subset of panels, use the #removeItems method.
10469 *
10470 * @chainable
10471 * @fires set
10472 */
10473 OO.ui.StackLayout.prototype.clearItems = function () {
10474 this.unsetCurrentItem();
10475 OO.ui.mixin.GroupElement.prototype.clearItems.call( this );
10476
10477 return this;
10478 };
10479
10480 /**
10481 * Show the specified panel.
10482 *
10483 * If another panel is currently displayed, it will be hidden.
10484 *
10485 * @param {OO.ui.Layout} item Panel to show
10486 * @chainable
10487 * @fires set
10488 */
10489 OO.ui.StackLayout.prototype.setItem = function ( item ) {
10490 if ( item !== this.currentItem ) {
10491 this.updateHiddenState( this.items, item );
10492
10493 if ( $.inArray( item, this.items ) !== -1 ) {
10494 this.currentItem = item;
10495 this.emit( 'set', item );
10496 } else {
10497 this.unsetCurrentItem();
10498 }
10499 }
10500
10501 return this;
10502 };
10503
10504 /**
10505 * Update the visibility of all items in case of non-continuous view.
10506 *
10507 * Ensure all items are hidden except for the selected one.
10508 * This method does nothing when the stack is continuous.
10509 *
10510 * @private
10511 * @param {OO.ui.Layout[]} items Item list iterate over
10512 * @param {OO.ui.Layout} [selectedItem] Selected item to show
10513 */
10514 OO.ui.StackLayout.prototype.updateHiddenState = function ( items, selectedItem ) {
10515 var i, len;
10516
10517 if ( !this.continuous ) {
10518 for ( i = 0, len = items.length; i < len; i++ ) {
10519 if ( !selectedItem || selectedItem !== items[ i ] ) {
10520 items[ i ].$element.addClass( 'oo-ui-element-hidden' );
10521 }
10522 }
10523 if ( selectedItem ) {
10524 selectedItem.$element.removeClass( 'oo-ui-element-hidden' );
10525 }
10526 }
10527 };
10528
10529 /**
10530 * BarToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
10531 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.MenuToolGroup MenuToolGroup}
10532 * and {@link OO.ui.ListToolGroup ListToolGroup}). The {@link OO.ui.Tool tools} in a BarToolGroup are
10533 * displayed by icon in a single row. The title of the tool is displayed when users move the mouse over
10534 * the tool.
10535 *
10536 * BarToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar is
10537 * set up.
10538 *
10539 * @example
10540 * // Example of a BarToolGroup with two tools
10541 * var toolFactory = new OO.ui.ToolFactory();
10542 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
10543 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
10544 *
10545 * // We will be placing status text in this element when tools are used
10546 * var $area = $( '<p>' ).text( 'Example of a BarToolGroup with two tools.' );
10547 *
10548 * // Define the tools that we're going to place in our toolbar
10549 *
10550 * // Create a class inheriting from OO.ui.Tool
10551 * function PictureTool() {
10552 * PictureTool.parent.apply( this, arguments );
10553 * }
10554 * OO.inheritClass( PictureTool, OO.ui.Tool );
10555 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
10556 * // of 'icon' and 'title' (displayed icon and text).
10557 * PictureTool.static.name = 'picture';
10558 * PictureTool.static.icon = 'picture';
10559 * PictureTool.static.title = 'Insert picture';
10560 * // Defines the action that will happen when this tool is selected (clicked).
10561 * PictureTool.prototype.onSelect = function () {
10562 * $area.text( 'Picture tool clicked!' );
10563 * // Never display this tool as "active" (selected).
10564 * this.setActive( false );
10565 * };
10566 * // Make this tool available in our toolFactory and thus our toolbar
10567 * toolFactory.register( PictureTool );
10568 *
10569 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
10570 * // little popup window (a PopupWidget).
10571 * function HelpTool( toolGroup, config ) {
10572 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
10573 * padded: true,
10574 * label: 'Help',
10575 * head: true
10576 * } }, config ) );
10577 * this.popup.$body.append( '<p>I am helpful!</p>' );
10578 * }
10579 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
10580 * HelpTool.static.name = 'help';
10581 * HelpTool.static.icon = 'help';
10582 * HelpTool.static.title = 'Help';
10583 * toolFactory.register( HelpTool );
10584 *
10585 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
10586 * // used once (but not all defined tools must be used).
10587 * toolbar.setup( [
10588 * {
10589 * // 'bar' tool groups display tools by icon only
10590 * type: 'bar',
10591 * include: [ 'picture', 'help' ]
10592 * }
10593 * ] );
10594 *
10595 * // Create some UI around the toolbar and place it in the document
10596 * var frame = new OO.ui.PanelLayout( {
10597 * expanded: false,
10598 * framed: true
10599 * } );
10600 * var contentFrame = new OO.ui.PanelLayout( {
10601 * expanded: false,
10602 * padded: true
10603 * } );
10604 * frame.$element.append(
10605 * toolbar.$element,
10606 * contentFrame.$element.append( $area )
10607 * );
10608 * $( 'body' ).append( frame.$element );
10609 *
10610 * // Here is where the toolbar is actually built. This must be done after inserting it into the
10611 * // document.
10612 * toolbar.initialize();
10613 *
10614 * For more information about how to add tools to a bar tool group, please see {@link OO.ui.ToolGroup toolgroup}.
10615 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
10616 *
10617 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
10618 *
10619 * @class
10620 * @extends OO.ui.ToolGroup
10621 *
10622 * @constructor
10623 * @param {OO.ui.Toolbar} toolbar
10624 * @param {Object} [config] Configuration options
10625 */
10626 OO.ui.BarToolGroup = function OoUiBarToolGroup( toolbar, config ) {
10627 // Allow passing positional parameters inside the config object
10628 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
10629 config = toolbar;
10630 toolbar = config.toolbar;
10631 }
10632
10633 // Parent constructor
10634 OO.ui.BarToolGroup.parent.call( this, toolbar, config );
10635
10636 // Initialization
10637 this.$element.addClass( 'oo-ui-barToolGroup' );
10638 };
10639
10640 /* Setup */
10641
10642 OO.inheritClass( OO.ui.BarToolGroup, OO.ui.ToolGroup );
10643
10644 /* Static Properties */
10645
10646 OO.ui.BarToolGroup.static.titleTooltips = true;
10647
10648 OO.ui.BarToolGroup.static.accelTooltips = true;
10649
10650 OO.ui.BarToolGroup.static.name = 'bar';
10651
10652 /**
10653 * PopupToolGroup is an abstract base class used by both {@link OO.ui.MenuToolGroup MenuToolGroup}
10654 * and {@link OO.ui.ListToolGroup ListToolGroup} to provide a popup--an overlaid menu or list of tools with an
10655 * optional icon and label. This class can be used for other base classes that also use this functionality.
10656 *
10657 * @abstract
10658 * @class
10659 * @extends OO.ui.ToolGroup
10660 * @mixins OO.ui.mixin.IconElement
10661 * @mixins OO.ui.mixin.IndicatorElement
10662 * @mixins OO.ui.mixin.LabelElement
10663 * @mixins OO.ui.mixin.TitledElement
10664 * @mixins OO.ui.mixin.ClippableElement
10665 * @mixins OO.ui.mixin.TabIndexedElement
10666 *
10667 * @constructor
10668 * @param {OO.ui.Toolbar} toolbar
10669 * @param {Object} [config] Configuration options
10670 * @cfg {string} [header] Text to display at the top of the popup
10671 */
10672 OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
10673 // Allow passing positional parameters inside the config object
10674 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
10675 config = toolbar;
10676 toolbar = config.toolbar;
10677 }
10678
10679 // Configuration initialization
10680 config = config || {};
10681
10682 // Parent constructor
10683 OO.ui.PopupToolGroup.parent.call( this, toolbar, config );
10684
10685 // Properties
10686 this.active = false;
10687 this.dragging = false;
10688 this.onBlurHandler = this.onBlur.bind( this );
10689 this.$handle = $( '<span>' );
10690
10691 // Mixin constructors
10692 OO.ui.mixin.IconElement.call( this, config );
10693 OO.ui.mixin.IndicatorElement.call( this, config );
10694 OO.ui.mixin.LabelElement.call( this, config );
10695 OO.ui.mixin.TitledElement.call( this, config );
10696 OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
10697 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
10698
10699 // Events
10700 this.$handle.on( {
10701 keydown: this.onHandleMouseKeyDown.bind( this ),
10702 keyup: this.onHandleMouseKeyUp.bind( this ),
10703 mousedown: this.onHandleMouseKeyDown.bind( this ),
10704 mouseup: this.onHandleMouseKeyUp.bind( this )
10705 } );
10706
10707 // Initialization
10708 this.$handle
10709 .addClass( 'oo-ui-popupToolGroup-handle' )
10710 .append( this.$icon, this.$label, this.$indicator );
10711 // If the pop-up should have a header, add it to the top of the toolGroup.
10712 // Note: If this feature is useful for other widgets, we could abstract it into an
10713 // OO.ui.HeaderedElement mixin constructor.
10714 if ( config.header !== undefined ) {
10715 this.$group
10716 .prepend( $( '<span>' )
10717 .addClass( 'oo-ui-popupToolGroup-header' )
10718 .text( config.header )
10719 );
10720 }
10721 this.$element
10722 .addClass( 'oo-ui-popupToolGroup' )
10723 .prepend( this.$handle );
10724 };
10725
10726 /* Setup */
10727
10728 OO.inheritClass( OO.ui.PopupToolGroup, OO.ui.ToolGroup );
10729 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.IconElement );
10730 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.IndicatorElement );
10731 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.LabelElement );
10732 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.TitledElement );
10733 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.ClippableElement );
10734 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.TabIndexedElement );
10735
10736 /* Methods */
10737
10738 /**
10739 * @inheritdoc
10740 */
10741 OO.ui.PopupToolGroup.prototype.setDisabled = function () {
10742 // Parent method
10743 OO.ui.PopupToolGroup.parent.prototype.setDisabled.apply( this, arguments );
10744
10745 if ( this.isDisabled() && this.isElementAttached() ) {
10746 this.setActive( false );
10747 }
10748 };
10749
10750 /**
10751 * Handle focus being lost.
10752 *
10753 * The event is actually generated from a mouseup/keyup, so it is not a normal blur event object.
10754 *
10755 * @protected
10756 * @param {jQuery.Event} e Mouse up or key up event
10757 */
10758 OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) {
10759 // Only deactivate when clicking outside the dropdown element
10760 if ( $( e.target ).closest( '.oo-ui-popupToolGroup' )[ 0 ] !== this.$element[ 0 ] ) {
10761 this.setActive( false );
10762 }
10763 };
10764
10765 /**
10766 * @inheritdoc
10767 */
10768 OO.ui.PopupToolGroup.prototype.onMouseKeyUp = function ( e ) {
10769 // Only close toolgroup when a tool was actually selected
10770 if (
10771 !this.isDisabled() && this.pressed && this.pressed === this.getTargetTool( e ) &&
10772 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
10773 ) {
10774 this.setActive( false );
10775 }
10776 return OO.ui.PopupToolGroup.parent.prototype.onMouseKeyUp.call( this, e );
10777 };
10778
10779 /**
10780 * Handle mouse up and key up events.
10781 *
10782 * @protected
10783 * @param {jQuery.Event} e Mouse up or key up event
10784 */
10785 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyUp = function ( e ) {
10786 if (
10787 !this.isDisabled() &&
10788 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
10789 ) {
10790 return false;
10791 }
10792 };
10793
10794 /**
10795 * Handle mouse down and key down events.
10796 *
10797 * @protected
10798 * @param {jQuery.Event} e Mouse down or key down event
10799 */
10800 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyDown = function ( e ) {
10801 if (
10802 !this.isDisabled() &&
10803 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
10804 ) {
10805 this.setActive( !this.active );
10806 return false;
10807 }
10808 };
10809
10810 /**
10811 * Switch into 'active' mode.
10812 *
10813 * When active, the popup is visible. A mouseup event anywhere in the document will trigger
10814 * deactivation.
10815 */
10816 OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
10817 value = !!value;
10818 if ( this.active !== value ) {
10819 this.active = value;
10820 if ( value ) {
10821 this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler, true );
10822 this.getElementDocument().addEventListener( 'keyup', this.onBlurHandler, true );
10823
10824 // Try anchoring the popup to the left first
10825 this.$element.addClass( 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left' );
10826 this.toggleClipping( true );
10827 if ( this.isClippedHorizontally() ) {
10828 // Anchoring to the left caused the popup to clip, so anchor it to the right instead
10829 this.toggleClipping( false );
10830 this.$element
10831 .removeClass( 'oo-ui-popupToolGroup-left' )
10832 .addClass( 'oo-ui-popupToolGroup-right' );
10833 this.toggleClipping( true );
10834 }
10835 } else {
10836 this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler, true );
10837 this.getElementDocument().removeEventListener( 'keyup', this.onBlurHandler, true );
10838 this.$element.removeClass(
10839 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left oo-ui-popupToolGroup-right'
10840 );
10841 this.toggleClipping( false );
10842 }
10843 }
10844 };
10845
10846 /**
10847 * ListToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
10848 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.MenuToolGroup MenuToolGroup}
10849 * and {@link OO.ui.BarToolGroup BarToolGroup}). The {@link OO.ui.Tool tools} in a ListToolGroup are displayed
10850 * by label in a dropdown menu. The title of the tool is used as the label text. The menu itself can be configured
10851 * with a label, icon, indicator, header, and title.
10852 *
10853 * ListToolGroups can be configured to be expanded and collapsed. Collapsed lists will have a ‘More’ option that
10854 * users can select to see the full list of tools. If a collapsed toolgroup is expanded, a ‘Fewer’ option permits
10855 * users to collapse the list again.
10856 *
10857 * ListToolGroups are created by a {@link OO.ui.ToolGroupFactory toolgroup factory} when the toolbar is set up. The factory
10858 * requires the ListToolGroup's symbolic name, 'list', which is specified along with the other configurations. For more
10859 * information about how to add tools to a ListToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
10860 *
10861 * @example
10862 * // Example of a ListToolGroup
10863 * var toolFactory = new OO.ui.ToolFactory();
10864 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
10865 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
10866 *
10867 * // Configure and register two tools
10868 * function SettingsTool() {
10869 * SettingsTool.parent.apply( this, arguments );
10870 * }
10871 * OO.inheritClass( SettingsTool, OO.ui.Tool );
10872 * SettingsTool.static.name = 'settings';
10873 * SettingsTool.static.icon = 'settings';
10874 * SettingsTool.static.title = 'Change settings';
10875 * SettingsTool.prototype.onSelect = function () {
10876 * this.setActive( false );
10877 * };
10878 * toolFactory.register( SettingsTool );
10879 * // Register two more tools, nothing interesting here
10880 * function StuffTool() {
10881 * StuffTool.parent.apply( this, arguments );
10882 * }
10883 * OO.inheritClass( StuffTool, OO.ui.Tool );
10884 * StuffTool.static.name = 'stuff';
10885 * StuffTool.static.icon = 'ellipsis';
10886 * StuffTool.static.title = 'Change the world';
10887 * StuffTool.prototype.onSelect = function () {
10888 * this.setActive( false );
10889 * };
10890 * toolFactory.register( StuffTool );
10891 * toolbar.setup( [
10892 * {
10893 * // Configurations for list toolgroup.
10894 * type: 'list',
10895 * label: 'ListToolGroup',
10896 * indicator: 'down',
10897 * icon: 'picture',
10898 * title: 'This is the title, displayed when user moves the mouse over the list toolgroup',
10899 * header: 'This is the header',
10900 * include: [ 'settings', 'stuff' ],
10901 * allowCollapse: ['stuff']
10902 * }
10903 * ] );
10904 *
10905 * // Create some UI around the toolbar and place it in the document
10906 * var frame = new OO.ui.PanelLayout( {
10907 * expanded: false,
10908 * framed: true
10909 * } );
10910 * frame.$element.append(
10911 * toolbar.$element
10912 * );
10913 * $( 'body' ).append( frame.$element );
10914 * // Build the toolbar. This must be done after the toolbar has been appended to the document.
10915 * toolbar.initialize();
10916 *
10917 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
10918 *
10919 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
10920 *
10921 * @class
10922 * @extends OO.ui.PopupToolGroup
10923 *
10924 * @constructor
10925 * @param {OO.ui.Toolbar} toolbar
10926 * @param {Object} [config] Configuration options
10927 * @cfg {Array} [allowCollapse] Allow the specified tools to be collapsed. By default, collapsible tools
10928 * will only be displayed if users click the ‘More’ option displayed at the bottom of the list. If
10929 * the list is expanded, a ‘Fewer’ option permits users to collapse the list again. Any tools that
10930 * are included in the toolgroup, but are not designated as collapsible, will always be displayed.
10931 * To open a collapsible list in its expanded state, set #expanded to 'true'.
10932 * @cfg {Array} [forceExpand] Expand the specified tools. All other tools will be designated as collapsible.
10933 * Unless #expanded is set to true, the collapsible tools will be collapsed when the list is first opened.
10934 * @cfg {boolean} [expanded=false] Expand collapsible tools. This config is only relevant if tools have
10935 * been designated as collapsible. When expanded is set to true, all tools in the group will be displayed
10936 * when the list is first opened. Users can collapse the list with a ‘Fewer’ option at the bottom.
10937 */
10938 OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
10939 // Allow passing positional parameters inside the config object
10940 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
10941 config = toolbar;
10942 toolbar = config.toolbar;
10943 }
10944
10945 // Configuration initialization
10946 config = config || {};
10947
10948 // Properties (must be set before parent constructor, which calls #populate)
10949 this.allowCollapse = config.allowCollapse;
10950 this.forceExpand = config.forceExpand;
10951 this.expanded = config.expanded !== undefined ? config.expanded : false;
10952 this.collapsibleTools = [];
10953
10954 // Parent constructor
10955 OO.ui.ListToolGroup.parent.call( this, toolbar, config );
10956
10957 // Initialization
10958 this.$element.addClass( 'oo-ui-listToolGroup' );
10959 };
10960
10961 /* Setup */
10962
10963 OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup );
10964
10965 /* Static Properties */
10966
10967 OO.ui.ListToolGroup.static.name = 'list';
10968
10969 /* Methods */
10970
10971 /**
10972 * @inheritdoc
10973 */
10974 OO.ui.ListToolGroup.prototype.populate = function () {
10975 var i, len, allowCollapse = [];
10976
10977 OO.ui.ListToolGroup.parent.prototype.populate.call( this );
10978
10979 // Update the list of collapsible tools
10980 if ( this.allowCollapse !== undefined ) {
10981 allowCollapse = this.allowCollapse;
10982 } else if ( this.forceExpand !== undefined ) {
10983 allowCollapse = OO.simpleArrayDifference( Object.keys( this.tools ), this.forceExpand );
10984 }
10985
10986 this.collapsibleTools = [];
10987 for ( i = 0, len = allowCollapse.length; i < len; i++ ) {
10988 if ( this.tools[ allowCollapse[ i ] ] !== undefined ) {
10989 this.collapsibleTools.push( this.tools[ allowCollapse[ i ] ] );
10990 }
10991 }
10992
10993 // Keep at the end, even when tools are added
10994 this.$group.append( this.getExpandCollapseTool().$element );
10995
10996 this.getExpandCollapseTool().toggle( this.collapsibleTools.length !== 0 );
10997 this.updateCollapsibleState();
10998 };
10999
11000 OO.ui.ListToolGroup.prototype.getExpandCollapseTool = function () {
11001 if ( this.expandCollapseTool === undefined ) {
11002 var ExpandCollapseTool = function () {
11003 ExpandCollapseTool.parent.apply( this, arguments );
11004 };
11005
11006 OO.inheritClass( ExpandCollapseTool, OO.ui.Tool );
11007
11008 ExpandCollapseTool.prototype.onSelect = function () {
11009 this.toolGroup.expanded = !this.toolGroup.expanded;
11010 this.toolGroup.updateCollapsibleState();
11011 this.setActive( false );
11012 };
11013 ExpandCollapseTool.prototype.onUpdateState = function () {
11014 // Do nothing. Tool interface requires an implementation of this function.
11015 };
11016
11017 ExpandCollapseTool.static.name = 'more-fewer';
11018
11019 this.expandCollapseTool = new ExpandCollapseTool( this );
11020 }
11021 return this.expandCollapseTool;
11022 };
11023
11024 /**
11025 * @inheritdoc
11026 */
11027 OO.ui.ListToolGroup.prototype.onMouseKeyUp = function ( e ) {
11028 // Do not close the popup when the user wants to show more/fewer tools
11029 if (
11030 $( e.target ).closest( '.oo-ui-tool-name-more-fewer' ).length &&
11031 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
11032 ) {
11033 // HACK: Prevent the popup list from being hidden. Skip the PopupToolGroup implementation (which
11034 // hides the popup list when a tool is selected) and call ToolGroup's implementation directly.
11035 return OO.ui.ListToolGroup.parent.parent.prototype.onMouseKeyUp.call( this, e );
11036 } else {
11037 return OO.ui.ListToolGroup.parent.prototype.onMouseKeyUp.call( this, e );
11038 }
11039 };
11040
11041 OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () {
11042 var i, len;
11043
11044 this.getExpandCollapseTool()
11045 .setIcon( this.expanded ? 'collapse' : 'expand' )
11046 .setTitle( OO.ui.msg( this.expanded ? 'ooui-toolgroup-collapse' : 'ooui-toolgroup-expand' ) );
11047
11048 for ( i = 0, len = this.collapsibleTools.length; i < len; i++ ) {
11049 this.collapsibleTools[ i ].toggle( this.expanded );
11050 }
11051 };
11052
11053 /**
11054 * MenuToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
11055 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.BarToolGroup BarToolGroup}
11056 * and {@link OO.ui.ListToolGroup ListToolGroup}). MenuToolGroups contain selectable {@link OO.ui.Tool tools},
11057 * which are displayed by label in a dropdown menu. The tool's title is used as the label text, and the
11058 * menu label is updated to reflect which tool or tools are currently selected. If no tools are selected,
11059 * the menu label is empty. The menu can be configured with an indicator, icon, title, and/or header.
11060 *
11061 * MenuToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar
11062 * is set up. Note that all tools must define an {@link OO.ui.Tool#onUpdateState onUpdateState} method if
11063 * a MenuToolGroup is used.
11064 *
11065 * @example
11066 * // Example of a MenuToolGroup
11067 * var toolFactory = new OO.ui.ToolFactory();
11068 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
11069 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
11070 *
11071 * // We will be placing status text in this element when tools are used
11072 * var $area = $( '<p>' ).text( 'An example of a MenuToolGroup. Select a tool from the dropdown menu.' );
11073 *
11074 * // Define the tools that we're going to place in our toolbar
11075 *
11076 * function SettingsTool() {
11077 * SettingsTool.parent.apply( this, arguments );
11078 * this.reallyActive = false;
11079 * }
11080 * OO.inheritClass( SettingsTool, OO.ui.Tool );
11081 * SettingsTool.static.name = 'settings';
11082 * SettingsTool.static.icon = 'settings';
11083 * SettingsTool.static.title = 'Change settings';
11084 * SettingsTool.prototype.onSelect = function () {
11085 * $area.text( 'Settings tool clicked!' );
11086 * // Toggle the active state on each click
11087 * this.reallyActive = !this.reallyActive;
11088 * this.setActive( this.reallyActive );
11089 * // To update the menu label
11090 * this.toolbar.emit( 'updateState' );
11091 * };
11092 * SettingsTool.prototype.onUpdateState = function () {
11093 * };
11094 * toolFactory.register( SettingsTool );
11095 *
11096 * function StuffTool() {
11097 * StuffTool.parent.apply( this, arguments );
11098 * this.reallyActive = false;
11099 * }
11100 * OO.inheritClass( StuffTool, OO.ui.Tool );
11101 * StuffTool.static.name = 'stuff';
11102 * StuffTool.static.icon = 'ellipsis';
11103 * StuffTool.static.title = 'More stuff';
11104 * StuffTool.prototype.onSelect = function () {
11105 * $area.text( 'More stuff tool clicked!' );
11106 * // Toggle the active state on each click
11107 * this.reallyActive = !this.reallyActive;
11108 * this.setActive( this.reallyActive );
11109 * // To update the menu label
11110 * this.toolbar.emit( 'updateState' );
11111 * };
11112 * StuffTool.prototype.onUpdateState = function () {
11113 * };
11114 * toolFactory.register( StuffTool );
11115 *
11116 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
11117 * // used once (but not all defined tools must be used).
11118 * toolbar.setup( [
11119 * {
11120 * type: 'menu',
11121 * header: 'This is the (optional) header',
11122 * title: 'This is the (optional) title',
11123 * indicator: 'down',
11124 * include: [ 'settings', 'stuff' ]
11125 * }
11126 * ] );
11127 *
11128 * // Create some UI around the toolbar and place it in the document
11129 * var frame = new OO.ui.PanelLayout( {
11130 * expanded: false,
11131 * framed: true
11132 * } );
11133 * var contentFrame = new OO.ui.PanelLayout( {
11134 * expanded: false,
11135 * padded: true
11136 * } );
11137 * frame.$element.append(
11138 * toolbar.$element,
11139 * contentFrame.$element.append( $area )
11140 * );
11141 * $( 'body' ).append( frame.$element );
11142 *
11143 * // Here is where the toolbar is actually built. This must be done after inserting it into the
11144 * // document.
11145 * toolbar.initialize();
11146 * toolbar.emit( 'updateState' );
11147 *
11148 * For more information about how to add tools to a MenuToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
11149 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki] [1].
11150 *
11151 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
11152 *
11153 * @class
11154 * @extends OO.ui.PopupToolGroup
11155 *
11156 * @constructor
11157 * @param {OO.ui.Toolbar} toolbar
11158 * @param {Object} [config] Configuration options
11159 */
11160 OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
11161 // Allow passing positional parameters inside the config object
11162 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
11163 config = toolbar;
11164 toolbar = config.toolbar;
11165 }
11166
11167 // Configuration initialization
11168 config = config || {};
11169
11170 // Parent constructor
11171 OO.ui.MenuToolGroup.parent.call( this, toolbar, config );
11172
11173 // Events
11174 this.toolbar.connect( this, { updateState: 'onUpdateState' } );
11175
11176 // Initialization
11177 this.$element.addClass( 'oo-ui-menuToolGroup' );
11178 };
11179
11180 /* Setup */
11181
11182 OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup );
11183
11184 /* Static Properties */
11185
11186 OO.ui.MenuToolGroup.static.name = 'menu';
11187
11188 /* Methods */
11189
11190 /**
11191 * Handle the toolbar state being updated.
11192 *
11193 * When the state changes, the title of each active item in the menu will be joined together and
11194 * used as a label for the group. The label will be empty if none of the items are active.
11195 *
11196 * @private
11197 */
11198 OO.ui.MenuToolGroup.prototype.onUpdateState = function () {
11199 var name,
11200 labelTexts = [];
11201
11202 for ( name in this.tools ) {
11203 if ( this.tools[ name ].isActive() ) {
11204 labelTexts.push( this.tools[ name ].getTitle() );
11205 }
11206 }
11207
11208 this.setLabel( labelTexts.join( ', ' ) || ' ' );
11209 };
11210
11211 /**
11212 * Popup tools open a popup window when they are selected from the {@link OO.ui.Toolbar toolbar}. Each popup tool is configured
11213 * with a static name, title, and icon, as well with as any popup configurations. Unlike other tools, popup tools do not require that developers specify
11214 * an #onSelect or #onUpdateState method, as these methods have been implemented already.
11215 *
11216 * // Example of a popup tool. When selected, a popup tool displays
11217 * // a popup window.
11218 * function HelpTool( toolGroup, config ) {
11219 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
11220 * padded: true,
11221 * label: 'Help',
11222 * head: true
11223 * } }, config ) );
11224 * this.popup.$body.append( '<p>I am helpful!</p>' );
11225 * };
11226 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
11227 * HelpTool.static.name = 'help';
11228 * HelpTool.static.icon = 'help';
11229 * HelpTool.static.title = 'Help';
11230 * toolFactory.register( HelpTool );
11231 *
11232 * For an example of a toolbar that contains a popup tool, see {@link OO.ui.Toolbar toolbars}. For more information about
11233 * toolbars in genreral, please see the [OOjs UI documentation on MediaWiki][1].
11234 *
11235 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
11236 *
11237 * @abstract
11238 * @class
11239 * @extends OO.ui.Tool
11240 * @mixins OO.ui.mixin.PopupElement
11241 *
11242 * @constructor
11243 * @param {OO.ui.ToolGroup} toolGroup
11244 * @param {Object} [config] Configuration options
11245 */
11246 OO.ui.PopupTool = function OoUiPopupTool( toolGroup, config ) {
11247 // Allow passing positional parameters inside the config object
11248 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
11249 config = toolGroup;
11250 toolGroup = config.toolGroup;
11251 }
11252
11253 // Parent constructor
11254 OO.ui.PopupTool.parent.call( this, toolGroup, config );
11255
11256 // Mixin constructors
11257 OO.ui.mixin.PopupElement.call( this, config );
11258
11259 // Initialization
11260 this.$element
11261 .addClass( 'oo-ui-popupTool' )
11262 .append( this.popup.$element );
11263 };
11264
11265 /* Setup */
11266
11267 OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool );
11268 OO.mixinClass( OO.ui.PopupTool, OO.ui.mixin.PopupElement );
11269
11270 /* Methods */
11271
11272 /**
11273 * Handle the tool being selected.
11274 *
11275 * @inheritdoc
11276 */
11277 OO.ui.PopupTool.prototype.onSelect = function () {
11278 if ( !this.isDisabled() ) {
11279 this.popup.toggle();
11280 }
11281 this.setActive( false );
11282 return false;
11283 };
11284
11285 /**
11286 * Handle the toolbar state being updated.
11287 *
11288 * @inheritdoc
11289 */
11290 OO.ui.PopupTool.prototype.onUpdateState = function () {
11291 this.setActive( false );
11292 };
11293
11294 /**
11295 * A ToolGroupTool is a special sort of tool that can contain other {@link OO.ui.Tool tools}
11296 * and {@link OO.ui.ToolGroup toolgroups}. The ToolGroupTool was specifically designed to be used
11297 * inside a {@link OO.ui.BarToolGroup bar} toolgroup to provide access to additional tools from
11298 * the bar item. Included tools will be displayed in a dropdown {@link OO.ui.ListToolGroup list}
11299 * when the ToolGroupTool is selected.
11300 *
11301 * // Example: ToolGroupTool with two nested tools, 'setting1' and 'setting2', defined elsewhere.
11302 *
11303 * function SettingsTool() {
11304 * SettingsTool.parent.apply( this, arguments );
11305 * };
11306 * OO.inheritClass( SettingsTool, OO.ui.ToolGroupTool );
11307 * SettingsTool.static.name = 'settings';
11308 * SettingsTool.static.title = 'Change settings';
11309 * SettingsTool.static.groupConfig = {
11310 * icon: 'settings',
11311 * label: 'ToolGroupTool',
11312 * include: [ 'setting1', 'setting2' ]
11313 * };
11314 * toolFactory.register( SettingsTool );
11315 *
11316 * For more information, please see the [OOjs UI documentation on MediaWiki][1].
11317 *
11318 * Please note that this implementation is subject to change per [T74159] [2].
11319 *
11320 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars#ToolGroupTool
11321 * [2]: https://phabricator.wikimedia.org/T74159
11322 *
11323 * @abstract
11324 * @class
11325 * @extends OO.ui.Tool
11326 *
11327 * @constructor
11328 * @param {OO.ui.ToolGroup} toolGroup
11329 * @param {Object} [config] Configuration options
11330 */
11331 OO.ui.ToolGroupTool = function OoUiToolGroupTool( toolGroup, config ) {
11332 // Allow passing positional parameters inside the config object
11333 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
11334 config = toolGroup;
11335 toolGroup = config.toolGroup;
11336 }
11337
11338 // Parent constructor
11339 OO.ui.ToolGroupTool.parent.call( this, toolGroup, config );
11340
11341 // Properties
11342 this.innerToolGroup = this.createGroup( this.constructor.static.groupConfig );
11343
11344 // Events
11345 this.innerToolGroup.connect( this, { disable: 'onToolGroupDisable' } );
11346
11347 // Initialization
11348 this.$link.remove();
11349 this.$element
11350 .addClass( 'oo-ui-toolGroupTool' )
11351 .append( this.innerToolGroup.$element );
11352 };
11353
11354 /* Setup */
11355
11356 OO.inheritClass( OO.ui.ToolGroupTool, OO.ui.Tool );
11357
11358 /* Static Properties */
11359
11360 /**
11361 * Toolgroup configuration.
11362 *
11363 * The toolgroup configuration consists of the tools to include, as well as an icon and label
11364 * to use for the bar item. Tools can be included by symbolic name, group, or with the
11365 * wildcard selector. Please see {@link OO.ui.ToolGroup toolgroup} for more information.
11366 *
11367 * @property {Object.<string,Array>}
11368 */
11369 OO.ui.ToolGroupTool.static.groupConfig = {};
11370
11371 /* Methods */
11372
11373 /**
11374 * Handle the tool being selected.
11375 *
11376 * @inheritdoc
11377 */
11378 OO.ui.ToolGroupTool.prototype.onSelect = function () {
11379 this.innerToolGroup.setActive( !this.innerToolGroup.active );
11380 return false;
11381 };
11382
11383 /**
11384 * Synchronize disabledness state of the tool with the inner toolgroup.
11385 *
11386 * @private
11387 * @param {boolean} disabled Element is disabled
11388 */
11389 OO.ui.ToolGroupTool.prototype.onToolGroupDisable = function ( disabled ) {
11390 this.setDisabled( disabled );
11391 };
11392
11393 /**
11394 * Handle the toolbar state being updated.
11395 *
11396 * @inheritdoc
11397 */
11398 OO.ui.ToolGroupTool.prototype.onUpdateState = function () {
11399 this.setActive( false );
11400 };
11401
11402 /**
11403 * Build a {@link OO.ui.ToolGroup toolgroup} from the specified configuration.
11404 *
11405 * @param {Object.<string,Array>} group Toolgroup configuration. Please see {@link OO.ui.ToolGroup toolgroup} for
11406 * more information.
11407 * @return {OO.ui.ListToolGroup}
11408 */
11409 OO.ui.ToolGroupTool.prototype.createGroup = function ( group ) {
11410 if ( group.include === '*' ) {
11411 // Apply defaults to catch-all groups
11412 if ( group.label === undefined ) {
11413 group.label = OO.ui.msg( 'ooui-toolbar-more' );
11414 }
11415 }
11416
11417 return this.toolbar.getToolGroupFactory().create( 'list', this.toolbar, group );
11418 };
11419
11420 /**
11421 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
11422 *
11423 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
11424 *
11425 * @private
11426 * @abstract
11427 * @class
11428 * @extends OO.ui.mixin.GroupElement
11429 *
11430 * @constructor
11431 * @param {Object} [config] Configuration options
11432 */
11433 OO.ui.mixin.GroupWidget = function OoUiMixinGroupWidget( config ) {
11434 // Parent constructor
11435 OO.ui.mixin.GroupWidget.parent.call( this, config );
11436 };
11437
11438 /* Setup */
11439
11440 OO.inheritClass( OO.ui.mixin.GroupWidget, OO.ui.mixin.GroupElement );
11441
11442 /* Methods */
11443
11444 /**
11445 * Set the disabled state of the widget.
11446 *
11447 * This will also update the disabled state of child widgets.
11448 *
11449 * @param {boolean} disabled Disable widget
11450 * @chainable
11451 */
11452 OO.ui.mixin.GroupWidget.prototype.setDisabled = function ( disabled ) {
11453 var i, len;
11454
11455 // Parent method
11456 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
11457 OO.ui.Widget.prototype.setDisabled.call( this, disabled );
11458
11459 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
11460 if ( this.items ) {
11461 for ( i = 0, len = this.items.length; i < len; i++ ) {
11462 this.items[ i ].updateDisabled();
11463 }
11464 }
11465
11466 return this;
11467 };
11468
11469 /**
11470 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
11471 *
11472 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
11473 * allows bidirectional communication.
11474 *
11475 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
11476 *
11477 * @private
11478 * @abstract
11479 * @class
11480 *
11481 * @constructor
11482 */
11483 OO.ui.mixin.ItemWidget = function OoUiMixinItemWidget() {
11484 //
11485 };
11486
11487 /* Methods */
11488
11489 /**
11490 * Check if widget is disabled.
11491 *
11492 * Checks parent if present, making disabled state inheritable.
11493 *
11494 * @return {boolean} Widget is disabled
11495 */
11496 OO.ui.mixin.ItemWidget.prototype.isDisabled = function () {
11497 return this.disabled ||
11498 ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() );
11499 };
11500
11501 /**
11502 * Set group element is in.
11503 *
11504 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
11505 * @chainable
11506 */
11507 OO.ui.mixin.ItemWidget.prototype.setElementGroup = function ( group ) {
11508 // Parent method
11509 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
11510 OO.ui.Element.prototype.setElementGroup.call( this, group );
11511
11512 // Initialize item disabled states
11513 this.updateDisabled();
11514
11515 return this;
11516 };
11517
11518 /**
11519 * OutlineControlsWidget is a set of controls for an {@link OO.ui.OutlineSelectWidget outline select widget}.
11520 * Controls include moving items up and down, removing items, and adding different kinds of items.
11521 *
11522 * **Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.**
11523 *
11524 * @class
11525 * @extends OO.ui.Widget
11526 * @mixins OO.ui.mixin.GroupElement
11527 * @mixins OO.ui.mixin.IconElement
11528 *
11529 * @constructor
11530 * @param {OO.ui.OutlineSelectWidget} outline Outline to control
11531 * @param {Object} [config] Configuration options
11532 * @cfg {Object} [abilities] List of abilties
11533 * @cfg {boolean} [abilities.move=true] Allow moving movable items
11534 * @cfg {boolean} [abilities.remove=true] Allow removing removable items
11535 */
11536 OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
11537 // Allow passing positional parameters inside the config object
11538 if ( OO.isPlainObject( outline ) && config === undefined ) {
11539 config = outline;
11540 outline = config.outline;
11541 }
11542
11543 // Configuration initialization
11544 config = $.extend( { icon: 'add' }, config );
11545
11546 // Parent constructor
11547 OO.ui.OutlineControlsWidget.parent.call( this, config );
11548
11549 // Mixin constructors
11550 OO.ui.mixin.GroupElement.call( this, config );
11551 OO.ui.mixin.IconElement.call( this, config );
11552
11553 // Properties
11554 this.outline = outline;
11555 this.$movers = $( '<div>' );
11556 this.upButton = new OO.ui.ButtonWidget( {
11557 framed: false,
11558 icon: 'collapse',
11559 title: OO.ui.msg( 'ooui-outline-control-move-up' )
11560 } );
11561 this.downButton = new OO.ui.ButtonWidget( {
11562 framed: false,
11563 icon: 'expand',
11564 title: OO.ui.msg( 'ooui-outline-control-move-down' )
11565 } );
11566 this.removeButton = new OO.ui.ButtonWidget( {
11567 framed: false,
11568 icon: 'remove',
11569 title: OO.ui.msg( 'ooui-outline-control-remove' )
11570 } );
11571 this.abilities = { move: true, remove: true };
11572
11573 // Events
11574 outline.connect( this, {
11575 select: 'onOutlineChange',
11576 add: 'onOutlineChange',
11577 remove: 'onOutlineChange'
11578 } );
11579 this.upButton.connect( this, { click: [ 'emit', 'move', -1 ] } );
11580 this.downButton.connect( this, { click: [ 'emit', 'move', 1 ] } );
11581 this.removeButton.connect( this, { click: [ 'emit', 'remove' ] } );
11582
11583 // Initialization
11584 this.$element.addClass( 'oo-ui-outlineControlsWidget' );
11585 this.$group.addClass( 'oo-ui-outlineControlsWidget-items' );
11586 this.$movers
11587 .addClass( 'oo-ui-outlineControlsWidget-movers' )
11588 .append( this.removeButton.$element, this.upButton.$element, this.downButton.$element );
11589 this.$element.append( this.$icon, this.$group, this.$movers );
11590 this.setAbilities( config.abilities || {} );
11591 };
11592
11593 /* Setup */
11594
11595 OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget );
11596 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.mixin.GroupElement );
11597 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.mixin.IconElement );
11598
11599 /* Events */
11600
11601 /**
11602 * @event move
11603 * @param {number} places Number of places to move
11604 */
11605
11606 /**
11607 * @event remove
11608 */
11609
11610 /* Methods */
11611
11612 /**
11613 * Set abilities.
11614 *
11615 * @param {Object} abilities List of abilties
11616 * @param {boolean} [abilities.move] Allow moving movable items
11617 * @param {boolean} [abilities.remove] Allow removing removable items
11618 */
11619 OO.ui.OutlineControlsWidget.prototype.setAbilities = function ( abilities ) {
11620 var ability;
11621
11622 for ( ability in this.abilities ) {
11623 if ( abilities[ability] !== undefined ) {
11624 this.abilities[ability] = !!abilities[ability];
11625 }
11626 }
11627
11628 this.onOutlineChange();
11629 };
11630
11631 /**
11632 *
11633 * @private
11634 * Handle outline change events.
11635 */
11636 OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () {
11637 var i, len, firstMovable, lastMovable,
11638 items = this.outline.getItems(),
11639 selectedItem = this.outline.getSelectedItem(),
11640 movable = this.abilities.move && selectedItem && selectedItem.isMovable(),
11641 removable = this.abilities.remove && selectedItem && selectedItem.isRemovable();
11642
11643 if ( movable ) {
11644 i = -1;
11645 len = items.length;
11646 while ( ++i < len ) {
11647 if ( items[ i ].isMovable() ) {
11648 firstMovable = items[ i ];
11649 break;
11650 }
11651 }
11652 i = len;
11653 while ( i-- ) {
11654 if ( items[ i ].isMovable() ) {
11655 lastMovable = items[ i ];
11656 break;
11657 }
11658 }
11659 }
11660 this.upButton.setDisabled( !movable || selectedItem === firstMovable );
11661 this.downButton.setDisabled( !movable || selectedItem === lastMovable );
11662 this.removeButton.setDisabled( !removable );
11663 };
11664
11665 /**
11666 * ToggleWidget implements basic behavior of widgets with an on/off state.
11667 * Please see OO.ui.ToggleButtonWidget and OO.ui.ToggleSwitchWidget for examples.
11668 *
11669 * @abstract
11670 * @class
11671 * @extends OO.ui.Widget
11672 *
11673 * @constructor
11674 * @param {Object} [config] Configuration options
11675 * @cfg {boolean} [value=false] The toggle’s initial on/off state.
11676 * By default, the toggle is in the 'off' state.
11677 */
11678 OO.ui.ToggleWidget = function OoUiToggleWidget( config ) {
11679 // Configuration initialization
11680 config = config || {};
11681
11682 // Parent constructor
11683 OO.ui.ToggleWidget.parent.call( this, config );
11684
11685 // Properties
11686 this.value = null;
11687
11688 // Initialization
11689 this.$element.addClass( 'oo-ui-toggleWidget' );
11690 this.setValue( !!config.value );
11691 };
11692
11693 /* Setup */
11694
11695 OO.inheritClass( OO.ui.ToggleWidget, OO.ui.Widget );
11696
11697 /* Events */
11698
11699 /**
11700 * @event change
11701 *
11702 * A change event is emitted when the on/off state of the toggle changes.
11703 *
11704 * @param {boolean} value Value representing the new state of the toggle
11705 */
11706
11707 /* Methods */
11708
11709 /**
11710 * Get the value representing the toggle’s state.
11711 *
11712 * @return {boolean} The on/off state of the toggle
11713 */
11714 OO.ui.ToggleWidget.prototype.getValue = function () {
11715 return this.value;
11716 };
11717
11718 /**
11719 * Set the state of the toggle: `true` for 'on', `false' for 'off'.
11720 *
11721 * @param {boolean} value The state of the toggle
11722 * @fires change
11723 * @chainable
11724 */
11725 OO.ui.ToggleWidget.prototype.setValue = function ( value ) {
11726 value = !!value;
11727 if ( this.value !== value ) {
11728 this.value = value;
11729 this.emit( 'change', value );
11730 this.$element.toggleClass( 'oo-ui-toggleWidget-on', value );
11731 this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value );
11732 this.$element.attr( 'aria-checked', value.toString() );
11733 }
11734 return this;
11735 };
11736
11737 /**
11738 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
11739 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
11740 * removed, and cleared from the group.
11741 *
11742 * @example
11743 * // Example: A ButtonGroupWidget with two buttons
11744 * var button1 = new OO.ui.PopupButtonWidget( {
11745 * label: 'Select a category',
11746 * icon: 'menu',
11747 * popup: {
11748 * $content: $( '<p>List of categories...</p>' ),
11749 * padded: true,
11750 * align: 'left'
11751 * }
11752 * } );
11753 * var button2 = new OO.ui.ButtonWidget( {
11754 * label: 'Add item'
11755 * });
11756 * var buttonGroup = new OO.ui.ButtonGroupWidget( {
11757 * items: [button1, button2]
11758 * } );
11759 * $( 'body' ).append( buttonGroup.$element );
11760 *
11761 * @class
11762 * @extends OO.ui.Widget
11763 * @mixins OO.ui.mixin.GroupElement
11764 *
11765 * @constructor
11766 * @param {Object} [config] Configuration options
11767 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
11768 */
11769 OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
11770 // Configuration initialization
11771 config = config || {};
11772
11773 // Parent constructor
11774 OO.ui.ButtonGroupWidget.parent.call( this, config );
11775
11776 // Mixin constructors
11777 OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
11778
11779 // Initialization
11780 this.$element.addClass( 'oo-ui-buttonGroupWidget' );
11781 if ( Array.isArray( config.items ) ) {
11782 this.addItems( config.items );
11783 }
11784 };
11785
11786 /* Setup */
11787
11788 OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget );
11789 OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.mixin.GroupElement );
11790
11791 /**
11792 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
11793 * feels, and functionality can be customized via the class’s configuration options
11794 * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
11795 * and examples.
11796 *
11797 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
11798 *
11799 * @example
11800 * // A button widget
11801 * var button = new OO.ui.ButtonWidget( {
11802 * label: 'Button with Icon',
11803 * icon: 'remove',
11804 * iconTitle: 'Remove'
11805 * } );
11806 * $( 'body' ).append( button.$element );
11807 *
11808 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
11809 *
11810 * @class
11811 * @extends OO.ui.Widget
11812 * @mixins OO.ui.mixin.ButtonElement
11813 * @mixins OO.ui.mixin.IconElement
11814 * @mixins OO.ui.mixin.IndicatorElement
11815 * @mixins OO.ui.mixin.LabelElement
11816 * @mixins OO.ui.mixin.TitledElement
11817 * @mixins OO.ui.mixin.FlaggedElement
11818 * @mixins OO.ui.mixin.TabIndexedElement
11819 *
11820 * @constructor
11821 * @param {Object} [config] Configuration options
11822 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
11823 * @cfg {string} [target] The frame or window in which to open the hyperlink.
11824 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
11825 */
11826 OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
11827 // Configuration initialization
11828 config = config || {};
11829
11830 // Parent constructor
11831 OO.ui.ButtonWidget.parent.call( this, config );
11832
11833 // Mixin constructors
11834 OO.ui.mixin.ButtonElement.call( this, config );
11835 OO.ui.mixin.IconElement.call( this, config );
11836 OO.ui.mixin.IndicatorElement.call( this, config );
11837 OO.ui.mixin.LabelElement.call( this, config );
11838 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
11839 OO.ui.mixin.FlaggedElement.call( this, config );
11840 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
11841
11842 // Properties
11843 this.href = null;
11844 this.target = null;
11845 this.noFollow = false;
11846
11847 // Events
11848 this.connect( this, { disable: 'onDisable' } );
11849
11850 // Initialization
11851 this.$button.append( this.$icon, this.$label, this.$indicator );
11852 this.$element
11853 .addClass( 'oo-ui-buttonWidget' )
11854 .append( this.$button );
11855 this.setHref( config.href );
11856 this.setTarget( config.target );
11857 this.setNoFollow( config.noFollow );
11858 };
11859
11860 /* Setup */
11861
11862 OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget );
11863 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.ButtonElement );
11864 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.IconElement );
11865 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.IndicatorElement );
11866 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.LabelElement );
11867 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.TitledElement );
11868 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.FlaggedElement );
11869 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.TabIndexedElement );
11870
11871 /* Methods */
11872
11873 /**
11874 * @inheritdoc
11875 */
11876 OO.ui.ButtonWidget.prototype.onMouseDown = function ( e ) {
11877 if ( !this.isDisabled() ) {
11878 // Remove the tab-index while the button is down to prevent the button from stealing focus
11879 this.$button.removeAttr( 'tabindex' );
11880 }
11881
11882 return OO.ui.mixin.ButtonElement.prototype.onMouseDown.call( this, e );
11883 };
11884
11885 /**
11886 * @inheritdoc
11887 */
11888 OO.ui.ButtonWidget.prototype.onMouseUp = function ( e ) {
11889 if ( !this.isDisabled() ) {
11890 // Restore the tab-index after the button is up to restore the button's accessibility
11891 this.$button.attr( 'tabindex', this.tabIndex );
11892 }
11893
11894 return OO.ui.mixin.ButtonElement.prototype.onMouseUp.call( this, e );
11895 };
11896
11897 /**
11898 * Get hyperlink location.
11899 *
11900 * @return {string} Hyperlink location
11901 */
11902 OO.ui.ButtonWidget.prototype.getHref = function () {
11903 return this.href;
11904 };
11905
11906 /**
11907 * Get hyperlink target.
11908 *
11909 * @return {string} Hyperlink target
11910 */
11911 OO.ui.ButtonWidget.prototype.getTarget = function () {
11912 return this.target;
11913 };
11914
11915 /**
11916 * Get search engine traversal hint.
11917 *
11918 * @return {boolean} Whether search engines should avoid traversing this hyperlink
11919 */
11920 OO.ui.ButtonWidget.prototype.getNoFollow = function () {
11921 return this.noFollow;
11922 };
11923
11924 /**
11925 * Set hyperlink location.
11926 *
11927 * @param {string|null} href Hyperlink location, null to remove
11928 */
11929 OO.ui.ButtonWidget.prototype.setHref = function ( href ) {
11930 href = typeof href === 'string' ? href : null;
11931
11932 if ( href !== this.href ) {
11933 this.href = href;
11934 this.updateHref();
11935 }
11936
11937 return this;
11938 };
11939
11940 /**
11941 * Update the `href` attribute, in case of changes to href or
11942 * disabled state.
11943 *
11944 * @private
11945 * @chainable
11946 */
11947 OO.ui.ButtonWidget.prototype.updateHref = function () {
11948 if ( this.href !== null && !this.isDisabled() ) {
11949 this.$button.attr( 'href', this.href );
11950 } else {
11951 this.$button.removeAttr( 'href' );
11952 }
11953
11954 return this;
11955 };
11956
11957 /**
11958 * Handle disable events.
11959 *
11960 * @private
11961 * @param {boolean} disabled Element is disabled
11962 */
11963 OO.ui.ButtonWidget.prototype.onDisable = function () {
11964 this.updateHref();
11965 };
11966
11967 /**
11968 * Set hyperlink target.
11969 *
11970 * @param {string|null} target Hyperlink target, null to remove
11971 */
11972 OO.ui.ButtonWidget.prototype.setTarget = function ( target ) {
11973 target = typeof target === 'string' ? target : null;
11974
11975 if ( target !== this.target ) {
11976 this.target = target;
11977 if ( target !== null ) {
11978 this.$button.attr( 'target', target );
11979 } else {
11980 this.$button.removeAttr( 'target' );
11981 }
11982 }
11983
11984 return this;
11985 };
11986
11987 /**
11988 * Set search engine traversal hint.
11989 *
11990 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
11991 */
11992 OO.ui.ButtonWidget.prototype.setNoFollow = function ( noFollow ) {
11993 noFollow = typeof noFollow === 'boolean' ? noFollow : true;
11994
11995 if ( noFollow !== this.noFollow ) {
11996 this.noFollow = noFollow;
11997 if ( noFollow ) {
11998 this.$button.attr( 'rel', 'nofollow' );
11999 } else {
12000 this.$button.removeAttr( 'rel' );
12001 }
12002 }
12003
12004 return this;
12005 };
12006
12007 /**
12008 * An ActionWidget is a {@link OO.ui.ButtonWidget button widget} that executes an action.
12009 * Action widgets are used with OO.ui.ActionSet, which manages the behavior and availability
12010 * of the actions.
12011 *
12012 * Both actions and action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
12013 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information
12014 * and examples.
12015 *
12016 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
12017 *
12018 * @class
12019 * @extends OO.ui.ButtonWidget
12020 * @mixins OO.ui.mixin.PendingElement
12021 *
12022 * @constructor
12023 * @param {Object} [config] Configuration options
12024 * @cfg {string} [action] Symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
12025 * @cfg {string[]} [modes] Symbolic names of the modes (e.g., ‘edit’ or ‘read’) in which the action
12026 * should be made available. See the action set's {@link OO.ui.ActionSet#setMode setMode} method
12027 * for more information about setting modes.
12028 * @cfg {boolean} [framed=false] Render the action button with a frame
12029 */
12030 OO.ui.ActionWidget = function OoUiActionWidget( config ) {
12031 // Configuration initialization
12032 config = $.extend( { framed: false }, config );
12033
12034 // Parent constructor
12035 OO.ui.ActionWidget.parent.call( this, config );
12036
12037 // Mixin constructors
12038 OO.ui.mixin.PendingElement.call( this, config );
12039
12040 // Properties
12041 this.action = config.action || '';
12042 this.modes = config.modes || [];
12043 this.width = 0;
12044 this.height = 0;
12045
12046 // Initialization
12047 this.$element.addClass( 'oo-ui-actionWidget' );
12048 };
12049
12050 /* Setup */
12051
12052 OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget );
12053 OO.mixinClass( OO.ui.ActionWidget, OO.ui.mixin.PendingElement );
12054
12055 /* Events */
12056
12057 /**
12058 * A resize event is emitted when the size of the widget changes.
12059 *
12060 * @event resize
12061 */
12062
12063 /* Methods */
12064
12065 /**
12066 * Check if the action is configured to be available in the specified `mode`.
12067 *
12068 * @param {string} mode Name of mode
12069 * @return {boolean} The action is configured with the mode
12070 */
12071 OO.ui.ActionWidget.prototype.hasMode = function ( mode ) {
12072 return this.modes.indexOf( mode ) !== -1;
12073 };
12074
12075 /**
12076 * Get the symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
12077 *
12078 * @return {string}
12079 */
12080 OO.ui.ActionWidget.prototype.getAction = function () {
12081 return this.action;
12082 };
12083
12084 /**
12085 * Get the symbolic name of the mode or modes for which the action is configured to be available.
12086 *
12087 * The current mode is set with the action set's {@link OO.ui.ActionSet#setMode setMode} method.
12088 * Only actions that are configured to be avaiable in the current mode will be visible. All other actions
12089 * are hidden.
12090 *
12091 * @return {string[]}
12092 */
12093 OO.ui.ActionWidget.prototype.getModes = function () {
12094 return this.modes.slice();
12095 };
12096
12097 /**
12098 * Emit a resize event if the size has changed.
12099 *
12100 * @private
12101 * @chainable
12102 */
12103 OO.ui.ActionWidget.prototype.propagateResize = function () {
12104 var width, height;
12105
12106 if ( this.isElementAttached() ) {
12107 width = this.$element.width();
12108 height = this.$element.height();
12109
12110 if ( width !== this.width || height !== this.height ) {
12111 this.width = width;
12112 this.height = height;
12113 this.emit( 'resize' );
12114 }
12115 }
12116
12117 return this;
12118 };
12119
12120 /**
12121 * @inheritdoc
12122 */
12123 OO.ui.ActionWidget.prototype.setIcon = function () {
12124 // Mixin method
12125 OO.ui.mixin.IconElement.prototype.setIcon.apply( this, arguments );
12126 this.propagateResize();
12127
12128 return this;
12129 };
12130
12131 /**
12132 * @inheritdoc
12133 */
12134 OO.ui.ActionWidget.prototype.setLabel = function () {
12135 // Mixin method
12136 OO.ui.mixin.LabelElement.prototype.setLabel.apply( this, arguments );
12137 this.propagateResize();
12138
12139 return this;
12140 };
12141
12142 /**
12143 * @inheritdoc
12144 */
12145 OO.ui.ActionWidget.prototype.setFlags = function () {
12146 // Mixin method
12147 OO.ui.mixin.FlaggedElement.prototype.setFlags.apply( this, arguments );
12148 this.propagateResize();
12149
12150 return this;
12151 };
12152
12153 /**
12154 * @inheritdoc
12155 */
12156 OO.ui.ActionWidget.prototype.clearFlags = function () {
12157 // Mixin method
12158 OO.ui.mixin.FlaggedElement.prototype.clearFlags.apply( this, arguments );
12159 this.propagateResize();
12160
12161 return this;
12162 };
12163
12164 /**
12165 * Toggle the visibility of the action button.
12166 *
12167 * @param {boolean} [show] Show button, omit to toggle visibility
12168 * @chainable
12169 */
12170 OO.ui.ActionWidget.prototype.toggle = function () {
12171 // Parent method
12172 OO.ui.ActionWidget.parent.prototype.toggle.apply( this, arguments );
12173 this.propagateResize();
12174
12175 return this;
12176 };
12177
12178 /**
12179 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
12180 * which is used to display additional information or options.
12181 *
12182 * @example
12183 * // Example of a popup button.
12184 * var popupButton = new OO.ui.PopupButtonWidget( {
12185 * label: 'Popup button with options',
12186 * icon: 'menu',
12187 * popup: {
12188 * $content: $( '<p>Additional options here.</p>' ),
12189 * padded: true,
12190 * align: 'force-left'
12191 * }
12192 * } );
12193 * // Append the button to the DOM.
12194 * $( 'body' ).append( popupButton.$element );
12195 *
12196 * @class
12197 * @extends OO.ui.ButtonWidget
12198 * @mixins OO.ui.mixin.PopupElement
12199 *
12200 * @constructor
12201 * @param {Object} [config] Configuration options
12202 */
12203 OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
12204 // Parent constructor
12205 OO.ui.PopupButtonWidget.parent.call( this, config );
12206
12207 // Mixin constructors
12208 OO.ui.mixin.PopupElement.call( this, config );
12209
12210 // Events
12211 this.connect( this, { click: 'onAction' } );
12212
12213 // Initialization
12214 this.$element
12215 .addClass( 'oo-ui-popupButtonWidget' )
12216 .attr( 'aria-haspopup', 'true' )
12217 .append( this.popup.$element );
12218 };
12219
12220 /* Setup */
12221
12222 OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
12223 OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.mixin.PopupElement );
12224
12225 /* Methods */
12226
12227 /**
12228 * Handle the button action being triggered.
12229 *
12230 * @private
12231 */
12232 OO.ui.PopupButtonWidget.prototype.onAction = function () {
12233 this.popup.toggle();
12234 };
12235
12236 /**
12237 * ToggleButtons are buttons that have a state (‘on’ or ‘off’) that is represented by a
12238 * Boolean value. Like other {@link OO.ui.ButtonWidget buttons}, toggle buttons can be
12239 * configured with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators},
12240 * {@link OO.ui.mixin.TitledElement titles}, {@link OO.ui.mixin.FlaggedElement styling flags},
12241 * and {@link OO.ui.mixin.LabelElement labels}. Please see
12242 * the [OOjs UI documentation][1] on MediaWiki for more information.
12243 *
12244 * @example
12245 * // Toggle buttons in the 'off' and 'on' state.
12246 * var toggleButton1 = new OO.ui.ToggleButtonWidget( {
12247 * label: 'Toggle Button off'
12248 * } );
12249 * var toggleButton2 = new OO.ui.ToggleButtonWidget( {
12250 * label: 'Toggle Button on',
12251 * value: true
12252 * } );
12253 * // Append the buttons to the DOM.
12254 * $( 'body' ).append( toggleButton1.$element, toggleButton2.$element );
12255 *
12256 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Toggle_buttons
12257 *
12258 * @class
12259 * @extends OO.ui.ToggleWidget
12260 * @mixins OO.ui.mixin.ButtonElement
12261 * @mixins OO.ui.mixin.IconElement
12262 * @mixins OO.ui.mixin.IndicatorElement
12263 * @mixins OO.ui.mixin.LabelElement
12264 * @mixins OO.ui.mixin.TitledElement
12265 * @mixins OO.ui.mixin.FlaggedElement
12266 * @mixins OO.ui.mixin.TabIndexedElement
12267 *
12268 * @constructor
12269 * @param {Object} [config] Configuration options
12270 * @cfg {boolean} [value=false] The toggle button’s initial on/off
12271 * state. By default, the button is in the 'off' state.
12272 */
12273 OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) {
12274 // Configuration initialization
12275 config = config || {};
12276
12277 // Parent constructor
12278 OO.ui.ToggleButtonWidget.parent.call( this, config );
12279
12280 // Mixin constructors
12281 OO.ui.mixin.ButtonElement.call( this, config );
12282 OO.ui.mixin.IconElement.call( this, config );
12283 OO.ui.mixin.IndicatorElement.call( this, config );
12284 OO.ui.mixin.LabelElement.call( this, config );
12285 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
12286 OO.ui.mixin.FlaggedElement.call( this, config );
12287 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
12288
12289 // Events
12290 this.connect( this, { click: 'onAction' } );
12291
12292 // Initialization
12293 this.$button.append( this.$icon, this.$label, this.$indicator );
12294 this.$element
12295 .addClass( 'oo-ui-toggleButtonWidget' )
12296 .append( this.$button );
12297 };
12298
12299 /* Setup */
12300
12301 OO.inheritClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget );
12302 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.ButtonElement );
12303 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.IconElement );
12304 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.IndicatorElement );
12305 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.LabelElement );
12306 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.TitledElement );
12307 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.FlaggedElement );
12308 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.TabIndexedElement );
12309
12310 /* Methods */
12311
12312 /**
12313 * Handle the button action being triggered.
12314 *
12315 * @private
12316 */
12317 OO.ui.ToggleButtonWidget.prototype.onAction = function () {
12318 this.setValue( !this.value );
12319 };
12320
12321 /**
12322 * @inheritdoc
12323 */
12324 OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
12325 value = !!value;
12326 if ( value !== this.value ) {
12327 // Might be called from parent constructor before ButtonElement constructor
12328 if ( this.$button ) {
12329 this.$button.attr( 'aria-pressed', value.toString() );
12330 }
12331 this.setActive( value );
12332 }
12333
12334 // Parent method
12335 OO.ui.ToggleButtonWidget.parent.prototype.setValue.call( this, value );
12336
12337 return this;
12338 };
12339
12340 /**
12341 * @inheritdoc
12342 */
12343 OO.ui.ToggleButtonWidget.prototype.setButtonElement = function ( $button ) {
12344 if ( this.$button ) {
12345 this.$button.removeAttr( 'aria-pressed' );
12346 }
12347 OO.ui.mixin.ButtonElement.prototype.setButtonElement.call( this, $button );
12348 this.$button.attr( 'aria-pressed', this.value.toString() );
12349 };
12350
12351 /**
12352 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
12353 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
12354 * users can interact with it.
12355 *
12356 * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
12357 * OO.ui.DropdownInputWidget instead.
12358 *
12359 * @example
12360 * // Example: A DropdownWidget with a menu that contains three options
12361 * var dropDown = new OO.ui.DropdownWidget( {
12362 * label: 'Dropdown menu: Select a menu option',
12363 * menu: {
12364 * items: [
12365 * new OO.ui.MenuOptionWidget( {
12366 * data: 'a',
12367 * label: 'First'
12368 * } ),
12369 * new OO.ui.MenuOptionWidget( {
12370 * data: 'b',
12371 * label: 'Second'
12372 * } ),
12373 * new OO.ui.MenuOptionWidget( {
12374 * data: 'c',
12375 * label: 'Third'
12376 * } )
12377 * ]
12378 * }
12379 * } );
12380 *
12381 * $( 'body' ).append( dropDown.$element );
12382 *
12383 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
12384 *
12385 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
12386 *
12387 * @class
12388 * @extends OO.ui.Widget
12389 * @mixins OO.ui.mixin.IconElement
12390 * @mixins OO.ui.mixin.IndicatorElement
12391 * @mixins OO.ui.mixin.LabelElement
12392 * @mixins OO.ui.mixin.TitledElement
12393 * @mixins OO.ui.mixin.TabIndexedElement
12394 *
12395 * @constructor
12396 * @param {Object} [config] Configuration options
12397 * @cfg {Object} [menu] Configuration options to pass to menu widget
12398 */
12399 OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
12400 // Configuration initialization
12401 config = $.extend( { indicator: 'down' }, config );
12402
12403 // Parent constructor
12404 OO.ui.DropdownWidget.parent.call( this, config );
12405
12406 // Properties (must be set before TabIndexedElement constructor call)
12407 this.$handle = this.$( '<span>' );
12408
12409 // Mixin constructors
12410 OO.ui.mixin.IconElement.call( this, config );
12411 OO.ui.mixin.IndicatorElement.call( this, config );
12412 OO.ui.mixin.LabelElement.call( this, config );
12413 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
12414 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
12415
12416 // Properties
12417 this.menu = new OO.ui.MenuSelectWidget( $.extend( { widget: this }, config.menu ) );
12418
12419 // Events
12420 this.$handle.on( {
12421 click: this.onClick.bind( this ),
12422 keypress: this.onKeyPress.bind( this )
12423 } );
12424 this.menu.connect( this, { select: 'onMenuSelect' } );
12425
12426 // Initialization
12427 this.$handle
12428 .addClass( 'oo-ui-dropdownWidget-handle' )
12429 .append( this.$icon, this.$label, this.$indicator );
12430 this.$element
12431 .addClass( 'oo-ui-dropdownWidget' )
12432 .append( this.$handle, this.menu.$element );
12433 };
12434
12435 /* Setup */
12436
12437 OO.inheritClass( OO.ui.DropdownWidget, OO.ui.Widget );
12438 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.IconElement );
12439 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.IndicatorElement );
12440 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.LabelElement );
12441 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.TitledElement );
12442 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.TabIndexedElement );
12443
12444 /* Methods */
12445
12446 /**
12447 * Get the menu.
12448 *
12449 * @return {OO.ui.MenuSelectWidget} Menu of widget
12450 */
12451 OO.ui.DropdownWidget.prototype.getMenu = function () {
12452 return this.menu;
12453 };
12454
12455 /**
12456 * Handles menu select events.
12457 *
12458 * @private
12459 * @param {OO.ui.MenuOptionWidget} item Selected menu item
12460 */
12461 OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
12462 var selectedLabel;
12463
12464 if ( !item ) {
12465 this.setLabel( null );
12466 return;
12467 }
12468
12469 selectedLabel = item.getLabel();
12470
12471 // If the label is a DOM element, clone it, because setLabel will append() it
12472 if ( selectedLabel instanceof jQuery ) {
12473 selectedLabel = selectedLabel.clone();
12474 }
12475
12476 this.setLabel( selectedLabel );
12477 };
12478
12479 /**
12480 * Handle mouse click events.
12481 *
12482 * @private
12483 * @param {jQuery.Event} e Mouse click event
12484 */
12485 OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
12486 if ( !this.isDisabled() && e.which === 1 ) {
12487 this.menu.toggle();
12488 }
12489 return false;
12490 };
12491
12492 /**
12493 * Handle key press events.
12494 *
12495 * @private
12496 * @param {jQuery.Event} e Key press event
12497 */
12498 OO.ui.DropdownWidget.prototype.onKeyPress = function ( e ) {
12499 if ( !this.isDisabled() &&
12500 ( ( e.which === OO.ui.Keys.SPACE && !this.menu.isVisible() ) || e.which === OO.ui.Keys.ENTER )
12501 ) {
12502 this.menu.toggle();
12503 return false;
12504 }
12505 };
12506
12507 /**
12508 * SelectFileWidgets allow for selecting files, using the HTML5 File API. These
12509 * widgets can be configured with {@link OO.ui.mixin.IconElement icons} and {@link
12510 * OO.ui.mixin.IndicatorElement indicators}.
12511 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
12512 *
12513 * @example
12514 * // Example of a file select widget
12515 * var selectFile = new OO.ui.SelectFileWidget();
12516 * $( 'body' ).append( selectFile.$element );
12517 *
12518 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets
12519 *
12520 * @class
12521 * @extends OO.ui.Widget
12522 * @mixins OO.ui.mixin.IconElement
12523 * @mixins OO.ui.mixin.IndicatorElement
12524 * @mixins OO.ui.mixin.PendingElement
12525 * @mixins OO.ui.mixin.LabelElement
12526 * @mixins OO.ui.mixin.TabIndexedElement
12527 *
12528 * @constructor
12529 * @param {Object} [config] Configuration options
12530 * @cfg {string[]|null} [accept=null] MIME types to accept. null accepts all types.
12531 * @cfg {string} [placeholder] Text to display when no file is selected.
12532 * @cfg {string} [notsupported] Text to display when file support is missing in the browser.
12533 * @cfg {boolean} [droppable=true] Whether to accept files by drag and drop.
12534 */
12535 OO.ui.SelectFileWidget = function OoUiSelectFileWidget( config ) {
12536 var dragHandler;
12537
12538 // Configuration initialization
12539 config = $.extend( {
12540 accept: null,
12541 placeholder: OO.ui.msg( 'ooui-selectfile-placeholder' ),
12542 notsupported: OO.ui.msg( 'ooui-selectfile-not-supported' ),
12543 droppable: true
12544 }, config );
12545
12546 // Parent constructor
12547 OO.ui.SelectFileWidget.parent.call( this, config );
12548
12549 // Properties (must be set before TabIndexedElement constructor call)
12550 this.$handle = $( '<span>' );
12551
12552 // Mixin constructors
12553 OO.ui.mixin.IconElement.call( this, config );
12554 OO.ui.mixin.IndicatorElement.call( this, config );
12555 OO.ui.mixin.PendingElement.call( this, config );
12556 OO.ui.mixin.LabelElement.call( this, $.extend( config, { autoFitLabel: true } ) );
12557 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
12558
12559 // Properties
12560 this.isSupported = this.constructor.static.isSupported();
12561 this.currentFile = null;
12562 if ( Array.isArray( config.accept ) ) {
12563 this.accept = config.accept;
12564 } else {
12565 this.accept = null;
12566 }
12567 this.placeholder = config.placeholder;
12568 this.notsupported = config.notsupported;
12569 this.onFileSelectedHandler = this.onFileSelected.bind( this );
12570
12571 this.clearButton = new OO.ui.ButtonWidget( {
12572 classes: [ 'oo-ui-selectFileWidget-clearButton' ],
12573 framed: false,
12574 icon: 'remove',
12575 disabled: this.disabled
12576 } );
12577
12578 // Events
12579 this.$handle.on( {
12580 keypress: this.onKeyPress.bind( this )
12581 } );
12582 this.clearButton.connect( this, {
12583 click: 'onClearClick'
12584 } );
12585 if ( config.droppable ) {
12586 dragHandler = this.onDragEnterOrOver.bind( this );
12587 this.$handle.on( {
12588 dragenter: dragHandler,
12589 dragover: dragHandler,
12590 dragleave: this.onDragLeave.bind( this ),
12591 drop: this.onDrop.bind( this )
12592 } );
12593 }
12594
12595 // Initialization
12596 this.addInput();
12597 this.updateUI();
12598 this.$label.addClass( 'oo-ui-selectFileWidget-label' );
12599 this.$handle
12600 .addClass( 'oo-ui-selectFileWidget-handle' )
12601 .append( this.$icon, this.$label, this.clearButton.$element, this.$indicator );
12602 this.$element
12603 .addClass( 'oo-ui-selectFileWidget' )
12604 .append( this.$handle );
12605 if ( config.droppable ) {
12606 this.$element.addClass( 'oo-ui-selectFileWidget-droppable' );
12607 }
12608 };
12609
12610 /* Setup */
12611
12612 OO.inheritClass( OO.ui.SelectFileWidget, OO.ui.Widget );
12613 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.IconElement );
12614 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.IndicatorElement );
12615 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.PendingElement );
12616 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.LabelElement );
12617 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.TabIndexedElement );
12618
12619 /* Static properties */
12620
12621 /**
12622 * Check if this widget is supported
12623 *
12624 * @static
12625 * @return {boolean}
12626 */
12627 OO.ui.SelectFileWidget.static.isSupported = function () {
12628 var $input;
12629 if ( OO.ui.SelectFileWidget.static.isSupportedCache === null ) {
12630 $input = $( '<input type="file">' );
12631 OO.ui.SelectFileWidget.static.isSupportedCache = $input[0].files !== undefined;
12632 }
12633 return OO.ui.SelectFileWidget.static.isSupportedCache;
12634 };
12635
12636 OO.ui.SelectFileWidget.static.isSupportedCache = null;
12637
12638 /* Events */
12639
12640 /**
12641 * @event change
12642 *
12643 * A change event is emitted when the on/off state of the toggle changes.
12644 *
12645 * @param {File|null} value New value
12646 */
12647
12648 /* Methods */
12649
12650 /**
12651 * Get the current value of the field
12652 *
12653 * @return {File|null}
12654 */
12655 OO.ui.SelectFileWidget.prototype.getValue = function () {
12656 return this.currentFile;
12657 };
12658
12659 /**
12660 * Set the current value of the field
12661 *
12662 * @param {File|null} file File to select
12663 */
12664 OO.ui.SelectFileWidget.prototype.setValue = function ( file ) {
12665 if ( this.currentFile !== file ) {
12666 this.currentFile = file;
12667 this.updateUI();
12668 this.emit( 'change', this.currentFile );
12669 }
12670 };
12671
12672 /**
12673 * Update the user interface when a file is selected or unselected
12674 *
12675 * @protected
12676 */
12677 OO.ui.SelectFileWidget.prototype.updateUI = function () {
12678 if ( !this.isSupported ) {
12679 this.$element.addClass( 'oo-ui-selectFileWidget-notsupported' );
12680 this.$element.removeClass( 'oo-ui-selectFileWidget-empty' );
12681 this.setLabel( this.notsupported );
12682 } else if ( this.currentFile ) {
12683 this.$element.removeClass( 'oo-ui-selectFileWidget-empty' );
12684 this.setLabel( this.currentFile.name +
12685 ( this.currentFile.type !== '' ? OO.ui.msg( 'ooui-semicolon-separator' ) + this.currentFile.type : '' )
12686 );
12687 } else {
12688 this.$element.addClass( 'oo-ui-selectFileWidget-empty' );
12689 this.setLabel( this.placeholder );
12690 }
12691
12692 if ( this.$input ) {
12693 this.$input.attr( 'title', this.getLabel() );
12694 }
12695 };
12696
12697 /**
12698 * Add the input to the handle
12699 *
12700 * @private
12701 */
12702 OO.ui.SelectFileWidget.prototype.addInput = function () {
12703 if ( this.$input ) {
12704 this.$input.remove();
12705 }
12706
12707 if ( !this.isSupported ) {
12708 this.$input = null;
12709 return;
12710 }
12711
12712 this.$input = $( '<input type="file">' );
12713 this.$input.on( 'change', this.onFileSelectedHandler );
12714 this.$input.attr( {
12715 tabindex: -1,
12716 title: this.getLabel()
12717 } );
12718 if ( this.accept ) {
12719 this.$input.attr( 'accept', this.accept.join( ', ' ) );
12720 }
12721 this.$handle.append( this.$input );
12722 };
12723
12724 /**
12725 * Determine if we should accept this file
12726 *
12727 * @private
12728 * @param {File} file
12729 * @return {boolean}
12730 */
12731 OO.ui.SelectFileWidget.prototype.isFileAcceptable = function ( file ) {
12732 var i, mime, mimeTest;
12733
12734 if ( !this.accept || file.type === '' ) {
12735 return true;
12736 }
12737
12738 mime = file.type;
12739 for ( i = 0; i < this.accept.length; i++ ) {
12740 mimeTest = this.accept[i];
12741 if ( mimeTest === mime ) {
12742 return true;
12743 } else if ( mimeTest.substr( -2 ) === '/*' ) {
12744 mimeTest = mimeTest.substr( 0, mimeTest.length - 1 );
12745 if ( mime.substr( 0, mimeTest.length ) === mimeTest ) {
12746 return true;
12747 }
12748 }
12749 }
12750
12751 return false;
12752 };
12753
12754 /**
12755 * Handle file selection from the input
12756 *
12757 * @private
12758 * @param {jQuery.Event} e
12759 */
12760 OO.ui.SelectFileWidget.prototype.onFileSelected = function ( e ) {
12761 var file = null;
12762
12763 if ( e.target.files && e.target.files[0] ) {
12764 file = e.target.files[0];
12765 if ( !this.isFileAcceptable( file ) ) {
12766 file = null;
12767 }
12768 }
12769
12770 this.setValue( file );
12771 this.addInput();
12772 };
12773
12774 /**
12775 * Handle clear button click events.
12776 *
12777 * @private
12778 */
12779 OO.ui.SelectFileWidget.prototype.onClearClick = function () {
12780 this.setValue( null );
12781 return false;
12782 };
12783
12784 /**
12785 * Handle key press events.
12786 *
12787 * @private
12788 * @param {jQuery.Event} e Key press event
12789 */
12790 OO.ui.SelectFileWidget.prototype.onKeyPress = function ( e ) {
12791 if ( this.isSupported && !this.isDisabled() && this.$input &&
12792 ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
12793 ) {
12794 this.$input.click();
12795 return false;
12796 }
12797 };
12798
12799 /**
12800 * Handle drag enter and over events
12801 *
12802 * @private
12803 * @param {jQuery.Event} e Drag event
12804 */
12805 OO.ui.SelectFileWidget.prototype.onDragEnterOrOver = function ( e ) {
12806 var file = null,
12807 dt = e.originalEvent.dataTransfer;
12808
12809 e.preventDefault();
12810 e.stopPropagation();
12811
12812 if ( this.isDisabled() || !this.isSupported ) {
12813 this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
12814 dt.dropEffect = 'none';
12815 return false;
12816 }
12817
12818 if ( dt && dt.files && dt.files[0] ) {
12819 file = dt.files[0];
12820 if ( !this.isFileAcceptable( file ) ) {
12821 file = null;
12822 }
12823 } else if ( dt && dt.types && $.inArray( 'Files', dt.types ) ) {
12824 // We know we have files so set 'file' to something truthy, we just
12825 // can't know any details about them.
12826 // * https://bugzilla.mozilla.org/show_bug.cgi?id=640534
12827 file = 'Files exist, but details are unknown';
12828 }
12829 if ( file ) {
12830 this.$element.addClass( 'oo-ui-selectFileWidget-canDrop' );
12831 } else {
12832 this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
12833 dt.dropEffect = 'none';
12834 }
12835
12836 return false;
12837 };
12838
12839 /**
12840 * Handle drag leave events
12841 *
12842 * @private
12843 * @param {jQuery.Event} e Drag event
12844 */
12845 OO.ui.SelectFileWidget.prototype.onDragLeave = function () {
12846 this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
12847 };
12848
12849 /**
12850 * Handle drop events
12851 *
12852 * @private
12853 * @param {jQuery.Event} e Drop event
12854 */
12855 OO.ui.SelectFileWidget.prototype.onDrop = function ( e ) {
12856 var file = null,
12857 dt = e.originalEvent.dataTransfer;
12858
12859 e.preventDefault();
12860 e.stopPropagation();
12861 this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
12862
12863 if ( this.isDisabled() || !this.isSupported ) {
12864 return false;
12865 }
12866
12867 if ( dt && dt.files && dt.files[0] ) {
12868 file = dt.files[0];
12869 if ( !this.isFileAcceptable( file ) ) {
12870 file = null;
12871 }
12872 }
12873 if ( file ) {
12874 this.setValue( file );
12875 }
12876
12877 return false;
12878 };
12879
12880 /**
12881 * @inheritdoc
12882 */
12883 OO.ui.SelectFileWidget.prototype.setDisabled = function ( state ) {
12884 OO.ui.SelectFileWidget.parent.prototype.setDisabled.call( this, state );
12885 if ( this.clearButton ) {
12886 this.clearButton.setDisabled( state );
12887 }
12888 return this;
12889 };
12890
12891 /**
12892 * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
12893 * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
12894 * for a list of icons included in the library.
12895 *
12896 * @example
12897 * // An icon widget with a label
12898 * var myIcon = new OO.ui.IconWidget( {
12899 * icon: 'help',
12900 * iconTitle: 'Help'
12901 * } );
12902 * // Create a label.
12903 * var iconLabel = new OO.ui.LabelWidget( {
12904 * label: 'Help'
12905 * } );
12906 * $( 'body' ).append( myIcon.$element, iconLabel.$element );
12907 *
12908 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
12909 *
12910 * @class
12911 * @extends OO.ui.Widget
12912 * @mixins OO.ui.mixin.IconElement
12913 * @mixins OO.ui.mixin.TitledElement
12914 * @mixins OO.ui.mixin.FlaggedElement
12915 *
12916 * @constructor
12917 * @param {Object} [config] Configuration options
12918 */
12919 OO.ui.IconWidget = function OoUiIconWidget( config ) {
12920 // Configuration initialization
12921 config = config || {};
12922
12923 // Parent constructor
12924 OO.ui.IconWidget.parent.call( this, config );
12925
12926 // Mixin constructors
12927 OO.ui.mixin.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) );
12928 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
12929 OO.ui.mixin.FlaggedElement.call( this, $.extend( {}, config, { $flagged: this.$element } ) );
12930
12931 // Initialization
12932 this.$element.addClass( 'oo-ui-iconWidget' );
12933 };
12934
12935 /* Setup */
12936
12937 OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
12938 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.IconElement );
12939 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.TitledElement );
12940 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.FlaggedElement );
12941
12942 /* Static Properties */
12943
12944 OO.ui.IconWidget.static.tagName = 'span';
12945
12946 /**
12947 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
12948 * attention to the status of an item or to clarify the function of a control. For a list of
12949 * indicators included in the library, please see the [OOjs UI documentation on MediaWiki][1].
12950 *
12951 * @example
12952 * // Example of an indicator widget
12953 * var indicator1 = new OO.ui.IndicatorWidget( {
12954 * indicator: 'alert'
12955 * } );
12956 *
12957 * // Create a fieldset layout to add a label
12958 * var fieldset = new OO.ui.FieldsetLayout();
12959 * fieldset.addItems( [
12960 * new OO.ui.FieldLayout( indicator1, { label: 'An alert indicator:' } )
12961 * ] );
12962 * $( 'body' ).append( fieldset.$element );
12963 *
12964 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
12965 *
12966 * @class
12967 * @extends OO.ui.Widget
12968 * @mixins OO.ui.mixin.IndicatorElement
12969 * @mixins OO.ui.mixin.TitledElement
12970 *
12971 * @constructor
12972 * @param {Object} [config] Configuration options
12973 */
12974 OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
12975 // Configuration initialization
12976 config = config || {};
12977
12978 // Parent constructor
12979 OO.ui.IndicatorWidget.parent.call( this, config );
12980
12981 // Mixin constructors
12982 OO.ui.mixin.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) );
12983 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
12984
12985 // Initialization
12986 this.$element.addClass( 'oo-ui-indicatorWidget' );
12987 };
12988
12989 /* Setup */
12990
12991 OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
12992 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.mixin.IndicatorElement );
12993 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.mixin.TitledElement );
12994
12995 /* Static Properties */
12996
12997 OO.ui.IndicatorWidget.static.tagName = 'span';
12998
12999 /**
13000 * InputWidget is the base class for all input widgets, which
13001 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
13002 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
13003 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
13004 *
13005 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
13006 *
13007 * @abstract
13008 * @class
13009 * @extends OO.ui.Widget
13010 * @mixins OO.ui.mixin.FlaggedElement
13011 * @mixins OO.ui.mixin.TabIndexedElement
13012 *
13013 * @constructor
13014 * @param {Object} [config] Configuration options
13015 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
13016 * @cfg {string} [value=''] The value of the input.
13017 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
13018 * before it is accepted.
13019 */
13020 OO.ui.InputWidget = function OoUiInputWidget( config ) {
13021 // Configuration initialization
13022 config = config || {};
13023
13024 // Parent constructor
13025 OO.ui.InputWidget.parent.call( this, config );
13026
13027 // Properties
13028 this.$input = this.getInputElement( config );
13029 this.value = '';
13030 this.inputFilter = config.inputFilter;
13031
13032 // Mixin constructors
13033 OO.ui.mixin.FlaggedElement.call( this, config );
13034 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$input } ) );
13035
13036 // Events
13037 this.$input.on( 'keydown mouseup cut paste change input select', this.onEdit.bind( this ) );
13038
13039 // Initialization
13040 this.$input
13041 .attr( 'name', config.name )
13042 .prop( 'disabled', this.isDisabled() );
13043 this.$element.addClass( 'oo-ui-inputWidget' ).append( this.$input, $( '<span>' ) );
13044 this.setValue( config.value );
13045 };
13046
13047 /* Setup */
13048
13049 OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
13050 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.FlaggedElement );
13051 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.TabIndexedElement );
13052
13053 /* Static Properties */
13054
13055 OO.ui.InputWidget.static.supportsSimpleLabel = true;
13056
13057 /* Events */
13058
13059 /**
13060 * @event change
13061 *
13062 * A change event is emitted when the value of the input changes.
13063 *
13064 * @param {string} value
13065 */
13066
13067 /* Methods */
13068
13069 /**
13070 * Get input element.
13071 *
13072 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
13073 * different circumstances. The element must have a `value` property (like form elements).
13074 *
13075 * @protected
13076 * @param {Object} config Configuration options
13077 * @return {jQuery} Input element
13078 */
13079 OO.ui.InputWidget.prototype.getInputElement = function () {
13080 return $( '<input>' );
13081 };
13082
13083 /**
13084 * Handle potentially value-changing events.
13085 *
13086 * @private
13087 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
13088 */
13089 OO.ui.InputWidget.prototype.onEdit = function () {
13090 var widget = this;
13091 if ( !this.isDisabled() ) {
13092 // Allow the stack to clear so the value will be updated
13093 setTimeout( function () {
13094 widget.setValue( widget.$input.val() );
13095 } );
13096 }
13097 };
13098
13099 /**
13100 * Get the value of the input.
13101 *
13102 * @return {string} Input value
13103 */
13104 OO.ui.InputWidget.prototype.getValue = function () {
13105 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
13106 // it, and we won't know unless they're kind enough to trigger a 'change' event.
13107 var value = this.$input.val();
13108 if ( this.value !== value ) {
13109 this.setValue( value );
13110 }
13111 return this.value;
13112 };
13113
13114 /**
13115 * Set the direction of the input, either RTL (right-to-left) or LTR (left-to-right).
13116 *
13117 * @param {boolean} isRTL
13118 * Direction is right-to-left
13119 */
13120 OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
13121 this.$input.prop( 'dir', isRTL ? 'rtl' : 'ltr' );
13122 };
13123
13124 /**
13125 * Set the value of the input.
13126 *
13127 * @param {string} value New value
13128 * @fires change
13129 * @chainable
13130 */
13131 OO.ui.InputWidget.prototype.setValue = function ( value ) {
13132 value = this.cleanUpValue( value );
13133 // Update the DOM if it has changed. Note that with cleanUpValue, it
13134 // is possible for the DOM value to change without this.value changing.
13135 if ( this.$input.val() !== value ) {
13136 this.$input.val( value );
13137 }
13138 if ( this.value !== value ) {
13139 this.value = value;
13140 this.emit( 'change', this.value );
13141 }
13142 return this;
13143 };
13144
13145 /**
13146 * Clean up incoming value.
13147 *
13148 * Ensures value is a string, and converts undefined and null to empty string.
13149 *
13150 * @private
13151 * @param {string} value Original value
13152 * @return {string} Cleaned up value
13153 */
13154 OO.ui.InputWidget.prototype.cleanUpValue = function ( value ) {
13155 if ( value === undefined || value === null ) {
13156 return '';
13157 } else if ( this.inputFilter ) {
13158 return this.inputFilter( String( value ) );
13159 } else {
13160 return String( value );
13161 }
13162 };
13163
13164 /**
13165 * Simulate the behavior of clicking on a label bound to this input. This method is only called by
13166 * {@link OO.ui.LabelWidget LabelWidget} and {@link OO.ui.FieldLayout FieldLayout}. It should not be
13167 * called directly.
13168 */
13169 OO.ui.InputWidget.prototype.simulateLabelClick = function () {
13170 if ( !this.isDisabled() ) {
13171 if ( this.$input.is( ':checkbox, :radio' ) ) {
13172 this.$input.click();
13173 }
13174 if ( this.$input.is( ':input' ) ) {
13175 this.$input[ 0 ].focus();
13176 }
13177 }
13178 };
13179
13180 /**
13181 * @inheritdoc
13182 */
13183 OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
13184 OO.ui.InputWidget.parent.prototype.setDisabled.call( this, state );
13185 if ( this.$input ) {
13186 this.$input.prop( 'disabled', this.isDisabled() );
13187 }
13188 return this;
13189 };
13190
13191 /**
13192 * Focus the input.
13193 *
13194 * @chainable
13195 */
13196 OO.ui.InputWidget.prototype.focus = function () {
13197 this.$input[ 0 ].focus();
13198 return this;
13199 };
13200
13201 /**
13202 * Blur the input.
13203 *
13204 * @chainable
13205 */
13206 OO.ui.InputWidget.prototype.blur = function () {
13207 this.$input[ 0 ].blur();
13208 return this;
13209 };
13210
13211 /**
13212 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
13213 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
13214 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
13215 * HTML `<button/>` (the default) or an HTML `<input/>` tags. See the
13216 * [OOjs UI documentation on MediaWiki] [1] for more information.
13217 *
13218 * @example
13219 * // A ButtonInputWidget rendered as an HTML button, the default.
13220 * var button = new OO.ui.ButtonInputWidget( {
13221 * label: 'Input button',
13222 * icon: 'check',
13223 * value: 'check'
13224 * } );
13225 * $( 'body' ).append( button.$element );
13226 *
13227 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
13228 *
13229 * @class
13230 * @extends OO.ui.InputWidget
13231 * @mixins OO.ui.mixin.ButtonElement
13232 * @mixins OO.ui.mixin.IconElement
13233 * @mixins OO.ui.mixin.IndicatorElement
13234 * @mixins OO.ui.mixin.LabelElement
13235 * @mixins OO.ui.mixin.TitledElement
13236 *
13237 * @constructor
13238 * @param {Object} [config] Configuration options
13239 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
13240 * @cfg {boolean} [useInputTag=false] Use an `<input/>` tag instead of a `<button/>` tag, the default.
13241 * Widgets configured to be an `<input/>` do not support {@link #icon icons} and {@link #indicator indicators},
13242 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
13243 * be set to `true` when there’s need to support IE6 in a form with multiple buttons.
13244 */
13245 OO.ui.ButtonInputWidget = function OoUiButtonInputWidget( config ) {
13246 // Configuration initialization
13247 config = $.extend( { type: 'button', useInputTag: false }, config );
13248
13249 // Properties (must be set before parent constructor, which calls #setValue)
13250 this.useInputTag = config.useInputTag;
13251
13252 // Parent constructor
13253 OO.ui.ButtonInputWidget.parent.call( this, config );
13254
13255 // Mixin constructors
13256 OO.ui.mixin.ButtonElement.call( this, $.extend( {}, config, { $button: this.$input } ) );
13257 OO.ui.mixin.IconElement.call( this, config );
13258 OO.ui.mixin.IndicatorElement.call( this, config );
13259 OO.ui.mixin.LabelElement.call( this, config );
13260 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
13261
13262 // Initialization
13263 if ( !config.useInputTag ) {
13264 this.$input.append( this.$icon, this.$label, this.$indicator );
13265 }
13266 this.$element.addClass( 'oo-ui-buttonInputWidget' );
13267 };
13268
13269 /* Setup */
13270
13271 OO.inheritClass( OO.ui.ButtonInputWidget, OO.ui.InputWidget );
13272 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.ButtonElement );
13273 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IconElement );
13274 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IndicatorElement );
13275 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.LabelElement );
13276 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.TitledElement );
13277
13278 /* Methods */
13279
13280 /**
13281 * @inheritdoc
13282 * @protected
13283 */
13284 OO.ui.ButtonInputWidget.prototype.getInputElement = function ( config ) {
13285 var type = [ 'button', 'submit', 'reset' ].indexOf( config.type ) !== -1 ?
13286 config.type :
13287 'button';
13288 return $( '<' + ( config.useInputTag ? 'input' : 'button' ) + ' type="' + type + '">' );
13289 };
13290
13291 /**
13292 * Set label value.
13293 *
13294 * If #useInputTag is `true`, the label is set as the `value` of the `<input/>` tag.
13295 *
13296 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
13297 * text, or `null` for no label
13298 * @chainable
13299 */
13300 OO.ui.ButtonInputWidget.prototype.setLabel = function ( label ) {
13301 OO.ui.mixin.LabelElement.prototype.setLabel.call( this, label );
13302
13303 if ( this.useInputTag ) {
13304 if ( typeof label === 'function' ) {
13305 label = OO.ui.resolveMsg( label );
13306 }
13307 if ( label instanceof jQuery ) {
13308 label = label.text();
13309 }
13310 if ( !label ) {
13311 label = '';
13312 }
13313 this.$input.val( label );
13314 }
13315
13316 return this;
13317 };
13318
13319 /**
13320 * Set the value of the input.
13321 *
13322 * This method is disabled for button inputs configured as {@link #useInputTag <input/> tags}, as
13323 * they do not support {@link #value values}.
13324 *
13325 * @param {string} value New value
13326 * @chainable
13327 */
13328 OO.ui.ButtonInputWidget.prototype.setValue = function ( value ) {
13329 if ( !this.useInputTag ) {
13330 OO.ui.ButtonInputWidget.parent.prototype.setValue.call( this, value );
13331 }
13332 return this;
13333 };
13334
13335 /**
13336 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
13337 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
13338 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
13339 * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
13340 *
13341 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
13342 *
13343 * @example
13344 * // An example of selected, unselected, and disabled checkbox inputs
13345 * var checkbox1=new OO.ui.CheckboxInputWidget( {
13346 * value: 'a',
13347 * selected: true
13348 * } );
13349 * var checkbox2=new OO.ui.CheckboxInputWidget( {
13350 * value: 'b'
13351 * } );
13352 * var checkbox3=new OO.ui.CheckboxInputWidget( {
13353 * value:'c',
13354 * disabled: true
13355 * } );
13356 * // Create a fieldset layout with fields for each checkbox.
13357 * var fieldset = new OO.ui.FieldsetLayout( {
13358 * label: 'Checkboxes'
13359 * } );
13360 * fieldset.addItems( [
13361 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
13362 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
13363 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
13364 * ] );
13365 * $( 'body' ).append( fieldset.$element );
13366 *
13367 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
13368 *
13369 * @class
13370 * @extends OO.ui.InputWidget
13371 *
13372 * @constructor
13373 * @param {Object} [config] Configuration options
13374 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
13375 */
13376 OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
13377 // Configuration initialization
13378 config = config || {};
13379
13380 // Parent constructor
13381 OO.ui.CheckboxInputWidget.parent.call( this, config );
13382
13383 // Initialization
13384 this.$element.addClass( 'oo-ui-checkboxInputWidget' );
13385 this.setSelected( config.selected !== undefined ? config.selected : false );
13386 };
13387
13388 /* Setup */
13389
13390 OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
13391
13392 /* Methods */
13393
13394 /**
13395 * @inheritdoc
13396 * @protected
13397 */
13398 OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
13399 return $( '<input type="checkbox" />' );
13400 };
13401
13402 /**
13403 * @inheritdoc
13404 */
13405 OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
13406 var widget = this;
13407 if ( !this.isDisabled() ) {
13408 // Allow the stack to clear so the value will be updated
13409 setTimeout( function () {
13410 widget.setSelected( widget.$input.prop( 'checked' ) );
13411 } );
13412 }
13413 };
13414
13415 /**
13416 * Set selection state of this checkbox.
13417 *
13418 * @param {boolean} state `true` for selected
13419 * @chainable
13420 */
13421 OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state ) {
13422 state = !!state;
13423 if ( this.selected !== state ) {
13424 this.selected = state;
13425 this.$input.prop( 'checked', this.selected );
13426 this.emit( 'change', this.selected );
13427 }
13428 return this;
13429 };
13430
13431 /**
13432 * Check if this checkbox is selected.
13433 *
13434 * @return {boolean} Checkbox is selected
13435 */
13436 OO.ui.CheckboxInputWidget.prototype.isSelected = function () {
13437 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
13438 // it, and we won't know unless they're kind enough to trigger a 'change' event.
13439 var selected = this.$input.prop( 'checked' );
13440 if ( this.selected !== selected ) {
13441 this.setSelected( selected );
13442 }
13443 return this.selected;
13444 };
13445
13446 /**
13447 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
13448 * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
13449 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
13450 * more information about input widgets.
13451 *
13452 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
13453 * are no options. If no `value` configuration option is provided, the first option is selected.
13454 * If you need a state representing no value (no option being selected), use a DropdownWidget.
13455 *
13456 * This and OO.ui.RadioSelectInputWidget support the same configuration options.
13457 *
13458 * @example
13459 * // Example: A DropdownInputWidget with three options
13460 * var dropdownInput = new OO.ui.DropdownInputWidget( {
13461 * options: [
13462 * { data: 'a', label: 'First' },
13463 * { data: 'b', label: 'Second'},
13464 * { data: 'c', label: 'Third' }
13465 * ]
13466 * } );
13467 * $( 'body' ).append( dropdownInput.$element );
13468 *
13469 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
13470 *
13471 * @class
13472 * @extends OO.ui.InputWidget
13473 *
13474 * @constructor
13475 * @param {Object} [config] Configuration options
13476 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
13477 */
13478 OO.ui.DropdownInputWidget = function OoUiDropdownInputWidget( config ) {
13479 // Configuration initialization
13480 config = config || {};
13481
13482 // Properties (must be done before parent constructor which calls #setDisabled)
13483 this.dropdownWidget = new OO.ui.DropdownWidget();
13484
13485 // Parent constructor
13486 OO.ui.DropdownInputWidget.parent.call( this, config );
13487
13488 // Events
13489 this.dropdownWidget.getMenu().connect( this, { select: 'onMenuSelect' } );
13490
13491 // Initialization
13492 this.setOptions( config.options || [] );
13493 this.$element
13494 .addClass( 'oo-ui-dropdownInputWidget' )
13495 .append( this.dropdownWidget.$element );
13496 };
13497
13498 /* Setup */
13499
13500 OO.inheritClass( OO.ui.DropdownInputWidget, OO.ui.InputWidget );
13501
13502 /* Methods */
13503
13504 /**
13505 * @inheritdoc
13506 * @protected
13507 */
13508 OO.ui.DropdownInputWidget.prototype.getInputElement = function () {
13509 return $( '<input type="hidden">' );
13510 };
13511
13512 /**
13513 * Handles menu select events.
13514 *
13515 * @private
13516 * @param {OO.ui.MenuOptionWidget} item Selected menu item
13517 */
13518 OO.ui.DropdownInputWidget.prototype.onMenuSelect = function ( item ) {
13519 this.setValue( item.getData() );
13520 };
13521
13522 /**
13523 * @inheritdoc
13524 */
13525 OO.ui.DropdownInputWidget.prototype.setValue = function ( value ) {
13526 value = this.cleanUpValue( value );
13527 this.dropdownWidget.getMenu().selectItemByData( value );
13528 OO.ui.DropdownInputWidget.parent.prototype.setValue.call( this, value );
13529 return this;
13530 };
13531
13532 /**
13533 * @inheritdoc
13534 */
13535 OO.ui.DropdownInputWidget.prototype.setDisabled = function ( state ) {
13536 this.dropdownWidget.setDisabled( state );
13537 OO.ui.DropdownInputWidget.parent.prototype.setDisabled.call( this, state );
13538 return this;
13539 };
13540
13541 /**
13542 * Set the options available for this input.
13543 *
13544 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
13545 * @chainable
13546 */
13547 OO.ui.DropdownInputWidget.prototype.setOptions = function ( options ) {
13548 var
13549 value = this.getValue(),
13550 widget = this;
13551
13552 // Rebuild the dropdown menu
13553 this.dropdownWidget.getMenu()
13554 .clearItems()
13555 .addItems( options.map( function ( opt ) {
13556 var optValue = widget.cleanUpValue( opt.data );
13557 return new OO.ui.MenuOptionWidget( {
13558 data: optValue,
13559 label: opt.label !== undefined ? opt.label : optValue
13560 } );
13561 } ) );
13562
13563 // Restore the previous value, or reset to something sensible
13564 if ( this.dropdownWidget.getMenu().getItemFromData( value ) ) {
13565 // Previous value is still available, ensure consistency with the dropdown
13566 this.setValue( value );
13567 } else {
13568 // No longer valid, reset
13569 if ( options.length ) {
13570 this.setValue( options[ 0 ].data );
13571 }
13572 }
13573
13574 return this;
13575 };
13576
13577 /**
13578 * @inheritdoc
13579 */
13580 OO.ui.DropdownInputWidget.prototype.focus = function () {
13581 this.dropdownWidget.getMenu().toggle( true );
13582 return this;
13583 };
13584
13585 /**
13586 * @inheritdoc
13587 */
13588 OO.ui.DropdownInputWidget.prototype.blur = function () {
13589 this.dropdownWidget.getMenu().toggle( false );
13590 return this;
13591 };
13592
13593 /**
13594 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
13595 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
13596 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
13597 * please see the [OOjs UI documentation on MediaWiki][1].
13598 *
13599 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
13600 *
13601 * @example
13602 * // An example of selected, unselected, and disabled radio inputs
13603 * var radio1 = new OO.ui.RadioInputWidget( {
13604 * value: 'a',
13605 * selected: true
13606 * } );
13607 * var radio2 = new OO.ui.RadioInputWidget( {
13608 * value: 'b'
13609 * } );
13610 * var radio3 = new OO.ui.RadioInputWidget( {
13611 * value: 'c',
13612 * disabled: true
13613 * } );
13614 * // Create a fieldset layout with fields for each radio button.
13615 * var fieldset = new OO.ui.FieldsetLayout( {
13616 * label: 'Radio inputs'
13617 * } );
13618 * fieldset.addItems( [
13619 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
13620 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
13621 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
13622 * ] );
13623 * $( 'body' ).append( fieldset.$element );
13624 *
13625 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
13626 *
13627 * @class
13628 * @extends OO.ui.InputWidget
13629 *
13630 * @constructor
13631 * @param {Object} [config] Configuration options
13632 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
13633 */
13634 OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) {
13635 // Configuration initialization
13636 config = config || {};
13637
13638 // Parent constructor
13639 OO.ui.RadioInputWidget.parent.call( this, config );
13640
13641 // Initialization
13642 this.$element.addClass( 'oo-ui-radioInputWidget' );
13643 this.setSelected( config.selected !== undefined ? config.selected : false );
13644 };
13645
13646 /* Setup */
13647
13648 OO.inheritClass( OO.ui.RadioInputWidget, OO.ui.InputWidget );
13649
13650 /* Methods */
13651
13652 /**
13653 * @inheritdoc
13654 * @protected
13655 */
13656 OO.ui.RadioInputWidget.prototype.getInputElement = function () {
13657 return $( '<input type="radio" />' );
13658 };
13659
13660 /**
13661 * @inheritdoc
13662 */
13663 OO.ui.RadioInputWidget.prototype.onEdit = function () {
13664 // RadioInputWidget doesn't track its state.
13665 };
13666
13667 /**
13668 * Set selection state of this radio button.
13669 *
13670 * @param {boolean} state `true` for selected
13671 * @chainable
13672 */
13673 OO.ui.RadioInputWidget.prototype.setSelected = function ( state ) {
13674 // RadioInputWidget doesn't track its state.
13675 this.$input.prop( 'checked', state );
13676 return this;
13677 };
13678
13679 /**
13680 * Check if this radio button is selected.
13681 *
13682 * @return {boolean} Radio is selected
13683 */
13684 OO.ui.RadioInputWidget.prototype.isSelected = function () {
13685 return this.$input.prop( 'checked' );
13686 };
13687
13688 /**
13689 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
13690 * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
13691 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
13692 * more information about input widgets.
13693 *
13694 * This and OO.ui.DropdownInputWidget support the same configuration options.
13695 *
13696 * @example
13697 * // Example: A RadioSelectInputWidget with three options
13698 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
13699 * options: [
13700 * { data: 'a', label: 'First' },
13701 * { data: 'b', label: 'Second'},
13702 * { data: 'c', label: 'Third' }
13703 * ]
13704 * } );
13705 * $( 'body' ).append( radioSelectInput.$element );
13706 *
13707 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
13708 *
13709 * @class
13710 * @extends OO.ui.InputWidget
13711 *
13712 * @constructor
13713 * @param {Object} [config] Configuration options
13714 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
13715 */
13716 OO.ui.RadioSelectInputWidget = function OoUiRadioSelectInputWidget( config ) {
13717 // Configuration initialization
13718 config = config || {};
13719
13720 // Properties (must be done before parent constructor which calls #setDisabled)
13721 this.radioSelectWidget = new OO.ui.RadioSelectWidget();
13722
13723 // Parent constructor
13724 OO.ui.RadioSelectInputWidget.parent.call( this, config );
13725
13726 // Events
13727 this.radioSelectWidget.connect( this, { select: 'onMenuSelect' } );
13728
13729 // Initialization
13730 this.setOptions( config.options || [] );
13731 this.$element
13732 .addClass( 'oo-ui-radioSelectInputWidget' )
13733 .append( this.radioSelectWidget.$element );
13734 };
13735
13736 /* Setup */
13737
13738 OO.inheritClass( OO.ui.RadioSelectInputWidget, OO.ui.InputWidget );
13739
13740 /* Static Properties */
13741
13742 OO.ui.RadioSelectInputWidget.static.supportsSimpleLabel = false;
13743
13744 /* Methods */
13745
13746 /**
13747 * @inheritdoc
13748 * @protected
13749 */
13750 OO.ui.RadioSelectInputWidget.prototype.getInputElement = function () {
13751 return $( '<input type="hidden">' );
13752 };
13753
13754 /**
13755 * Handles menu select events.
13756 *
13757 * @private
13758 * @param {OO.ui.RadioOptionWidget} item Selected menu item
13759 */
13760 OO.ui.RadioSelectInputWidget.prototype.onMenuSelect = function ( item ) {
13761 this.setValue( item.getData() );
13762 };
13763
13764 /**
13765 * @inheritdoc
13766 */
13767 OO.ui.RadioSelectInputWidget.prototype.setValue = function ( value ) {
13768 value = this.cleanUpValue( value );
13769 this.radioSelectWidget.selectItemByData( value );
13770 OO.ui.RadioSelectInputWidget.parent.prototype.setValue.call( this, value );
13771 return this;
13772 };
13773
13774 /**
13775 * @inheritdoc
13776 */
13777 OO.ui.RadioSelectInputWidget.prototype.setDisabled = function ( state ) {
13778 this.radioSelectWidget.setDisabled( state );
13779 OO.ui.RadioSelectInputWidget.parent.prototype.setDisabled.call( this, state );
13780 return this;
13781 };
13782
13783 /**
13784 * Set the options available for this input.
13785 *
13786 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
13787 * @chainable
13788 */
13789 OO.ui.RadioSelectInputWidget.prototype.setOptions = function ( options ) {
13790 var
13791 value = this.getValue(),
13792 widget = this;
13793
13794 // Rebuild the radioSelect menu
13795 this.radioSelectWidget
13796 .clearItems()
13797 .addItems( options.map( function ( opt ) {
13798 var optValue = widget.cleanUpValue( opt.data );
13799 return new OO.ui.RadioOptionWidget( {
13800 data: optValue,
13801 label: opt.label !== undefined ? opt.label : optValue
13802 } );
13803 } ) );
13804
13805 // Restore the previous value, or reset to something sensible
13806 if ( this.radioSelectWidget.getItemFromData( value ) ) {
13807 // Previous value is still available, ensure consistency with the radioSelect
13808 this.setValue( value );
13809 } else {
13810 // No longer valid, reset
13811 if ( options.length ) {
13812 this.setValue( options[ 0 ].data );
13813 }
13814 }
13815
13816 return this;
13817 };
13818
13819 /**
13820 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
13821 * size of the field as well as its presentation. In addition, these widgets can be configured
13822 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
13823 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
13824 * which modifies incoming values rather than validating them.
13825 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
13826 *
13827 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
13828 *
13829 * @example
13830 * // Example of a text input widget
13831 * var textInput = new OO.ui.TextInputWidget( {
13832 * value: 'Text input'
13833 * } )
13834 * $( 'body' ).append( textInput.$element );
13835 *
13836 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
13837 *
13838 * @class
13839 * @extends OO.ui.InputWidget
13840 * @mixins OO.ui.mixin.IconElement
13841 * @mixins OO.ui.mixin.IndicatorElement
13842 * @mixins OO.ui.mixin.PendingElement
13843 * @mixins OO.ui.mixin.LabelElement
13844 *
13845 * @constructor
13846 * @param {Object} [config] Configuration options
13847 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password', 'search',
13848 * 'email' or 'url'. Ignored if `multiline` is true.
13849 * @cfg {string} [placeholder] Placeholder text
13850 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
13851 * instruct the browser to focus this widget.
13852 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
13853 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
13854 * @cfg {boolean} [multiline=false] Allow multiple lines of text
13855 * @cfg {number} [rows] If multiline, number of visible lines in textarea. If used with `autosize`,
13856 * specifies minimum number of rows to display.
13857 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
13858 * Use the #maxRows config to specify a maximum number of displayed rows.
13859 * @cfg {boolean} [maxRows] Maximum number of rows to display when #autosize is set to true.
13860 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
13861 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
13862 * the value or placeholder text: `'before'` or `'after'`
13863 * @cfg {boolean} [required=false] Mark the field as required
13864 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
13865 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
13866 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
13867 * (the value must contain only numbers); when RegExp, a regular expression that must match the
13868 * value for it to be considered valid; when Function, a function receiving the value as parameter
13869 * that must return true, or promise resolving to true, for it to be considered valid.
13870 */
13871 OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
13872 // Configuration initialization
13873 config = $.extend( {
13874 type: 'text',
13875 labelPosition: 'after'
13876 }, config );
13877
13878 // Parent constructor
13879 OO.ui.TextInputWidget.parent.call( this, config );
13880
13881 // Mixin constructors
13882 OO.ui.mixin.IconElement.call( this, config );
13883 OO.ui.mixin.IndicatorElement.call( this, config );
13884 OO.ui.mixin.PendingElement.call( this, config );
13885 OO.ui.mixin.LabelElement.call( this, config );
13886
13887 // Properties
13888 this.readOnly = false;
13889 this.multiline = !!config.multiline;
13890 this.autosize = !!config.autosize;
13891 this.minRows = config.rows !== undefined ? config.rows : '';
13892 this.maxRows = config.maxRows || Math.max( 2 * ( this.minRows || 0 ), 10 );
13893 this.validate = null;
13894
13895 // Clone for resizing
13896 if ( this.autosize ) {
13897 this.$clone = this.$input
13898 .clone()
13899 .insertAfter( this.$input )
13900 .attr( 'aria-hidden', 'true' )
13901 .addClass( 'oo-ui-element-hidden' );
13902 }
13903
13904 this.setValidation( config.validate );
13905 this.setLabelPosition( config.labelPosition );
13906
13907 // Events
13908 this.$input.on( {
13909 keypress: this.onKeyPress.bind( this ),
13910 blur: this.onBlur.bind( this )
13911 } );
13912 this.$input.one( {
13913 focus: this.onElementAttach.bind( this )
13914 } );
13915 this.$icon.on( 'mousedown', this.onIconMouseDown.bind( this ) );
13916 this.$indicator.on( 'mousedown', this.onIndicatorMouseDown.bind( this ) );
13917 this.on( 'labelChange', this.updatePosition.bind( this ) );
13918 this.connect( this, { change: 'onChange' } );
13919
13920 // Initialization
13921 this.$element
13922 .addClass( 'oo-ui-textInputWidget' )
13923 .append( this.$icon, this.$indicator );
13924 this.setReadOnly( !!config.readOnly );
13925 if ( config.placeholder ) {
13926 this.$input.attr( 'placeholder', config.placeholder );
13927 }
13928 if ( config.maxLength !== undefined ) {
13929 this.$input.attr( 'maxlength', config.maxLength );
13930 }
13931 if ( config.autofocus ) {
13932 this.$input.attr( 'autofocus', 'autofocus' );
13933 }
13934 if ( config.required ) {
13935 this.$input.attr( 'required', 'required' );
13936 this.$input.attr( 'aria-required', 'true' );
13937 }
13938 if ( config.autocomplete === false ) {
13939 this.$input.attr( 'autocomplete', 'off' );
13940 }
13941 if ( this.multiline && config.rows ) {
13942 this.$input.attr( 'rows', config.rows );
13943 }
13944 if ( this.label || config.autosize ) {
13945 this.installParentChangeDetector();
13946 }
13947 };
13948
13949 /* Setup */
13950
13951 OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
13952 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IconElement );
13953 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IndicatorElement );
13954 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.PendingElement );
13955 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.LabelElement );
13956
13957 /* Static properties */
13958
13959 OO.ui.TextInputWidget.static.validationPatterns = {
13960 'non-empty': /.+/,
13961 integer: /^\d+$/
13962 };
13963
13964 /* Events */
13965
13966 /**
13967 * An `enter` event is emitted when the user presses 'enter' inside the text box.
13968 *
13969 * Not emitted if the input is multiline.
13970 *
13971 * @event enter
13972 */
13973
13974 /* Methods */
13975
13976 /**
13977 * Handle icon mouse down events.
13978 *
13979 * @private
13980 * @param {jQuery.Event} e Mouse down event
13981 * @fires icon
13982 */
13983 OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
13984 if ( e.which === 1 ) {
13985 this.$input[ 0 ].focus();
13986 return false;
13987 }
13988 };
13989
13990 /**
13991 * Handle indicator mouse down events.
13992 *
13993 * @private
13994 * @param {jQuery.Event} e Mouse down event
13995 * @fires indicator
13996 */
13997 OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
13998 if ( e.which === 1 ) {
13999 this.$input[ 0 ].focus();
14000 return false;
14001 }
14002 };
14003
14004 /**
14005 * Handle key press events.
14006 *
14007 * @private
14008 * @param {jQuery.Event} e Key press event
14009 * @fires enter If enter key is pressed and input is not multiline
14010 */
14011 OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
14012 if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) {
14013 this.emit( 'enter', e );
14014 }
14015 };
14016
14017 /**
14018 * Handle blur events.
14019 *
14020 * @private
14021 * @param {jQuery.Event} e Blur event
14022 */
14023 OO.ui.TextInputWidget.prototype.onBlur = function () {
14024 this.setValidityFlag();
14025 };
14026
14027 /**
14028 * Handle element attach events.
14029 *
14030 * @private
14031 * @param {jQuery.Event} e Element attach event
14032 */
14033 OO.ui.TextInputWidget.prototype.onElementAttach = function () {
14034 // Any previously calculated size is now probably invalid if we reattached elsewhere
14035 this.valCache = null;
14036 this.adjustSize();
14037 this.positionLabel();
14038 };
14039
14040 /**
14041 * Handle change events.
14042 *
14043 * @param {string} value
14044 * @private
14045 */
14046 OO.ui.TextInputWidget.prototype.onChange = function () {
14047 this.setValidityFlag();
14048 this.adjustSize();
14049 };
14050
14051 /**
14052 * Check if the input is {@link #readOnly read-only}.
14053 *
14054 * @return {boolean}
14055 */
14056 OO.ui.TextInputWidget.prototype.isReadOnly = function () {
14057 return this.readOnly;
14058 };
14059
14060 /**
14061 * Set the {@link #readOnly read-only} state of the input.
14062 *
14063 * @param {boolean} state Make input read-only
14064 * @chainable
14065 */
14066 OO.ui.TextInputWidget.prototype.setReadOnly = function ( state ) {
14067 this.readOnly = !!state;
14068 this.$input.prop( 'readOnly', this.readOnly );
14069 return this;
14070 };
14071
14072 /**
14073 * Support function for making #onElementAttach work across browsers.
14074 *
14075 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
14076 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
14077 *
14078 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
14079 * first time that the element gets attached to the documented.
14080 */
14081 OO.ui.TextInputWidget.prototype.installParentChangeDetector = function () {
14082 var mutationObserver, onRemove, topmostNode, fakeParentNode,
14083 MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
14084 widget = this;
14085
14086 if ( MutationObserver ) {
14087 // The new way. If only it wasn't so ugly.
14088
14089 if ( this.$element.closest( 'html' ).length ) {
14090 // Widget is attached already, do nothing. This breaks the functionality of this function when
14091 // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
14092 // would require observation of the whole document, which would hurt performance of other,
14093 // more important code.
14094 return;
14095 }
14096
14097 // Find topmost node in the tree
14098 topmostNode = this.$element[0];
14099 while ( topmostNode.parentNode ) {
14100 topmostNode = topmostNode.parentNode;
14101 }
14102
14103 // We have no way to detect the $element being attached somewhere without observing the entire
14104 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
14105 // parent node of $element, and instead detect when $element is removed from it (and thus
14106 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
14107 // doesn't get attached, we end up back here and create the parent.
14108
14109 mutationObserver = new MutationObserver( function ( mutations ) {
14110 var i, j, removedNodes;
14111 for ( i = 0; i < mutations.length; i++ ) {
14112 removedNodes = mutations[ i ].removedNodes;
14113 for ( j = 0; j < removedNodes.length; j++ ) {
14114 if ( removedNodes[ j ] === topmostNode ) {
14115 setTimeout( onRemove, 0 );
14116 return;
14117 }
14118 }
14119 }
14120 } );
14121
14122 onRemove = function () {
14123 // If the node was attached somewhere else, report it
14124 if ( widget.$element.closest( 'html' ).length ) {
14125 widget.onElementAttach();
14126 }
14127 mutationObserver.disconnect();
14128 widget.installParentChangeDetector();
14129 };
14130
14131 // Create a fake parent and observe it
14132 fakeParentNode = $( '<div>' ).append( this.$element )[0];
14133 mutationObserver.observe( fakeParentNode, { childList: true } );
14134 } else {
14135 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
14136 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
14137 this.$element.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach.bind( this ) );
14138 }
14139 };
14140
14141 /**
14142 * Automatically adjust the size of the text input.
14143 *
14144 * This only affects #multiline inputs that are {@link #autosize autosized}.
14145 *
14146 * @chainable
14147 */
14148 OO.ui.TextInputWidget.prototype.adjustSize = function () {
14149 var scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError, idealHeight;
14150
14151 if ( this.multiline && this.autosize && this.$input.val() !== this.valCache ) {
14152 this.$clone
14153 .val( this.$input.val() )
14154 .attr( 'rows', this.minRows )
14155 // Set inline height property to 0 to measure scroll height
14156 .css( 'height', 0 );
14157
14158 this.$clone.removeClass( 'oo-ui-element-hidden' );
14159
14160 this.valCache = this.$input.val();
14161
14162 scrollHeight = this.$clone[ 0 ].scrollHeight;
14163
14164 // Remove inline height property to measure natural heights
14165 this.$clone.css( 'height', '' );
14166 innerHeight = this.$clone.innerHeight();
14167 outerHeight = this.$clone.outerHeight();
14168
14169 // Measure max rows height
14170 this.$clone
14171 .attr( 'rows', this.maxRows )
14172 .css( 'height', 'auto' )
14173 .val( '' );
14174 maxInnerHeight = this.$clone.innerHeight();
14175
14176 // Difference between reported innerHeight and scrollHeight with no scrollbars present
14177 // Equals 1 on Blink-based browsers and 0 everywhere else
14178 measurementError = maxInnerHeight - this.$clone[ 0 ].scrollHeight;
14179 idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
14180
14181 this.$clone.addClass( 'oo-ui-element-hidden' );
14182
14183 // Only apply inline height when expansion beyond natural height is needed
14184 if ( idealHeight > innerHeight ) {
14185 // Use the difference between the inner and outer height as a buffer
14186 this.$input.css( 'height', idealHeight + ( outerHeight - innerHeight ) );
14187 } else {
14188 this.$input.css( 'height', '' );
14189 }
14190 }
14191 return this;
14192 };
14193
14194 /**
14195 * @inheritdoc
14196 * @protected
14197 */
14198 OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
14199 var type = [ 'text', 'password', 'search', 'email', 'url' ].indexOf( config.type ) !== -1 ?
14200 config.type :
14201 'text';
14202 return config.multiline ? $( '<textarea>' ) : $( '<input type="' + type + '" />' );
14203 };
14204
14205 /**
14206 * Check if the input supports multiple lines.
14207 *
14208 * @return {boolean}
14209 */
14210 OO.ui.TextInputWidget.prototype.isMultiline = function () {
14211 return !!this.multiline;
14212 };
14213
14214 /**
14215 * Check if the input automatically adjusts its size.
14216 *
14217 * @return {boolean}
14218 */
14219 OO.ui.TextInputWidget.prototype.isAutosizing = function () {
14220 return !!this.autosize;
14221 };
14222
14223 /**
14224 * Select the entire text of the input.
14225 *
14226 * @chainable
14227 */
14228 OO.ui.TextInputWidget.prototype.select = function () {
14229 this.$input.select();
14230 return this;
14231 };
14232
14233 /**
14234 * Set the validation pattern.
14235 *
14236 * The validation pattern is either a regular expression, a function, or the symbolic name of a
14237 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
14238 * value must contain only numbers).
14239 *
14240 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
14241 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
14242 */
14243 OO.ui.TextInputWidget.prototype.setValidation = function ( validate ) {
14244 if ( validate instanceof RegExp || validate instanceof Function ) {
14245 this.validate = validate;
14246 } else {
14247 this.validate = this.constructor.static.validationPatterns[ validate ] || /.*/;
14248 }
14249 };
14250
14251 /**
14252 * Sets the 'invalid' flag appropriately.
14253 *
14254 * @param {boolean} [isValid] Optionally override validation result
14255 */
14256 OO.ui.TextInputWidget.prototype.setValidityFlag = function ( isValid ) {
14257 var widget = this,
14258 setFlag = function ( valid ) {
14259 if ( !valid ) {
14260 widget.$input.attr( 'aria-invalid', 'true' );
14261 } else {
14262 widget.$input.removeAttr( 'aria-invalid' );
14263 }
14264 widget.setFlags( { invalid: !valid } );
14265 };
14266
14267 if ( isValid !== undefined ) {
14268 setFlag( isValid );
14269 } else {
14270 this.isValid().done( setFlag );
14271 }
14272 };
14273
14274 /**
14275 * Check if a value is valid.
14276 *
14277 * This method returns a promise that resolves with a boolean `true` if the current value is
14278 * considered valid according to the supplied {@link #validate validation pattern}.
14279 *
14280 * @return {jQuery.Promise} A promise that resolves to a boolean `true` if the value is valid.
14281 */
14282 OO.ui.TextInputWidget.prototype.isValid = function () {
14283 if ( this.validate instanceof Function ) {
14284 var result = this.validate( this.getValue() );
14285 if ( $.isFunction( result.promise ) ) {
14286 return result.promise();
14287 } else {
14288 return $.Deferred().resolve( !!result ).promise();
14289 }
14290 } else {
14291 return $.Deferred().resolve( !!this.getValue().match( this.validate ) ).promise();
14292 }
14293 };
14294
14295 /**
14296 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
14297 *
14298 * @param {string} labelPosition Label position, 'before' or 'after'
14299 * @chainable
14300 */
14301 OO.ui.TextInputWidget.prototype.setLabelPosition = function ( labelPosition ) {
14302 this.labelPosition = labelPosition;
14303 this.updatePosition();
14304 return this;
14305 };
14306
14307 /**
14308 * Deprecated alias of #setLabelPosition
14309 *
14310 * @deprecated Use setLabelPosition instead.
14311 */
14312 OO.ui.TextInputWidget.prototype.setPosition =
14313 OO.ui.TextInputWidget.prototype.setLabelPosition;
14314
14315 /**
14316 * Update the position of the inline label.
14317 *
14318 * This method is called by #setLabelPosition, and can also be called on its own if
14319 * something causes the label to be mispositioned.
14320 *
14321 *
14322 * @chainable
14323 */
14324 OO.ui.TextInputWidget.prototype.updatePosition = function () {
14325 var after = this.labelPosition === 'after';
14326
14327 this.$element
14328 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label && after )
14329 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label && !after );
14330
14331 if ( this.label ) {
14332 this.positionLabel();
14333 }
14334
14335 return this;
14336 };
14337
14338 /**
14339 * Position the label by setting the correct padding on the input.
14340 *
14341 * @private
14342 * @chainable
14343 */
14344 OO.ui.TextInputWidget.prototype.positionLabel = function () {
14345 // Clear old values
14346 this.$input
14347 // Clear old values if present
14348 .css( {
14349 'padding-right': '',
14350 'padding-left': ''
14351 } );
14352
14353 if ( this.label ) {
14354 this.$element.append( this.$label );
14355 } else {
14356 this.$label.detach();
14357 return;
14358 }
14359
14360 var after = this.labelPosition === 'after',
14361 rtl = this.$element.css( 'direction' ) === 'rtl',
14362 property = after === rtl ? 'padding-left' : 'padding-right';
14363
14364 this.$input.css( property, this.$label.outerWidth( true ) );
14365
14366 return this;
14367 };
14368
14369 /**
14370 * ComboBoxWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
14371 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
14372 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
14373 *
14374 * - by typing a value in the text input field. If the value exactly matches the value of a menu
14375 * option, that option will appear to be selected.
14376 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
14377 * input field.
14378 *
14379 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
14380 *
14381 * @example
14382 * // Example: A ComboBoxWidget.
14383 * var comboBox = new OO.ui.ComboBoxWidget( {
14384 * label: 'ComboBoxWidget',
14385 * input: { value: 'Option One' },
14386 * menu: {
14387 * items: [
14388 * new OO.ui.MenuOptionWidget( {
14389 * data: 'Option 1',
14390 * label: 'Option One'
14391 * } ),
14392 * new OO.ui.MenuOptionWidget( {
14393 * data: 'Option 2',
14394 * label: 'Option Two'
14395 * } ),
14396 * new OO.ui.MenuOptionWidget( {
14397 * data: 'Option 3',
14398 * label: 'Option Three'
14399 * } ),
14400 * new OO.ui.MenuOptionWidget( {
14401 * data: 'Option 4',
14402 * label: 'Option Four'
14403 * } ),
14404 * new OO.ui.MenuOptionWidget( {
14405 * data: 'Option 5',
14406 * label: 'Option Five'
14407 * } )
14408 * ]
14409 * }
14410 * } );
14411 * $( 'body' ).append( comboBox.$element );
14412 *
14413 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
14414 *
14415 * @class
14416 * @extends OO.ui.Widget
14417 * @mixins OO.ui.mixin.TabIndexedElement
14418 *
14419 * @constructor
14420 * @param {Object} [config] Configuration options
14421 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}.
14422 * @cfg {Object} [input] Configuration options to pass to the {@link OO.ui.TextInputWidget text input widget}.
14423 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
14424 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
14425 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
14426 */
14427 OO.ui.ComboBoxWidget = function OoUiComboBoxWidget( config ) {
14428 // Configuration initialization
14429 config = config || {};
14430
14431 // Parent constructor
14432 OO.ui.ComboBoxWidget.parent.call( this, config );
14433
14434 // Properties (must be set before TabIndexedElement constructor call)
14435 this.$indicator = this.$( '<span>' );
14436
14437 // Mixin constructors
14438 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$indicator } ) );
14439
14440 // Properties
14441 this.$overlay = config.$overlay || this.$element;
14442 this.input = new OO.ui.TextInputWidget( $.extend(
14443 {
14444 indicator: 'down',
14445 $indicator: this.$indicator,
14446 disabled: this.isDisabled()
14447 },
14448 config.input
14449 ) );
14450 this.input.$input.eq( 0 ).attr( {
14451 role: 'combobox',
14452 'aria-autocomplete': 'list'
14453 } );
14454 this.menu = new OO.ui.TextInputMenuSelectWidget( this.input, $.extend(
14455 {
14456 widget: this,
14457 input: this.input,
14458 disabled: this.isDisabled()
14459 },
14460 config.menu
14461 ) );
14462
14463 // Events
14464 this.$indicator.on( {
14465 click: this.onClick.bind( this ),
14466 keypress: this.onKeyPress.bind( this )
14467 } );
14468 this.input.connect( this, {
14469 change: 'onInputChange',
14470 enter: 'onInputEnter'
14471 } );
14472 this.menu.connect( this, {
14473 choose: 'onMenuChoose',
14474 add: 'onMenuItemsChange',
14475 remove: 'onMenuItemsChange'
14476 } );
14477
14478 // Initialization
14479 this.$element.addClass( 'oo-ui-comboBoxWidget' ).append( this.input.$element );
14480 this.$overlay.append( this.menu.$element );
14481 this.onMenuItemsChange();
14482 };
14483
14484 /* Setup */
14485
14486 OO.inheritClass( OO.ui.ComboBoxWidget, OO.ui.Widget );
14487 OO.mixinClass( OO.ui.ComboBoxWidget, OO.ui.mixin.TabIndexedElement );
14488
14489 /* Methods */
14490
14491 /**
14492 * Get the combobox's menu.
14493 * @return {OO.ui.TextInputMenuSelectWidget} Menu widget
14494 */
14495 OO.ui.ComboBoxWidget.prototype.getMenu = function () {
14496 return this.menu;
14497 };
14498
14499 /**
14500 * Get the combobox's text input widget.
14501 * @return {OO.ui.TextInputWidget} Text input widget
14502 */
14503 OO.ui.ComboBoxWidget.prototype.getInput = function () {
14504 return this.input;
14505 };
14506
14507 /**
14508 * Handle input change events.
14509 *
14510 * @private
14511 * @param {string} value New value
14512 */
14513 OO.ui.ComboBoxWidget.prototype.onInputChange = function ( value ) {
14514 var match = this.menu.getItemFromData( value );
14515
14516 this.menu.selectItem( match );
14517 if ( this.menu.getHighlightedItem() ) {
14518 this.menu.highlightItem( match );
14519 }
14520
14521 if ( !this.isDisabled() ) {
14522 this.menu.toggle( true );
14523 }
14524 };
14525
14526 /**
14527 * Handle mouse click events.
14528 *
14529 *
14530 * @private
14531 * @param {jQuery.Event} e Mouse click event
14532 */
14533 OO.ui.ComboBoxWidget.prototype.onClick = function ( e ) {
14534 if ( !this.isDisabled() && e.which === 1 ) {
14535 this.menu.toggle();
14536 this.input.$input[ 0 ].focus();
14537 }
14538 return false;
14539 };
14540
14541 /**
14542 * Handle key press events.
14543 *
14544 *
14545 * @private
14546 * @param {jQuery.Event} e Key press event
14547 */
14548 OO.ui.ComboBoxWidget.prototype.onKeyPress = function ( e ) {
14549 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
14550 this.menu.toggle();
14551 this.input.$input[ 0 ].focus();
14552 return false;
14553 }
14554 };
14555
14556 /**
14557 * Handle input enter events.
14558 *
14559 * @private
14560 */
14561 OO.ui.ComboBoxWidget.prototype.onInputEnter = function () {
14562 if ( !this.isDisabled() ) {
14563 this.menu.toggle( false );
14564 }
14565 };
14566
14567 /**
14568 * Handle menu choose events.
14569 *
14570 * @private
14571 * @param {OO.ui.OptionWidget} item Chosen item
14572 */
14573 OO.ui.ComboBoxWidget.prototype.onMenuChoose = function ( item ) {
14574 this.input.setValue( item.getData() );
14575 };
14576
14577 /**
14578 * Handle menu item change events.
14579 *
14580 * @private
14581 */
14582 OO.ui.ComboBoxWidget.prototype.onMenuItemsChange = function () {
14583 var match = this.menu.getItemFromData( this.input.getValue() );
14584 this.menu.selectItem( match );
14585 if ( this.menu.getHighlightedItem() ) {
14586 this.menu.highlightItem( match );
14587 }
14588 this.$element.toggleClass( 'oo-ui-comboBoxWidget-empty', this.menu.isEmpty() );
14589 };
14590
14591 /**
14592 * @inheritdoc
14593 */
14594 OO.ui.ComboBoxWidget.prototype.setDisabled = function ( disabled ) {
14595 // Parent method
14596 OO.ui.ComboBoxWidget.parent.prototype.setDisabled.call( this, disabled );
14597
14598 if ( this.input ) {
14599 this.input.setDisabled( this.isDisabled() );
14600 }
14601 if ( this.menu ) {
14602 this.menu.setDisabled( this.isDisabled() );
14603 }
14604
14605 return this;
14606 };
14607
14608 /**
14609 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
14610 * be configured with a `label` option that is set to a string, a label node, or a function:
14611 *
14612 * - String: a plaintext string
14613 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
14614 * label that includes a link or special styling, such as a gray color or additional graphical elements.
14615 * - Function: a function that will produce a string in the future. Functions are used
14616 * in cases where the value of the label is not currently defined.
14617 *
14618 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
14619 * will come into focus when the label is clicked.
14620 *
14621 * @example
14622 * // Examples of LabelWidgets
14623 * var label1 = new OO.ui.LabelWidget( {
14624 * label: 'plaintext label'
14625 * } );
14626 * var label2 = new OO.ui.LabelWidget( {
14627 * label: $( '<a href="default.html">jQuery label</a>' )
14628 * } );
14629 * // Create a fieldset layout with fields for each example
14630 * var fieldset = new OO.ui.FieldsetLayout();
14631 * fieldset.addItems( [
14632 * new OO.ui.FieldLayout( label1 ),
14633 * new OO.ui.FieldLayout( label2 )
14634 * ] );
14635 * $( 'body' ).append( fieldset.$element );
14636 *
14637 *
14638 * @class
14639 * @extends OO.ui.Widget
14640 * @mixins OO.ui.mixin.LabelElement
14641 *
14642 * @constructor
14643 * @param {Object} [config] Configuration options
14644 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
14645 * Clicking the label will focus the specified input field.
14646 */
14647 OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
14648 // Configuration initialization
14649 config = config || {};
14650
14651 // Parent constructor
14652 OO.ui.LabelWidget.parent.call( this, config );
14653
14654 // Mixin constructors
14655 OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, { $label: this.$element } ) );
14656 OO.ui.mixin.TitledElement.call( this, config );
14657
14658 // Properties
14659 this.input = config.input;
14660
14661 // Events
14662 if ( this.input instanceof OO.ui.InputWidget ) {
14663 this.$element.on( 'click', this.onClick.bind( this ) );
14664 }
14665
14666 // Initialization
14667 this.$element.addClass( 'oo-ui-labelWidget' );
14668 };
14669
14670 /* Setup */
14671
14672 OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
14673 OO.mixinClass( OO.ui.LabelWidget, OO.ui.mixin.LabelElement );
14674 OO.mixinClass( OO.ui.LabelWidget, OO.ui.mixin.TitledElement );
14675
14676 /* Static Properties */
14677
14678 OO.ui.LabelWidget.static.tagName = 'span';
14679
14680 /* Methods */
14681
14682 /**
14683 * Handles label mouse click events.
14684 *
14685 * @private
14686 * @param {jQuery.Event} e Mouse click event
14687 */
14688 OO.ui.LabelWidget.prototype.onClick = function () {
14689 this.input.simulateLabelClick();
14690 return false;
14691 };
14692
14693 /**
14694 * OptionWidgets are special elements that can be selected and configured with data. The
14695 * data is often unique for each option, but it does not have to be. OptionWidgets are used
14696 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
14697 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
14698 *
14699 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
14700 *
14701 * @class
14702 * @extends OO.ui.Widget
14703 * @mixins OO.ui.mixin.LabelElement
14704 * @mixins OO.ui.mixin.FlaggedElement
14705 *
14706 * @constructor
14707 * @param {Object} [config] Configuration options
14708 */
14709 OO.ui.OptionWidget = function OoUiOptionWidget( config ) {
14710 // Configuration initialization
14711 config = config || {};
14712
14713 // Parent constructor
14714 OO.ui.OptionWidget.parent.call( this, config );
14715
14716 // Mixin constructors
14717 OO.ui.mixin.ItemWidget.call( this );
14718 OO.ui.mixin.LabelElement.call( this, config );
14719 OO.ui.mixin.FlaggedElement.call( this, config );
14720
14721 // Properties
14722 this.selected = false;
14723 this.highlighted = false;
14724 this.pressed = false;
14725
14726 // Initialization
14727 this.$element
14728 .data( 'oo-ui-optionWidget', this )
14729 .attr( 'role', 'option' )
14730 .attr( 'aria-selected', 'false' )
14731 .addClass( 'oo-ui-optionWidget' )
14732 .append( this.$label );
14733 };
14734
14735 /* Setup */
14736
14737 OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
14738 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.ItemWidget );
14739 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.LabelElement );
14740 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.FlaggedElement );
14741
14742 /* Static Properties */
14743
14744 OO.ui.OptionWidget.static.selectable = true;
14745
14746 OO.ui.OptionWidget.static.highlightable = true;
14747
14748 OO.ui.OptionWidget.static.pressable = true;
14749
14750 OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
14751
14752 /* Methods */
14753
14754 /**
14755 * Check if the option can be selected.
14756 *
14757 * @return {boolean} Item is selectable
14758 */
14759 OO.ui.OptionWidget.prototype.isSelectable = function () {
14760 return this.constructor.static.selectable && !this.isDisabled();
14761 };
14762
14763 /**
14764 * Check if the option can be highlighted. A highlight indicates that the option
14765 * may be selected when a user presses enter or clicks. Disabled items cannot
14766 * be highlighted.
14767 *
14768 * @return {boolean} Item is highlightable
14769 */
14770 OO.ui.OptionWidget.prototype.isHighlightable = function () {
14771 return this.constructor.static.highlightable && !this.isDisabled();
14772 };
14773
14774 /**
14775 * Check if the option can be pressed. The pressed state occurs when a user mouses
14776 * down on an item, but has not yet let go of the mouse.
14777 *
14778 * @return {boolean} Item is pressable
14779 */
14780 OO.ui.OptionWidget.prototype.isPressable = function () {
14781 return this.constructor.static.pressable && !this.isDisabled();
14782 };
14783
14784 /**
14785 * Check if the option is selected.
14786 *
14787 * @return {boolean} Item is selected
14788 */
14789 OO.ui.OptionWidget.prototype.isSelected = function () {
14790 return this.selected;
14791 };
14792
14793 /**
14794 * Check if the option is highlighted. A highlight indicates that the
14795 * item may be selected when a user presses enter or clicks.
14796 *
14797 * @return {boolean} Item is highlighted
14798 */
14799 OO.ui.OptionWidget.prototype.isHighlighted = function () {
14800 return this.highlighted;
14801 };
14802
14803 /**
14804 * Check if the option is pressed. The pressed state occurs when a user mouses
14805 * down on an item, but has not yet let go of the mouse. The item may appear
14806 * selected, but it will not be selected until the user releases the mouse.
14807 *
14808 * @return {boolean} Item is pressed
14809 */
14810 OO.ui.OptionWidget.prototype.isPressed = function () {
14811 return this.pressed;
14812 };
14813
14814 /**
14815 * Set the option’s selected state. In general, all modifications to the selection
14816 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
14817 * method instead of this method.
14818 *
14819 * @param {boolean} [state=false] Select option
14820 * @chainable
14821 */
14822 OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
14823 if ( this.constructor.static.selectable ) {
14824 this.selected = !!state;
14825 this.$element
14826 .toggleClass( 'oo-ui-optionWidget-selected', state )
14827 .attr( 'aria-selected', state.toString() );
14828 if ( state && this.constructor.static.scrollIntoViewOnSelect ) {
14829 this.scrollElementIntoView();
14830 }
14831 this.updateThemeClasses();
14832 }
14833 return this;
14834 };
14835
14836 /**
14837 * Set the option’s highlighted state. In general, all programmatic
14838 * modifications to the highlight should be handled by the
14839 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
14840 * method instead of this method.
14841 *
14842 * @param {boolean} [state=false] Highlight option
14843 * @chainable
14844 */
14845 OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
14846 if ( this.constructor.static.highlightable ) {
14847 this.highlighted = !!state;
14848 this.$element.toggleClass( 'oo-ui-optionWidget-highlighted', state );
14849 this.updateThemeClasses();
14850 }
14851 return this;
14852 };
14853
14854 /**
14855 * Set the option’s pressed state. In general, all
14856 * programmatic modifications to the pressed state should be handled by the
14857 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
14858 * method instead of this method.
14859 *
14860 * @param {boolean} [state=false] Press option
14861 * @chainable
14862 */
14863 OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
14864 if ( this.constructor.static.pressable ) {
14865 this.pressed = !!state;
14866 this.$element.toggleClass( 'oo-ui-optionWidget-pressed', state );
14867 this.updateThemeClasses();
14868 }
14869 return this;
14870 };
14871
14872 /**
14873 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
14874 * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
14875 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
14876 * options. For more information about options and selects, please see the
14877 * [OOjs UI documentation on MediaWiki][1].
14878 *
14879 * @example
14880 * // Decorated options in a select widget
14881 * var select = new OO.ui.SelectWidget( {
14882 * items: [
14883 * new OO.ui.DecoratedOptionWidget( {
14884 * data: 'a',
14885 * label: 'Option with icon',
14886 * icon: 'help'
14887 * } ),
14888 * new OO.ui.DecoratedOptionWidget( {
14889 * data: 'b',
14890 * label: 'Option with indicator',
14891 * indicator: 'next'
14892 * } )
14893 * ]
14894 * } );
14895 * $( 'body' ).append( select.$element );
14896 *
14897 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
14898 *
14899 * @class
14900 * @extends OO.ui.OptionWidget
14901 * @mixins OO.ui.mixin.IconElement
14902 * @mixins OO.ui.mixin.IndicatorElement
14903 *
14904 * @constructor
14905 * @param {Object} [config] Configuration options
14906 */
14907 OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( config ) {
14908 // Parent constructor
14909 OO.ui.DecoratedOptionWidget.parent.call( this, config );
14910
14911 // Mixin constructors
14912 OO.ui.mixin.IconElement.call( this, config );
14913 OO.ui.mixin.IndicatorElement.call( this, config );
14914
14915 // Initialization
14916 this.$element
14917 .addClass( 'oo-ui-decoratedOptionWidget' )
14918 .prepend( this.$icon )
14919 .append( this.$indicator );
14920 };
14921
14922 /* Setup */
14923
14924 OO.inheritClass( OO.ui.DecoratedOptionWidget, OO.ui.OptionWidget );
14925 OO.mixinClass( OO.ui.DecoratedOptionWidget, OO.ui.mixin.IconElement );
14926 OO.mixinClass( OO.ui.DecoratedOptionWidget, OO.ui.mixin.IndicatorElement );
14927
14928 /**
14929 * ButtonOptionWidget is a special type of {@link OO.ui.mixin.ButtonElement button element} that
14930 * can be selected and configured with data. The class is
14931 * used with OO.ui.ButtonSelectWidget to create a selection of button options. Please see the
14932 * [OOjs UI documentation on MediaWiki] [1] for more information.
14933 *
14934 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_options
14935 *
14936 * @class
14937 * @extends OO.ui.DecoratedOptionWidget
14938 * @mixins OO.ui.mixin.ButtonElement
14939 * @mixins OO.ui.mixin.TabIndexedElement
14940 *
14941 * @constructor
14942 * @param {Object} [config] Configuration options
14943 */
14944 OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( config ) {
14945 // Configuration initialization
14946 config = $.extend( { tabIndex: -1 }, config );
14947
14948 // Parent constructor
14949 OO.ui.ButtonOptionWidget.parent.call( this, config );
14950
14951 // Mixin constructors
14952 OO.ui.mixin.ButtonElement.call( this, config );
14953 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
14954
14955 // Initialization
14956 this.$element.addClass( 'oo-ui-buttonOptionWidget' );
14957 this.$button.append( this.$element.contents() );
14958 this.$element.append( this.$button );
14959 };
14960
14961 /* Setup */
14962
14963 OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.DecoratedOptionWidget );
14964 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.mixin.ButtonElement );
14965 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.mixin.TabIndexedElement );
14966
14967 /* Static Properties */
14968
14969 // Allow button mouse down events to pass through so they can be handled by the parent select widget
14970 OO.ui.ButtonOptionWidget.static.cancelButtonMouseDownEvents = false;
14971
14972 OO.ui.ButtonOptionWidget.static.highlightable = false;
14973
14974 /* Methods */
14975
14976 /**
14977 * @inheritdoc
14978 */
14979 OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) {
14980 OO.ui.ButtonOptionWidget.parent.prototype.setSelected.call( this, state );
14981
14982 if ( this.constructor.static.selectable ) {
14983 this.setActive( state );
14984 }
14985
14986 return this;
14987 };
14988
14989 /**
14990 * RadioOptionWidget is an option widget that looks like a radio button.
14991 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
14992 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
14993 *
14994 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
14995 *
14996 * @class
14997 * @extends OO.ui.OptionWidget
14998 *
14999 * @constructor
15000 * @param {Object} [config] Configuration options
15001 */
15002 OO.ui.RadioOptionWidget = function OoUiRadioOptionWidget( config ) {
15003 // Configuration initialization
15004 config = config || {};
15005
15006 // Properties (must be done before parent constructor which calls #setDisabled)
15007 this.radio = new OO.ui.RadioInputWidget( { value: config.data, tabIndex: -1 } );
15008
15009 // Parent constructor
15010 OO.ui.RadioOptionWidget.parent.call( this, config );
15011
15012 // Events
15013 this.radio.$input.on( 'focus', this.onInputFocus.bind( this ) );
15014
15015 // Initialization
15016 this.$element
15017 .addClass( 'oo-ui-radioOptionWidget' )
15018 .prepend( this.radio.$element );
15019 };
15020
15021 /* Setup */
15022
15023 OO.inheritClass( OO.ui.RadioOptionWidget, OO.ui.OptionWidget );
15024
15025 /* Static Properties */
15026
15027 OO.ui.RadioOptionWidget.static.highlightable = false;
15028
15029 OO.ui.RadioOptionWidget.static.scrollIntoViewOnSelect = true;
15030
15031 OO.ui.RadioOptionWidget.static.pressable = false;
15032
15033 OO.ui.RadioOptionWidget.static.tagName = 'label';
15034
15035 /* Methods */
15036
15037 /**
15038 * @param {jQuery.Event} e Focus event
15039 * @private
15040 */
15041 OO.ui.RadioOptionWidget.prototype.onInputFocus = function () {
15042 this.radio.$input.blur();
15043 this.$element.parent().focus();
15044 };
15045
15046 /**
15047 * @inheritdoc
15048 */
15049 OO.ui.RadioOptionWidget.prototype.setSelected = function ( state ) {
15050 OO.ui.RadioOptionWidget.parent.prototype.setSelected.call( this, state );
15051
15052 this.radio.setSelected( state );
15053
15054 return this;
15055 };
15056
15057 /**
15058 * @inheritdoc
15059 */
15060 OO.ui.RadioOptionWidget.prototype.setDisabled = function ( disabled ) {
15061 OO.ui.RadioOptionWidget.parent.prototype.setDisabled.call( this, disabled );
15062
15063 this.radio.setDisabled( this.isDisabled() );
15064
15065 return this;
15066 };
15067
15068 /**
15069 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
15070 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
15071 * the [OOjs UI documentation on MediaWiki] [1] for more information.
15072 *
15073 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
15074 *
15075 * @class
15076 * @extends OO.ui.DecoratedOptionWidget
15077 *
15078 * @constructor
15079 * @param {Object} [config] Configuration options
15080 */
15081 OO.ui.MenuOptionWidget = function OoUiMenuOptionWidget( config ) {
15082 // Configuration initialization
15083 config = $.extend( { icon: 'check' }, config );
15084
15085 // Parent constructor
15086 OO.ui.MenuOptionWidget.parent.call( this, config );
15087
15088 // Initialization
15089 this.$element
15090 .attr( 'role', 'menuitem' )
15091 .addClass( 'oo-ui-menuOptionWidget' );
15092 };
15093
15094 /* Setup */
15095
15096 OO.inheritClass( OO.ui.MenuOptionWidget, OO.ui.DecoratedOptionWidget );
15097
15098 /* Static Properties */
15099
15100 OO.ui.MenuOptionWidget.static.scrollIntoViewOnSelect = true;
15101
15102 /**
15103 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
15104 * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
15105 *
15106 * @example
15107 * var myDropdown = new OO.ui.DropdownWidget( {
15108 * menu: {
15109 * items: [
15110 * new OO.ui.MenuSectionOptionWidget( {
15111 * label: 'Dogs'
15112 * } ),
15113 * new OO.ui.MenuOptionWidget( {
15114 * data: 'corgi',
15115 * label: 'Welsh Corgi'
15116 * } ),
15117 * new OO.ui.MenuOptionWidget( {
15118 * data: 'poodle',
15119 * label: 'Standard Poodle'
15120 * } ),
15121 * new OO.ui.MenuSectionOptionWidget( {
15122 * label: 'Cats'
15123 * } ),
15124 * new OO.ui.MenuOptionWidget( {
15125 * data: 'lion',
15126 * label: 'Lion'
15127 * } )
15128 * ]
15129 * }
15130 * } );
15131 * $( 'body' ).append( myDropdown.$element );
15132 *
15133 *
15134 * @class
15135 * @extends OO.ui.DecoratedOptionWidget
15136 *
15137 * @constructor
15138 * @param {Object} [config] Configuration options
15139 */
15140 OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( config ) {
15141 // Parent constructor
15142 OO.ui.MenuSectionOptionWidget.parent.call( this, config );
15143
15144 // Initialization
15145 this.$element.addClass( 'oo-ui-menuSectionOptionWidget' );
15146 };
15147
15148 /* Setup */
15149
15150 OO.inheritClass( OO.ui.MenuSectionOptionWidget, OO.ui.DecoratedOptionWidget );
15151
15152 /* Static Properties */
15153
15154 OO.ui.MenuSectionOptionWidget.static.selectable = false;
15155
15156 OO.ui.MenuSectionOptionWidget.static.highlightable = false;
15157
15158 /**
15159 * OutlineOptionWidget is an item in an {@link OO.ui.OutlineSelectWidget OutlineSelectWidget}.
15160 *
15161 * Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}, which contain
15162 * {@link OO.ui.PageLayout page layouts}. See {@link OO.ui.BookletLayout BookletLayout}
15163 * for an example.
15164 *
15165 * @class
15166 * @extends OO.ui.DecoratedOptionWidget
15167 *
15168 * @constructor
15169 * @param {Object} [config] Configuration options
15170 * @cfg {number} [level] Indentation level
15171 * @cfg {boolean} [movable] Allow modification from {@link OO.ui.OutlineControlsWidget outline controls}.
15172 */
15173 OO.ui.OutlineOptionWidget = function OoUiOutlineOptionWidget( config ) {
15174 // Configuration initialization
15175 config = config || {};
15176
15177 // Parent constructor
15178 OO.ui.OutlineOptionWidget.parent.call( this, config );
15179
15180 // Properties
15181 this.level = 0;
15182 this.movable = !!config.movable;
15183 this.removable = !!config.removable;
15184
15185 // Initialization
15186 this.$element.addClass( 'oo-ui-outlineOptionWidget' );
15187 this.setLevel( config.level );
15188 };
15189
15190 /* Setup */
15191
15192 OO.inheritClass( OO.ui.OutlineOptionWidget, OO.ui.DecoratedOptionWidget );
15193
15194 /* Static Properties */
15195
15196 OO.ui.OutlineOptionWidget.static.highlightable = false;
15197
15198 OO.ui.OutlineOptionWidget.static.scrollIntoViewOnSelect = true;
15199
15200 OO.ui.OutlineOptionWidget.static.levelClass = 'oo-ui-outlineOptionWidget-level-';
15201
15202 OO.ui.OutlineOptionWidget.static.levels = 3;
15203
15204 /* Methods */
15205
15206 /**
15207 * Check if item is movable.
15208 *
15209 * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
15210 *
15211 * @return {boolean} Item is movable
15212 */
15213 OO.ui.OutlineOptionWidget.prototype.isMovable = function () {
15214 return this.movable;
15215 };
15216
15217 /**
15218 * Check if item is removable.
15219 *
15220 * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
15221 *
15222 * @return {boolean} Item is removable
15223 */
15224 OO.ui.OutlineOptionWidget.prototype.isRemovable = function () {
15225 return this.removable;
15226 };
15227
15228 /**
15229 * Get indentation level.
15230 *
15231 * @return {number} Indentation level
15232 */
15233 OO.ui.OutlineOptionWidget.prototype.getLevel = function () {
15234 return this.level;
15235 };
15236
15237 /**
15238 * Set movability.
15239 *
15240 * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
15241 *
15242 * @param {boolean} movable Item is movable
15243 * @chainable
15244 */
15245 OO.ui.OutlineOptionWidget.prototype.setMovable = function ( movable ) {
15246 this.movable = !!movable;
15247 this.updateThemeClasses();
15248 return this;
15249 };
15250
15251 /**
15252 * Set removability.
15253 *
15254 * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
15255 *
15256 * @param {boolean} movable Item is removable
15257 * @chainable
15258 */
15259 OO.ui.OutlineOptionWidget.prototype.setRemovable = function ( removable ) {
15260 this.removable = !!removable;
15261 this.updateThemeClasses();
15262 return this;
15263 };
15264
15265 /**
15266 * Set indentation level.
15267 *
15268 * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
15269 * @chainable
15270 */
15271 OO.ui.OutlineOptionWidget.prototype.setLevel = function ( level ) {
15272 var levels = this.constructor.static.levels,
15273 levelClass = this.constructor.static.levelClass,
15274 i = levels;
15275
15276 this.level = level ? Math.max( 0, Math.min( levels - 1, level ) ) : 0;
15277 while ( i-- ) {
15278 if ( this.level === i ) {
15279 this.$element.addClass( levelClass + i );
15280 } else {
15281 this.$element.removeClass( levelClass + i );
15282 }
15283 }
15284 this.updateThemeClasses();
15285
15286 return this;
15287 };
15288
15289 /**
15290 * TabOptionWidget is an item in a {@link OO.ui.TabSelectWidget TabSelectWidget}.
15291 *
15292 * Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}, which contain
15293 * {@link OO.ui.CardLayout card layouts}. See {@link OO.ui.IndexLayout IndexLayout}
15294 * for an example.
15295 *
15296 * @class
15297 * @extends OO.ui.OptionWidget
15298 *
15299 * @constructor
15300 * @param {Object} [config] Configuration options
15301 */
15302 OO.ui.TabOptionWidget = function OoUiTabOptionWidget( config ) {
15303 // Configuration initialization
15304 config = config || {};
15305
15306 // Parent constructor
15307 OO.ui.TabOptionWidget.parent.call( this, config );
15308
15309 // Initialization
15310 this.$element.addClass( 'oo-ui-tabOptionWidget' );
15311 };
15312
15313 /* Setup */
15314
15315 OO.inheritClass( OO.ui.TabOptionWidget, OO.ui.OptionWidget );
15316
15317 /* Static Properties */
15318
15319 OO.ui.TabOptionWidget.static.highlightable = false;
15320
15321 /**
15322 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
15323 * By default, each popup has an anchor that points toward its origin.
15324 * Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples.
15325 *
15326 * @example
15327 * // A popup widget.
15328 * var popup = new OO.ui.PopupWidget( {
15329 * $content: $( '<p>Hi there!</p>' ),
15330 * padded: true,
15331 * width: 300
15332 * } );
15333 *
15334 * $( 'body' ).append( popup.$element );
15335 * // To display the popup, toggle the visibility to 'true'.
15336 * popup.toggle( true );
15337 *
15338 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups
15339 *
15340 * @class
15341 * @extends OO.ui.Widget
15342 * @mixins OO.ui.mixin.LabelElement
15343 *
15344 * @constructor
15345 * @param {Object} [config] Configuration options
15346 * @cfg {number} [width=320] Width of popup in pixels
15347 * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
15348 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
15349 * @cfg {string} [align='center'] Alignment of the popup: `center`, `force-left`, `force-right`, `backwards` or `forwards`.
15350 * If the popup is forced-left the popup body is leaning towards the left. For force-right alignment, the body of the
15351 * popup is leaning towards the right of the screen.
15352 * Using 'backwards' is a logical direction which will result in the popup leaning towards the beginning of the sentence
15353 * in the given language, which means it will flip to the correct positioning in right-to-left languages.
15354 * Using 'forward' will also result in a logical alignment where the body of the popup leans towards the end of the
15355 * sentence in the given language.
15356 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
15357 * See the [OOjs UI docs on MediaWiki][3] for an example.
15358 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
15359 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
15360 * @cfg {jQuery} [$content] Content to append to the popup's body
15361 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
15362 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
15363 * This config option is only relevant if #autoClose is set to `true`. See the [OOjs UI docs on MediaWiki][2]
15364 * for an example.
15365 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#autocloseExample
15366 * @cfg {boolean} [head] Show a popup header that contains a #label (if specified) and close
15367 * button.
15368 * @cfg {boolean} [padded] Add padding to the popup's body
15369 */
15370 OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
15371 // Configuration initialization
15372 config = config || {};
15373
15374 // Parent constructor
15375 OO.ui.PopupWidget.parent.call( this, config );
15376
15377 // Properties (must be set before ClippableElement constructor call)
15378 this.$body = $( '<div>' );
15379
15380 // Mixin constructors
15381 OO.ui.mixin.LabelElement.call( this, config );
15382 OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$body } ) );
15383
15384 // Properties
15385 this.$popup = $( '<div>' );
15386 this.$head = $( '<div>' );
15387 this.$anchor = $( '<div>' );
15388 // If undefined, will be computed lazily in updateDimensions()
15389 this.$container = config.$container;
15390 this.containerPadding = config.containerPadding !== undefined ? config.containerPadding : 10;
15391 this.autoClose = !!config.autoClose;
15392 this.$autoCloseIgnore = config.$autoCloseIgnore;
15393 this.transitionTimeout = null;
15394 this.anchor = null;
15395 this.width = config.width !== undefined ? config.width : 320;
15396 this.height = config.height !== undefined ? config.height : null;
15397 this.setAlignment( config.align );
15398 this.closeButton = new OO.ui.ButtonWidget( { framed: false, icon: 'close' } );
15399 this.onMouseDownHandler = this.onMouseDown.bind( this );
15400 this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
15401
15402 // Events
15403 this.closeButton.connect( this, { click: 'onCloseButtonClick' } );
15404
15405 // Initialization
15406 this.toggleAnchor( config.anchor === undefined || config.anchor );
15407 this.$body.addClass( 'oo-ui-popupWidget-body' );
15408 this.$anchor.addClass( 'oo-ui-popupWidget-anchor' );
15409 this.$head
15410 .addClass( 'oo-ui-popupWidget-head' )
15411 .append( this.$label, this.closeButton.$element );
15412 if ( !config.head ) {
15413 this.$head.addClass( 'oo-ui-element-hidden' );
15414 }
15415 this.$popup
15416 .addClass( 'oo-ui-popupWidget-popup' )
15417 .append( this.$head, this.$body );
15418 this.$element
15419 .addClass( 'oo-ui-popupWidget' )
15420 .append( this.$popup, this.$anchor );
15421 // Move content, which was added to #$element by OO.ui.Widget, to the body
15422 if ( config.$content instanceof jQuery ) {
15423 this.$body.append( config.$content );
15424 }
15425 if ( config.padded ) {
15426 this.$body.addClass( 'oo-ui-popupWidget-body-padded' );
15427 }
15428
15429 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
15430 // that reference properties not initialized at that time of parent class construction
15431 // TODO: Find a better way to handle post-constructor setup
15432 this.visible = false;
15433 this.$element.addClass( 'oo-ui-element-hidden' );
15434 };
15435
15436 /* Setup */
15437
15438 OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
15439 OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.LabelElement );
15440 OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.ClippableElement );
15441
15442 /* Methods */
15443
15444 /**
15445 * Handles mouse down events.
15446 *
15447 * @private
15448 * @param {MouseEvent} e Mouse down event
15449 */
15450 OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
15451 if (
15452 this.isVisible() &&
15453 !$.contains( this.$element[ 0 ], e.target ) &&
15454 ( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length )
15455 ) {
15456 this.toggle( false );
15457 }
15458 };
15459
15460 /**
15461 * Bind mouse down listener.
15462 *
15463 * @private
15464 */
15465 OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
15466 // Capture clicks outside popup
15467 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
15468 };
15469
15470 /**
15471 * Handles close button click events.
15472 *
15473 * @private
15474 */
15475 OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
15476 if ( this.isVisible() ) {
15477 this.toggle( false );
15478 }
15479 };
15480
15481 /**
15482 * Unbind mouse down listener.
15483 *
15484 * @private
15485 */
15486 OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
15487 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
15488 };
15489
15490 /**
15491 * Handles key down events.
15492 *
15493 * @private
15494 * @param {KeyboardEvent} e Key down event
15495 */
15496 OO.ui.PopupWidget.prototype.onDocumentKeyDown = function ( e ) {
15497 if (
15498 e.which === OO.ui.Keys.ESCAPE &&
15499 this.isVisible()
15500 ) {
15501 this.toggle( false );
15502 e.preventDefault();
15503 e.stopPropagation();
15504 }
15505 };
15506
15507 /**
15508 * Bind key down listener.
15509 *
15510 * @private
15511 */
15512 OO.ui.PopupWidget.prototype.bindKeyDownListener = function () {
15513 this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
15514 };
15515
15516 /**
15517 * Unbind key down listener.
15518 *
15519 * @private
15520 */
15521 OO.ui.PopupWidget.prototype.unbindKeyDownListener = function () {
15522 this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
15523 };
15524
15525 /**
15526 * Show, hide, or toggle the visibility of the anchor.
15527 *
15528 * @param {boolean} [show] Show anchor, omit to toggle
15529 */
15530 OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) {
15531 show = show === undefined ? !this.anchored : !!show;
15532
15533 if ( this.anchored !== show ) {
15534 if ( show ) {
15535 this.$element.addClass( 'oo-ui-popupWidget-anchored' );
15536 } else {
15537 this.$element.removeClass( 'oo-ui-popupWidget-anchored' );
15538 }
15539 this.anchored = show;
15540 }
15541 };
15542
15543 /**
15544 * Check if the anchor is visible.
15545 *
15546 * @return {boolean} Anchor is visible
15547 */
15548 OO.ui.PopupWidget.prototype.hasAnchor = function () {
15549 return this.anchor;
15550 };
15551
15552 /**
15553 * @inheritdoc
15554 */
15555 OO.ui.PopupWidget.prototype.toggle = function ( show ) {
15556 show = show === undefined ? !this.isVisible() : !!show;
15557
15558 var change = show !== this.isVisible();
15559
15560 // Parent method
15561 OO.ui.PopupWidget.parent.prototype.toggle.call( this, show );
15562
15563 if ( change ) {
15564 if ( show ) {
15565 if ( this.autoClose ) {
15566 this.bindMouseDownListener();
15567 this.bindKeyDownListener();
15568 }
15569 this.updateDimensions();
15570 this.toggleClipping( true );
15571 } else {
15572 this.toggleClipping( false );
15573 if ( this.autoClose ) {
15574 this.unbindMouseDownListener();
15575 this.unbindKeyDownListener();
15576 }
15577 }
15578 }
15579
15580 return this;
15581 };
15582
15583 /**
15584 * Set the size of the popup.
15585 *
15586 * Changing the size may also change the popup's position depending on the alignment.
15587 *
15588 * @param {number} width Width in pixels
15589 * @param {number} height Height in pixels
15590 * @param {boolean} [transition=false] Use a smooth transition
15591 * @chainable
15592 */
15593 OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) {
15594 this.width = width;
15595 this.height = height !== undefined ? height : null;
15596 if ( this.isVisible() ) {
15597 this.updateDimensions( transition );
15598 }
15599 };
15600
15601 /**
15602 * Update the size and position.
15603 *
15604 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
15605 * be called automatically.
15606 *
15607 * @param {boolean} [transition=false] Use a smooth transition
15608 * @chainable
15609 */
15610 OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
15611 var popupOffset, originOffset, containerLeft, containerWidth, containerRight,
15612 popupLeft, popupRight, overlapLeft, overlapRight, anchorWidth,
15613 align = this.align,
15614 widget = this;
15615
15616 if ( !this.$container ) {
15617 // Lazy-initialize $container if not specified in constructor
15618 this.$container = $( this.getClosestScrollableElementContainer() );
15619 }
15620
15621 // Set height and width before measuring things, since it might cause our measurements
15622 // to change (e.g. due to scrollbars appearing or disappearing)
15623 this.$popup.css( {
15624 width: this.width,
15625 height: this.height !== null ? this.height : 'auto'
15626 } );
15627
15628 // If we are in RTL, we need to flip the alignment, unless it is center
15629 if ( align === 'forwards' || align === 'backwards' ) {
15630 if ( this.$container.css( 'direction' ) === 'rtl' ) {
15631 align = ( { forwards: 'force-left', backwards: 'force-right' } )[ this.align ];
15632 } else {
15633 align = ( { forwards: 'force-right', backwards: 'force-left' } )[ this.align ];
15634 }
15635
15636 }
15637
15638 // Compute initial popupOffset based on alignment
15639 popupOffset = this.width * ( { 'force-left': -1, center: -0.5, 'force-right': 0 } )[ align ];
15640
15641 // Figure out if this will cause the popup to go beyond the edge of the container
15642 originOffset = this.$element.offset().left;
15643 containerLeft = this.$container.offset().left;
15644 containerWidth = this.$container.innerWidth();
15645 containerRight = containerLeft + containerWidth;
15646 popupLeft = popupOffset - this.containerPadding;
15647 popupRight = popupOffset + this.containerPadding + this.width + this.containerPadding;
15648 overlapLeft = ( originOffset + popupLeft ) - containerLeft;
15649 overlapRight = containerRight - ( originOffset + popupRight );
15650
15651 // Adjust offset to make the popup not go beyond the edge, if needed
15652 if ( overlapRight < 0 ) {
15653 popupOffset += overlapRight;
15654 } else if ( overlapLeft < 0 ) {
15655 popupOffset -= overlapLeft;
15656 }
15657
15658 // Adjust offset to avoid anchor being rendered too close to the edge
15659 // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
15660 // TODO: Find a measurement that works for CSS anchors and image anchors
15661 anchorWidth = this.$anchor[ 0 ].scrollWidth * 2;
15662 if ( popupOffset + this.width < anchorWidth ) {
15663 popupOffset = anchorWidth - this.width;
15664 } else if ( -popupOffset < anchorWidth ) {
15665 popupOffset = -anchorWidth;
15666 }
15667
15668 // Prevent transition from being interrupted
15669 clearTimeout( this.transitionTimeout );
15670 if ( transition ) {
15671 // Enable transition
15672 this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
15673 }
15674
15675 // Position body relative to anchor
15676 this.$popup.css( 'margin-left', popupOffset );
15677
15678 if ( transition ) {
15679 // Prevent transitioning after transition is complete
15680 this.transitionTimeout = setTimeout( function () {
15681 widget.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
15682 }, 200 );
15683 } else {
15684 // Prevent transitioning immediately
15685 this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
15686 }
15687
15688 // Reevaluate clipping state since we've relocated and resized the popup
15689 this.clip();
15690
15691 return this;
15692 };
15693
15694 /**
15695 * Set popup alignment
15696 * @param {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
15697 * `backwards` or `forwards`.
15698 */
15699 OO.ui.PopupWidget.prototype.setAlignment = function ( align ) {
15700 // Validate alignment and transform deprecated values
15701 if ( [ 'left', 'right', 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align ) > -1 ) {
15702 this.align = { left: 'force-right', right: 'force-left' }[ align ] || align;
15703 } else {
15704 this.align = 'center';
15705 }
15706 };
15707
15708 /**
15709 * Get popup alignment
15710 * @return {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
15711 * `backwards` or `forwards`.
15712 */
15713 OO.ui.PopupWidget.prototype.getAlignment = function () {
15714 return this.align;
15715 };
15716
15717 /**
15718 * Progress bars visually display the status of an operation, such as a download,
15719 * and can be either determinate or indeterminate:
15720 *
15721 * - **determinate** process bars show the percent of an operation that is complete.
15722 *
15723 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
15724 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
15725 * not use percentages.
15726 *
15727 * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
15728 *
15729 * @example
15730 * // Examples of determinate and indeterminate progress bars.
15731 * var progressBar1 = new OO.ui.ProgressBarWidget( {
15732 * progress: 33
15733 * } );
15734 * var progressBar2 = new OO.ui.ProgressBarWidget();
15735 *
15736 * // Create a FieldsetLayout to layout progress bars
15737 * var fieldset = new OO.ui.FieldsetLayout;
15738 * fieldset.addItems( [
15739 * new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
15740 * new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
15741 * ] );
15742 * $( 'body' ).append( fieldset.$element );
15743 *
15744 * @class
15745 * @extends OO.ui.Widget
15746 *
15747 * @constructor
15748 * @param {Object} [config] Configuration options
15749 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
15750 * To create a determinate progress bar, specify a number that reflects the initial percent complete.
15751 * By default, the progress bar is indeterminate.
15752 */
15753 OO.ui.ProgressBarWidget = function OoUiProgressBarWidget( config ) {
15754 // Configuration initialization
15755 config = config || {};
15756
15757 // Parent constructor
15758 OO.ui.ProgressBarWidget.parent.call( this, config );
15759
15760 // Properties
15761 this.$bar = $( '<div>' );
15762 this.progress = null;
15763
15764 // Initialization
15765 this.setProgress( config.progress !== undefined ? config.progress : false );
15766 this.$bar.addClass( 'oo-ui-progressBarWidget-bar' );
15767 this.$element
15768 .attr( {
15769 role: 'progressbar',
15770 'aria-valuemin': 0,
15771 'aria-valuemax': 100
15772 } )
15773 .addClass( 'oo-ui-progressBarWidget' )
15774 .append( this.$bar );
15775 };
15776
15777 /* Setup */
15778
15779 OO.inheritClass( OO.ui.ProgressBarWidget, OO.ui.Widget );
15780
15781 /* Static Properties */
15782
15783 OO.ui.ProgressBarWidget.static.tagName = 'div';
15784
15785 /* Methods */
15786
15787 /**
15788 * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
15789 *
15790 * @return {number|boolean} Progress percent
15791 */
15792 OO.ui.ProgressBarWidget.prototype.getProgress = function () {
15793 return this.progress;
15794 };
15795
15796 /**
15797 * Set the percent of the process completed or `false` for an indeterminate process.
15798 *
15799 * @param {number|boolean} progress Progress percent or `false` for indeterminate
15800 */
15801 OO.ui.ProgressBarWidget.prototype.setProgress = function ( progress ) {
15802 this.progress = progress;
15803
15804 if ( progress !== false ) {
15805 this.$bar.css( 'width', this.progress + '%' );
15806 this.$element.attr( 'aria-valuenow', this.progress );
15807 } else {
15808 this.$bar.css( 'width', '' );
15809 this.$element.removeAttr( 'aria-valuenow' );
15810 }
15811 this.$element.toggleClass( 'oo-ui-progressBarWidget-indeterminate', !progress );
15812 };
15813
15814 /**
15815 * SearchWidgets combine a {@link OO.ui.TextInputWidget text input field}, where users can type a search query,
15816 * and a {@link OO.ui.TextInputMenuSelectWidget menu} of search results, which is displayed beneath the query
15817 * field. Unlike {@link OO.ui.mixin.LookupElement lookup menus}, search result menus are always visible to the user.
15818 * Users can choose an item from the menu or type a query into the text field to search for a matching result item.
15819 * In general, search widgets are used inside a separate {@link OO.ui.Dialog dialog} window.
15820 *
15821 * Each time the query is changed, the search result menu is cleared and repopulated. Please see
15822 * the [OOjs UI demos][1] for an example.
15823 *
15824 * [1]: https://tools.wmflabs.org/oojs-ui/oojs-ui/demos/#dialogs-mediawiki-vector-ltr
15825 *
15826 * @class
15827 * @extends OO.ui.Widget
15828 *
15829 * @constructor
15830 * @param {Object} [config] Configuration options
15831 * @cfg {string|jQuery} [placeholder] Placeholder text for query input
15832 * @cfg {string} [value] Initial query value
15833 */
15834 OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
15835 // Configuration initialization
15836 config = config || {};
15837
15838 // Parent constructor
15839 OO.ui.SearchWidget.parent.call( this, config );
15840
15841 // Properties
15842 this.query = new OO.ui.TextInputWidget( {
15843 icon: 'search',
15844 placeholder: config.placeholder,
15845 value: config.value
15846 } );
15847 this.results = new OO.ui.SelectWidget();
15848 this.$query = $( '<div>' );
15849 this.$results = $( '<div>' );
15850
15851 // Events
15852 this.query.connect( this, {
15853 change: 'onQueryChange',
15854 enter: 'onQueryEnter'
15855 } );
15856 this.results.connect( this, {
15857 highlight: 'onResultsHighlight',
15858 select: 'onResultsSelect'
15859 } );
15860 this.query.$input.on( 'keydown', this.onQueryKeydown.bind( this ) );
15861
15862 // Initialization
15863 this.$query
15864 .addClass( 'oo-ui-searchWidget-query' )
15865 .append( this.query.$element );
15866 this.$results
15867 .addClass( 'oo-ui-searchWidget-results' )
15868 .append( this.results.$element );
15869 this.$element
15870 .addClass( 'oo-ui-searchWidget' )
15871 .append( this.$results, this.$query );
15872 };
15873
15874 /* Setup */
15875
15876 OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget );
15877
15878 /* Events */
15879
15880 /**
15881 * A 'highlight' event is emitted when an item is highlighted. The highlight indicates which
15882 * item will be selected. When a user mouses over a menu item, it is highlighted. If a search
15883 * string is typed into the query field instead, the first menu item that matches the query
15884 * will be highlighted.
15885
15886 * @event highlight
15887 * @deprecated Connect straight to getResults() events instead
15888 * @param {Object|null} item Item data or null if no item is highlighted
15889 */
15890
15891 /**
15892 * A 'select' event is emitted when an item is selected. A menu item is selected when it is clicked,
15893 * or when a user types a search query, a menu result is highlighted, and the user presses enter.
15894 *
15895 * @event select
15896 * @deprecated Connect straight to getResults() events instead
15897 * @param {Object|null} item Item data or null if no item is selected
15898 */
15899
15900 /* Methods */
15901
15902 /**
15903 * Handle query key down events.
15904 *
15905 * @private
15906 * @param {jQuery.Event} e Key down event
15907 */
15908 OO.ui.SearchWidget.prototype.onQueryKeydown = function ( e ) {
15909 var highlightedItem, nextItem,
15910 dir = e.which === OO.ui.Keys.DOWN ? 1 : ( e.which === OO.ui.Keys.UP ? -1 : 0 );
15911
15912 if ( dir ) {
15913 highlightedItem = this.results.getHighlightedItem();
15914 if ( !highlightedItem ) {
15915 highlightedItem = this.results.getSelectedItem();
15916 }
15917 nextItem = this.results.getRelativeSelectableItem( highlightedItem, dir );
15918 this.results.highlightItem( nextItem );
15919 nextItem.scrollElementIntoView();
15920 }
15921 };
15922
15923 /**
15924 * Handle select widget select events.
15925 *
15926 * Clears existing results. Subclasses should repopulate items according to new query.
15927 *
15928 * @private
15929 * @param {string} value New value
15930 */
15931 OO.ui.SearchWidget.prototype.onQueryChange = function () {
15932 // Reset
15933 this.results.clearItems();
15934 };
15935
15936 /**
15937 * Handle select widget enter key events.
15938 *
15939 * Selects highlighted item.
15940 *
15941 * @private
15942 * @param {string} value New value
15943 */
15944 OO.ui.SearchWidget.prototype.onQueryEnter = function () {
15945 // Reset
15946 this.results.selectItem( this.results.getHighlightedItem() );
15947 };
15948
15949 /**
15950 * Handle select widget highlight events.
15951 *
15952 * @private
15953 * @deprecated Connect straight to getResults() events instead
15954 * @param {OO.ui.OptionWidget} item Highlighted item
15955 * @fires highlight
15956 */
15957 OO.ui.SearchWidget.prototype.onResultsHighlight = function ( item ) {
15958 this.emit( 'highlight', item ? item.getData() : null );
15959 };
15960
15961 /**
15962 * Handle select widget select events.
15963 *
15964 * @private
15965 * @deprecated Connect straight to getResults() events instead
15966 * @param {OO.ui.OptionWidget} item Selected item
15967 * @fires select
15968 */
15969 OO.ui.SearchWidget.prototype.onResultsSelect = function ( item ) {
15970 this.emit( 'select', item ? item.getData() : null );
15971 };
15972
15973 /**
15974 * Get the query input.
15975 *
15976 * @return {OO.ui.TextInputWidget} Query input
15977 */
15978 OO.ui.SearchWidget.prototype.getQuery = function () {
15979 return this.query;
15980 };
15981
15982 /**
15983 * Get the search results menu.
15984 *
15985 * @return {OO.ui.SelectWidget} Menu of search results
15986 */
15987 OO.ui.SearchWidget.prototype.getResults = function () {
15988 return this.results;
15989 };
15990
15991 /**
15992 * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
15993 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
15994 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
15995 * menu selects}.
15996 *
15997 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
15998 * information, please see the [OOjs UI documentation on MediaWiki][1].
15999 *
16000 * @example
16001 * // Example of a select widget with three options
16002 * var select = new OO.ui.SelectWidget( {
16003 * items: [
16004 * new OO.ui.OptionWidget( {
16005 * data: 'a',
16006 * label: 'Option One',
16007 * } ),
16008 * new OO.ui.OptionWidget( {
16009 * data: 'b',
16010 * label: 'Option Two',
16011 * } ),
16012 * new OO.ui.OptionWidget( {
16013 * data: 'c',
16014 * label: 'Option Three',
16015 * } )
16016 * ]
16017 * } );
16018 * $( 'body' ).append( select.$element );
16019 *
16020 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
16021 *
16022 * @abstract
16023 * @class
16024 * @extends OO.ui.Widget
16025 * @mixins OO.ui.mixin.GroupElement
16026 *
16027 * @constructor
16028 * @param {Object} [config] Configuration options
16029 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
16030 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
16031 * the [OOjs UI documentation on MediaWiki] [2] for examples.
16032 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
16033 */
16034 OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
16035 // Configuration initialization
16036 config = config || {};
16037
16038 // Parent constructor
16039 OO.ui.SelectWidget.parent.call( this, config );
16040
16041 // Mixin constructors
16042 OO.ui.mixin.GroupWidget.call( this, $.extend( {}, config, { $group: this.$element } ) );
16043
16044 // Properties
16045 this.pressed = false;
16046 this.selecting = null;
16047 this.onMouseUpHandler = this.onMouseUp.bind( this );
16048 this.onMouseMoveHandler = this.onMouseMove.bind( this );
16049 this.onKeyDownHandler = this.onKeyDown.bind( this );
16050 this.onKeyPressHandler = this.onKeyPress.bind( this );
16051 this.keyPressBuffer = '';
16052 this.keyPressBufferTimer = null;
16053
16054 // Events
16055 this.connect( this, {
16056 toggle: 'onToggle'
16057 } );
16058 this.$element.on( {
16059 mousedown: this.onMouseDown.bind( this ),
16060 mouseover: this.onMouseOver.bind( this ),
16061 mouseleave: this.onMouseLeave.bind( this )
16062 } );
16063
16064 // Initialization
16065 this.$element
16066 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
16067 .attr( 'role', 'listbox' );
16068 if ( Array.isArray( config.items ) ) {
16069 this.addItems( config.items );
16070 }
16071 };
16072
16073 /* Setup */
16074
16075 OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
16076
16077 // Need to mixin base class as well
16078 OO.mixinClass( OO.ui.SelectWidget, OO.ui.mixin.GroupElement );
16079 OO.mixinClass( OO.ui.SelectWidget, OO.ui.mixin.GroupWidget );
16080
16081 /* Static */
16082 OO.ui.SelectWidget.static.passAllFilter = function () {
16083 return true;
16084 };
16085
16086 /* Events */
16087
16088 /**
16089 * @event highlight
16090 *
16091 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
16092 *
16093 * @param {OO.ui.OptionWidget|null} item Highlighted item
16094 */
16095
16096 /**
16097 * @event press
16098 *
16099 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
16100 * pressed state of an option.
16101 *
16102 * @param {OO.ui.OptionWidget|null} item Pressed item
16103 */
16104
16105 /**
16106 * @event select
16107 *
16108 * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
16109 *
16110 * @param {OO.ui.OptionWidget|null} item Selected item
16111 */
16112
16113 /**
16114 * @event choose
16115 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
16116 * @param {OO.ui.OptionWidget} item Chosen item
16117 */
16118
16119 /**
16120 * @event add
16121 *
16122 * An `add` event is emitted when options are added to the select with the #addItems method.
16123 *
16124 * @param {OO.ui.OptionWidget[]} items Added items
16125 * @param {number} index Index of insertion point
16126 */
16127
16128 /**
16129 * @event remove
16130 *
16131 * A `remove` event is emitted when options are removed from the select with the #clearItems
16132 * or #removeItems methods.
16133 *
16134 * @param {OO.ui.OptionWidget[]} items Removed items
16135 */
16136
16137 /* Methods */
16138
16139 /**
16140 * Handle mouse down events.
16141 *
16142 * @private
16143 * @param {jQuery.Event} e Mouse down event
16144 */
16145 OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
16146 var item;
16147
16148 if ( !this.isDisabled() && e.which === 1 ) {
16149 this.togglePressed( true );
16150 item = this.getTargetItem( e );
16151 if ( item && item.isSelectable() ) {
16152 this.pressItem( item );
16153 this.selecting = item;
16154 this.getElementDocument().addEventListener(
16155 'mouseup',
16156 this.onMouseUpHandler,
16157 true
16158 );
16159 this.getElementDocument().addEventListener(
16160 'mousemove',
16161 this.onMouseMoveHandler,
16162 true
16163 );
16164 }
16165 }
16166 return false;
16167 };
16168
16169 /**
16170 * Handle mouse up events.
16171 *
16172 * @private
16173 * @param {jQuery.Event} e Mouse up event
16174 */
16175 OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
16176 var item;
16177
16178 this.togglePressed( false );
16179 if ( !this.selecting ) {
16180 item = this.getTargetItem( e );
16181 if ( item && item.isSelectable() ) {
16182 this.selecting = item;
16183 }
16184 }
16185 if ( !this.isDisabled() && e.which === 1 && this.selecting ) {
16186 this.pressItem( null );
16187 this.chooseItem( this.selecting );
16188 this.selecting = null;
16189 }
16190
16191 this.getElementDocument().removeEventListener(
16192 'mouseup',
16193 this.onMouseUpHandler,
16194 true
16195 );
16196 this.getElementDocument().removeEventListener(
16197 'mousemove',
16198 this.onMouseMoveHandler,
16199 true
16200 );
16201
16202 return false;
16203 };
16204
16205 /**
16206 * Handle mouse move events.
16207 *
16208 * @private
16209 * @param {jQuery.Event} e Mouse move event
16210 */
16211 OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
16212 var item;
16213
16214 if ( !this.isDisabled() && this.pressed ) {
16215 item = this.getTargetItem( e );
16216 if ( item && item !== this.selecting && item.isSelectable() ) {
16217 this.pressItem( item );
16218 this.selecting = item;
16219 }
16220 }
16221 return false;
16222 };
16223
16224 /**
16225 * Handle mouse over events.
16226 *
16227 * @private
16228 * @param {jQuery.Event} e Mouse over event
16229 */
16230 OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
16231 var item;
16232
16233 if ( !this.isDisabled() ) {
16234 item = this.getTargetItem( e );
16235 this.highlightItem( item && item.isHighlightable() ? item : null );
16236 }
16237 return false;
16238 };
16239
16240 /**
16241 * Handle mouse leave events.
16242 *
16243 * @private
16244 * @param {jQuery.Event} e Mouse over event
16245 */
16246 OO.ui.SelectWidget.prototype.onMouseLeave = function () {
16247 if ( !this.isDisabled() ) {
16248 this.highlightItem( null );
16249 }
16250 return false;
16251 };
16252
16253 /**
16254 * Handle key down events.
16255 *
16256 * @protected
16257 * @param {jQuery.Event} e Key down event
16258 */
16259 OO.ui.SelectWidget.prototype.onKeyDown = function ( e ) {
16260 var nextItem,
16261 handled = false,
16262 currentItem = this.getHighlightedItem() || this.getSelectedItem();
16263
16264 if ( !this.isDisabled() && this.isVisible() ) {
16265 switch ( e.keyCode ) {
16266 case OO.ui.Keys.ENTER:
16267 if ( currentItem && currentItem.constructor.static.highlightable ) {
16268 // Was only highlighted, now let's select it. No-op if already selected.
16269 this.chooseItem( currentItem );
16270 handled = true;
16271 }
16272 break;
16273 case OO.ui.Keys.UP:
16274 case OO.ui.Keys.LEFT:
16275 this.clearKeyPressBuffer();
16276 nextItem = this.getRelativeSelectableItem( currentItem, -1 );
16277 handled = true;
16278 break;
16279 case OO.ui.Keys.DOWN:
16280 case OO.ui.Keys.RIGHT:
16281 this.clearKeyPressBuffer();
16282 nextItem = this.getRelativeSelectableItem( currentItem, 1 );
16283 handled = true;
16284 break;
16285 case OO.ui.Keys.ESCAPE:
16286 case OO.ui.Keys.TAB:
16287 if ( currentItem && currentItem.constructor.static.highlightable ) {
16288 currentItem.setHighlighted( false );
16289 }
16290 this.unbindKeyDownListener();
16291 this.unbindKeyPressListener();
16292 // Don't prevent tabbing away / defocusing
16293 handled = false;
16294 break;
16295 }
16296
16297 if ( nextItem ) {
16298 if ( nextItem.constructor.static.highlightable ) {
16299 this.highlightItem( nextItem );
16300 } else {
16301 this.chooseItem( nextItem );
16302 }
16303 nextItem.scrollElementIntoView();
16304 }
16305
16306 if ( handled ) {
16307 // Can't just return false, because e is not always a jQuery event
16308 e.preventDefault();
16309 e.stopPropagation();
16310 }
16311 }
16312 };
16313
16314 /**
16315 * Bind key down listener.
16316 *
16317 * @protected
16318 */
16319 OO.ui.SelectWidget.prototype.bindKeyDownListener = function () {
16320 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
16321 };
16322
16323 /**
16324 * Unbind key down listener.
16325 *
16326 * @protected
16327 */
16328 OO.ui.SelectWidget.prototype.unbindKeyDownListener = function () {
16329 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
16330 };
16331
16332 /**
16333 * Clear the key-press buffer
16334 *
16335 * @protected
16336 */
16337 OO.ui.SelectWidget.prototype.clearKeyPressBuffer = function () {
16338 if ( this.keyPressBufferTimer ) {
16339 clearTimeout( this.keyPressBufferTimer );
16340 this.keyPressBufferTimer = null;
16341 }
16342 this.keyPressBuffer = '';
16343 };
16344
16345 /**
16346 * Handle key press events.
16347 *
16348 * @protected
16349 * @param {jQuery.Event} e Key press event
16350 */
16351 OO.ui.SelectWidget.prototype.onKeyPress = function ( e ) {
16352 var c, filter, item;
16353
16354 if ( !e.charCode ) {
16355 if ( e.keyCode === OO.ui.Keys.BACKSPACE && this.keyPressBuffer !== '' ) {
16356 this.keyPressBuffer = this.keyPressBuffer.substr( 0, this.keyPressBuffer.length - 1 );
16357 return false;
16358 }
16359 return;
16360 }
16361 if ( String.fromCodePoint ) {
16362 c = String.fromCodePoint( e.charCode );
16363 } else {
16364 c = String.fromCharCode( e.charCode );
16365 }
16366
16367 if ( this.keyPressBufferTimer ) {
16368 clearTimeout( this.keyPressBufferTimer );
16369 }
16370 this.keyPressBufferTimer = setTimeout( this.clearKeyPressBuffer.bind( this ), 1500 );
16371
16372 item = this.getHighlightedItem() || this.getSelectedItem();
16373
16374 if ( this.keyPressBuffer === c ) {
16375 // Common (if weird) special case: typing "xxxx" will cycle through all
16376 // the items beginning with "x".
16377 if ( item ) {
16378 item = this.getRelativeSelectableItem( item, 1 );
16379 }
16380 } else {
16381 this.keyPressBuffer += c;
16382 }
16383
16384 filter = this.getItemMatcher( this.keyPressBuffer );
16385 if ( !item || !filter( item ) ) {
16386 item = this.getRelativeSelectableItem( item, 1, filter );
16387 }
16388 if ( item ) {
16389 if ( item.constructor.static.highlightable ) {
16390 this.highlightItem( item );
16391 } else {
16392 this.chooseItem( item );
16393 }
16394 item.scrollElementIntoView();
16395 }
16396
16397 return false;
16398 };
16399
16400 /**
16401 * Get a matcher for the specific string
16402 *
16403 * @protected
16404 * @param {string} s String to match against items
16405 * @return {Function} function ( OO.ui.OptionItem ) => boolean
16406 */
16407 OO.ui.SelectWidget.prototype.getItemMatcher = function ( s ) {
16408 var re;
16409
16410 if ( s.normalize ) {
16411 s = s.normalize();
16412 }
16413 re = new RegExp( '^\\s*' + s.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' ), 'i' );
16414 return function ( item ) {
16415 var l = item.getLabel();
16416 if ( typeof l !== 'string' ) {
16417 l = item.$label.text();
16418 }
16419 if ( l.normalize ) {
16420 l = l.normalize();
16421 }
16422 return re.test( l );
16423 };
16424 };
16425
16426 /**
16427 * Bind key press listener.
16428 *
16429 * @protected
16430 */
16431 OO.ui.SelectWidget.prototype.bindKeyPressListener = function () {
16432 this.getElementWindow().addEventListener( 'keypress', this.onKeyPressHandler, true );
16433 };
16434
16435 /**
16436 * Unbind key down listener.
16437 *
16438 * If you override this, be sure to call this.clearKeyPressBuffer() from your
16439 * implementation.
16440 *
16441 * @protected
16442 */
16443 OO.ui.SelectWidget.prototype.unbindKeyPressListener = function () {
16444 this.getElementWindow().removeEventListener( 'keypress', this.onKeyPressHandler, true );
16445 this.clearKeyPressBuffer();
16446 };
16447
16448 /**
16449 * Visibility change handler
16450 *
16451 * @protected
16452 * @param {boolean} visible
16453 */
16454 OO.ui.SelectWidget.prototype.onToggle = function ( visible ) {
16455 if ( !visible ) {
16456 this.clearKeyPressBuffer();
16457 }
16458 };
16459
16460 /**
16461 * Get the closest item to a jQuery.Event.
16462 *
16463 * @private
16464 * @param {jQuery.Event} e
16465 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
16466 */
16467 OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) {
16468 return $( e.target ).closest( '.oo-ui-optionWidget' ).data( 'oo-ui-optionWidget' ) || null;
16469 };
16470
16471 /**
16472 * Get selected item.
16473 *
16474 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
16475 */
16476 OO.ui.SelectWidget.prototype.getSelectedItem = function () {
16477 var i, len;
16478
16479 for ( i = 0, len = this.items.length; i < len; i++ ) {
16480 if ( this.items[ i ].isSelected() ) {
16481 return this.items[ i ];
16482 }
16483 }
16484 return null;
16485 };
16486
16487 /**
16488 * Get highlighted item.
16489 *
16490 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
16491 */
16492 OO.ui.SelectWidget.prototype.getHighlightedItem = function () {
16493 var i, len;
16494
16495 for ( i = 0, len = this.items.length; i < len; i++ ) {
16496 if ( this.items[ i ].isHighlighted() ) {
16497 return this.items[ i ];
16498 }
16499 }
16500 return null;
16501 };
16502
16503 /**
16504 * Toggle pressed state.
16505 *
16506 * Press is a state that occurs when a user mouses down on an item, but
16507 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
16508 * until the user releases the mouse.
16509 *
16510 * @param {boolean} pressed An option is being pressed
16511 */
16512 OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) {
16513 if ( pressed === undefined ) {
16514 pressed = !this.pressed;
16515 }
16516 if ( pressed !== this.pressed ) {
16517 this.$element
16518 .toggleClass( 'oo-ui-selectWidget-pressed', pressed )
16519 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed );
16520 this.pressed = pressed;
16521 }
16522 };
16523
16524 /**
16525 * Highlight an option. If the `item` param is omitted, no options will be highlighted
16526 * and any existing highlight will be removed. The highlight is mutually exclusive.
16527 *
16528 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
16529 * @fires highlight
16530 * @chainable
16531 */
16532 OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
16533 var i, len, highlighted,
16534 changed = false;
16535
16536 for ( i = 0, len = this.items.length; i < len; i++ ) {
16537 highlighted = this.items[ i ] === item;
16538 if ( this.items[ i ].isHighlighted() !== highlighted ) {
16539 this.items[ i ].setHighlighted( highlighted );
16540 changed = true;
16541 }
16542 }
16543 if ( changed ) {
16544 this.emit( 'highlight', item );
16545 }
16546
16547 return this;
16548 };
16549
16550 /**
16551 * Programmatically select an option by its data. If the `data` parameter is omitted,
16552 * or if the item does not exist, all options will be deselected.
16553 *
16554 * @param {Object|string} [data] Value of the item to select, omit to deselect all
16555 * @fires select
16556 * @chainable
16557 */
16558 OO.ui.SelectWidget.prototype.selectItemByData = function ( data ) {
16559 var itemFromData = this.getItemFromData( data );
16560 if ( data === undefined || !itemFromData ) {
16561 return this.selectItem();
16562 }
16563 return this.selectItem( itemFromData );
16564 };
16565
16566 /**
16567 * Programmatically select an option by its reference. If the `item` parameter is omitted,
16568 * all options will be deselected.
16569 *
16570 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
16571 * @fires select
16572 * @chainable
16573 */
16574 OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
16575 var i, len, selected,
16576 changed = false;
16577
16578 for ( i = 0, len = this.items.length; i < len; i++ ) {
16579 selected = this.items[ i ] === item;
16580 if ( this.items[ i ].isSelected() !== selected ) {
16581 this.items[ i ].setSelected( selected );
16582 changed = true;
16583 }
16584 }
16585 if ( changed ) {
16586 this.emit( 'select', item );
16587 }
16588
16589 return this;
16590 };
16591
16592 /**
16593 * Press an item.
16594 *
16595 * Press is a state that occurs when a user mouses down on an item, but has not
16596 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
16597 * releases the mouse.
16598 *
16599 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
16600 * @fires press
16601 * @chainable
16602 */
16603 OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
16604 var i, len, pressed,
16605 changed = false;
16606
16607 for ( i = 0, len = this.items.length; i < len; i++ ) {
16608 pressed = this.items[ i ] === item;
16609 if ( this.items[ i ].isPressed() !== pressed ) {
16610 this.items[ i ].setPressed( pressed );
16611 changed = true;
16612 }
16613 }
16614 if ( changed ) {
16615 this.emit( 'press', item );
16616 }
16617
16618 return this;
16619 };
16620
16621 /**
16622 * Choose an item.
16623 *
16624 * Note that ‘choose’ should never be modified programmatically. A user can choose
16625 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
16626 * use the #selectItem method.
16627 *
16628 * This method is identical to #selectItem, but may vary in subclasses that take additional action
16629 * when users choose an item with the keyboard or mouse.
16630 *
16631 * @param {OO.ui.OptionWidget} item Item to choose
16632 * @fires choose
16633 * @chainable
16634 */
16635 OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
16636 this.selectItem( item );
16637 this.emit( 'choose', item );
16638
16639 return this;
16640 };
16641
16642 /**
16643 * Get an option by its position relative to the specified item (or to the start of the option array,
16644 * if item is `null`). The direction in which to search through the option array is specified with a
16645 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
16646 * `null` if there are no options in the array.
16647 *
16648 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
16649 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
16650 * @param {Function} filter Only consider items for which this function returns
16651 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
16652 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
16653 */
16654 OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction, filter ) {
16655 var currentIndex, nextIndex, i,
16656 increase = direction > 0 ? 1 : -1,
16657 len = this.items.length;
16658
16659 if ( !$.isFunction( filter ) ) {
16660 filter = OO.ui.SelectWidget.static.passAllFilter;
16661 }
16662
16663 if ( item instanceof OO.ui.OptionWidget ) {
16664 currentIndex = $.inArray( item, this.items );
16665 nextIndex = ( currentIndex + increase + len ) % len;
16666 } else {
16667 // If no item is selected and moving forward, start at the beginning.
16668 // If moving backward, start at the end.
16669 nextIndex = direction > 0 ? 0 : len - 1;
16670 }
16671
16672 for ( i = 0; i < len; i++ ) {
16673 item = this.items[ nextIndex ];
16674 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() && filter( item ) ) {
16675 return item;
16676 }
16677 nextIndex = ( nextIndex + increase + len ) % len;
16678 }
16679 return null;
16680 };
16681
16682 /**
16683 * Get the next selectable item or `null` if there are no selectable items.
16684 * Disabled options and menu-section markers and breaks are not selectable.
16685 *
16686 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
16687 */
16688 OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
16689 var i, len, item;
16690
16691 for ( i = 0, len = this.items.length; i < len; i++ ) {
16692 item = this.items[ i ];
16693 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
16694 return item;
16695 }
16696 }
16697
16698 return null;
16699 };
16700
16701 /**
16702 * Add an array of options to the select. Optionally, an index number can be used to
16703 * specify an insertion point.
16704 *
16705 * @param {OO.ui.OptionWidget[]} items Items to add
16706 * @param {number} [index] Index to insert items after
16707 * @fires add
16708 * @chainable
16709 */
16710 OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
16711 // Mixin method
16712 OO.ui.mixin.GroupWidget.prototype.addItems.call( this, items, index );
16713
16714 // Always provide an index, even if it was omitted
16715 this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index );
16716
16717 return this;
16718 };
16719
16720 /**
16721 * Remove the specified array of options from the select. Options will be detached
16722 * from the DOM, not removed, so they can be reused later. To remove all options from
16723 * the select, you may wish to use the #clearItems method instead.
16724 *
16725 * @param {OO.ui.OptionWidget[]} items Items to remove
16726 * @fires remove
16727 * @chainable
16728 */
16729 OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
16730 var i, len, item;
16731
16732 // Deselect items being removed
16733 for ( i = 0, len = items.length; i < len; i++ ) {
16734 item = items[ i ];
16735 if ( item.isSelected() ) {
16736 this.selectItem( null );
16737 }
16738 }
16739
16740 // Mixin method
16741 OO.ui.mixin.GroupWidget.prototype.removeItems.call( this, items );
16742
16743 this.emit( 'remove', items );
16744
16745 return this;
16746 };
16747
16748 /**
16749 * Clear all options from the select. Options will be detached from the DOM, not removed,
16750 * so that they can be reused later. To remove a subset of options from the select, use
16751 * the #removeItems method.
16752 *
16753 * @fires remove
16754 * @chainable
16755 */
16756 OO.ui.SelectWidget.prototype.clearItems = function () {
16757 var items = this.items.slice();
16758
16759 // Mixin method
16760 OO.ui.mixin.GroupWidget.prototype.clearItems.call( this );
16761
16762 // Clear selection
16763 this.selectItem( null );
16764
16765 this.emit( 'remove', items );
16766
16767 return this;
16768 };
16769
16770 /**
16771 * ButtonSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains
16772 * button options and is used together with
16773 * OO.ui.ButtonOptionWidget. The ButtonSelectWidget provides an interface for
16774 * highlighting, choosing, and selecting mutually exclusive options. Please see
16775 * the [OOjs UI documentation on MediaWiki] [1] for more information.
16776 *
16777 * @example
16778 * // Example: A ButtonSelectWidget that contains three ButtonOptionWidgets
16779 * var option1 = new OO.ui.ButtonOptionWidget( {
16780 * data: 1,
16781 * label: 'Option 1',
16782 * title: 'Button option 1'
16783 * } );
16784 *
16785 * var option2 = new OO.ui.ButtonOptionWidget( {
16786 * data: 2,
16787 * label: 'Option 2',
16788 * title: 'Button option 2'
16789 * } );
16790 *
16791 * var option3 = new OO.ui.ButtonOptionWidget( {
16792 * data: 3,
16793 * label: 'Option 3',
16794 * title: 'Button option 3'
16795 * } );
16796 *
16797 * var buttonSelect=new OO.ui.ButtonSelectWidget( {
16798 * items: [ option1, option2, option3 ]
16799 * } );
16800 * $( 'body' ).append( buttonSelect.$element );
16801 *
16802 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
16803 *
16804 * @class
16805 * @extends OO.ui.SelectWidget
16806 * @mixins OO.ui.mixin.TabIndexedElement
16807 *
16808 * @constructor
16809 * @param {Object} [config] Configuration options
16810 */
16811 OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
16812 // Parent constructor
16813 OO.ui.ButtonSelectWidget.parent.call( this, config );
16814
16815 // Mixin constructors
16816 OO.ui.mixin.TabIndexedElement.call( this, config );
16817
16818 // Events
16819 this.$element.on( {
16820 focus: this.bindKeyDownListener.bind( this ),
16821 blur: this.unbindKeyDownListener.bind( this )
16822 } );
16823
16824 // Initialization
16825 this.$element.addClass( 'oo-ui-buttonSelectWidget' );
16826 };
16827
16828 /* Setup */
16829
16830 OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
16831 OO.mixinClass( OO.ui.ButtonSelectWidget, OO.ui.mixin.TabIndexedElement );
16832
16833 /**
16834 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
16835 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
16836 * an interface for adding, removing and selecting options.
16837 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
16838 *
16839 * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
16840 * OO.ui.RadioSelectInputWidget instead.
16841 *
16842 * @example
16843 * // A RadioSelectWidget with RadioOptions.
16844 * var option1 = new OO.ui.RadioOptionWidget( {
16845 * data: 'a',
16846 * label: 'Selected radio option'
16847 * } );
16848 *
16849 * var option2 = new OO.ui.RadioOptionWidget( {
16850 * data: 'b',
16851 * label: 'Unselected radio option'
16852 * } );
16853 *
16854 * var radioSelect=new OO.ui.RadioSelectWidget( {
16855 * items: [ option1, option2 ]
16856 * } );
16857 *
16858 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
16859 * radioSelect.selectItem( option1 );
16860 *
16861 * $( 'body' ).append( radioSelect.$element );
16862 *
16863 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
16864
16865 *
16866 * @class
16867 * @extends OO.ui.SelectWidget
16868 * @mixins OO.ui.mixin.TabIndexedElement
16869 *
16870 * @constructor
16871 * @param {Object} [config] Configuration options
16872 */
16873 OO.ui.RadioSelectWidget = function OoUiRadioSelectWidget( config ) {
16874 // Parent constructor
16875 OO.ui.RadioSelectWidget.parent.call( this, config );
16876
16877 // Mixin constructors
16878 OO.ui.mixin.TabIndexedElement.call( this, config );
16879
16880 // Events
16881 this.$element.on( {
16882 focus: this.bindKeyDownListener.bind( this ),
16883 blur: this.unbindKeyDownListener.bind( this )
16884 } );
16885
16886 // Initialization
16887 this.$element.addClass( 'oo-ui-radioSelectWidget' );
16888 };
16889
16890 /* Setup */
16891
16892 OO.inheritClass( OO.ui.RadioSelectWidget, OO.ui.SelectWidget );
16893 OO.mixinClass( OO.ui.RadioSelectWidget, OO.ui.mixin.TabIndexedElement );
16894
16895 /**
16896 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
16897 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
16898 * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxWidget ComboBoxWidget},
16899 * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
16900 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
16901 * and customized to be opened, closed, and displayed as needed.
16902 *
16903 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
16904 * mouse outside the menu.
16905 *
16906 * Menus also have support for keyboard interaction:
16907 *
16908 * - Enter/Return key: choose and select a menu option
16909 * - Up-arrow key: highlight the previous menu option
16910 * - Down-arrow key: highlight the next menu option
16911 * - Esc key: hide the menu
16912 *
16913 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
16914 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
16915 *
16916 * @class
16917 * @extends OO.ui.SelectWidget
16918 * @mixins OO.ui.mixin.ClippableElement
16919 *
16920 * @constructor
16921 * @param {Object} [config] Configuration options
16922 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
16923 * the text the user types. This config is used by {@link OO.ui.ComboBoxWidget ComboBoxWidget}
16924 * and {@link OO.ui.mixin.LookupElement LookupElement}
16925 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks the mouse
16926 * anywhere on the page outside of this widget, the menu is hidden. For example, if there is a button
16927 * that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
16928 * that button, unless the button (or its parent widget) is passed in here.
16929 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
16930 */
16931 OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) {
16932 // Configuration initialization
16933 config = config || {};
16934
16935 // Parent constructor
16936 OO.ui.MenuSelectWidget.parent.call( this, config );
16937
16938 // Mixin constructors
16939 OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
16940
16941 // Properties
16942 this.newItems = null;
16943 this.autoHide = config.autoHide === undefined || !!config.autoHide;
16944 this.$input = config.input ? config.input.$input : null;
16945 this.$widget = config.widget ? config.widget.$element : null;
16946 this.onDocumentMouseDownHandler = this.onDocumentMouseDown.bind( this );
16947
16948 // Initialization
16949 this.$element
16950 .addClass( 'oo-ui-menuSelectWidget' )
16951 .attr( 'role', 'menu' );
16952
16953 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
16954 // that reference properties not initialized at that time of parent class construction
16955 // TODO: Find a better way to handle post-constructor setup
16956 this.visible = false;
16957 this.$element.addClass( 'oo-ui-element-hidden' );
16958 };
16959
16960 /* Setup */
16961
16962 OO.inheritClass( OO.ui.MenuSelectWidget, OO.ui.SelectWidget );
16963 OO.mixinClass( OO.ui.MenuSelectWidget, OO.ui.mixin.ClippableElement );
16964
16965 /* Methods */
16966
16967 /**
16968 * Handles document mouse down events.
16969 *
16970 * @protected
16971 * @param {jQuery.Event} e Key down event
16972 */
16973 OO.ui.MenuSelectWidget.prototype.onDocumentMouseDown = function ( e ) {
16974 if (
16975 !OO.ui.contains( this.$element[ 0 ], e.target, true ) &&
16976 ( !this.$widget || !OO.ui.contains( this.$widget[ 0 ], e.target, true ) )
16977 ) {
16978 this.toggle( false );
16979 }
16980 };
16981
16982 /**
16983 * @inheritdoc
16984 */
16985 OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) {
16986 var currentItem = this.getHighlightedItem() || this.getSelectedItem();
16987
16988 if ( !this.isDisabled() && this.isVisible() ) {
16989 switch ( e.keyCode ) {
16990 case OO.ui.Keys.LEFT:
16991 case OO.ui.Keys.RIGHT:
16992 // Do nothing if a text field is associated, arrow keys will be handled natively
16993 if ( !this.$input ) {
16994 OO.ui.MenuSelectWidget.parent.prototype.onKeyDown.call( this, e );
16995 }
16996 break;
16997 case OO.ui.Keys.ESCAPE:
16998 case OO.ui.Keys.TAB:
16999 if ( currentItem ) {
17000 currentItem.setHighlighted( false );
17001 }
17002 this.toggle( false );
17003 // Don't prevent tabbing away, prevent defocusing
17004 if ( e.keyCode === OO.ui.Keys.ESCAPE ) {
17005 e.preventDefault();
17006 e.stopPropagation();
17007 }
17008 break;
17009 default:
17010 OO.ui.MenuSelectWidget.parent.prototype.onKeyDown.call( this, e );
17011 return;
17012 }
17013 }
17014 };
17015
17016 /**
17017 * @inheritdoc
17018 */
17019 OO.ui.MenuSelectWidget.prototype.bindKeyDownListener = function () {
17020 if ( this.$input ) {
17021 this.$input.on( 'keydown', this.onKeyDownHandler );
17022 } else {
17023 OO.ui.MenuSelectWidget.parent.prototype.bindKeyDownListener.call( this );
17024 }
17025 };
17026
17027 /**
17028 * @inheritdoc
17029 */
17030 OO.ui.MenuSelectWidget.prototype.unbindKeyDownListener = function () {
17031 if ( this.$input ) {
17032 this.$input.off( 'keydown', this.onKeyDownHandler );
17033 } else {
17034 OO.ui.MenuSelectWidget.parent.prototype.unbindKeyDownListener.call( this );
17035 }
17036 };
17037
17038 /**
17039 * @inheritdoc
17040 */
17041 OO.ui.MenuSelectWidget.prototype.bindKeyPressListener = function () {
17042 if ( !this.$input ) {
17043 OO.ui.MenuSelectWidget.parent.prototype.bindKeyPressListener.call( this );
17044 }
17045 };
17046
17047 /**
17048 * @inheritdoc
17049 */
17050 OO.ui.MenuSelectWidget.prototype.unbindKeyPressListener = function () {
17051 if ( this.$input ) {
17052 this.clearKeyPressBuffer();
17053 } else {
17054 OO.ui.MenuSelectWidget.parent.prototype.unbindKeyPressListener.call( this );
17055 }
17056 };
17057
17058 /**
17059 * Choose an item.
17060 *
17061 * When a user chooses an item, the menu is closed.
17062 *
17063 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
17064 * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
17065 * @param {OO.ui.OptionWidget} item Item to choose
17066 * @chainable
17067 */
17068 OO.ui.MenuSelectWidget.prototype.chooseItem = function ( item ) {
17069 OO.ui.MenuSelectWidget.parent.prototype.chooseItem.call( this, item );
17070 this.toggle( false );
17071 return this;
17072 };
17073
17074 /**
17075 * @inheritdoc
17076 */
17077 OO.ui.MenuSelectWidget.prototype.addItems = function ( items, index ) {
17078 var i, len, item;
17079
17080 // Parent method
17081 OO.ui.MenuSelectWidget.parent.prototype.addItems.call( this, items, index );
17082
17083 // Auto-initialize
17084 if ( !this.newItems ) {
17085 this.newItems = [];
17086 }
17087
17088 for ( i = 0, len = items.length; i < len; i++ ) {
17089 item = items[ i ];
17090 if ( this.isVisible() ) {
17091 // Defer fitting label until item has been attached
17092 item.fitLabel();
17093 } else {
17094 this.newItems.push( item );
17095 }
17096 }
17097
17098 // Reevaluate clipping
17099 this.clip();
17100
17101 return this;
17102 };
17103
17104 /**
17105 * @inheritdoc
17106 */
17107 OO.ui.MenuSelectWidget.prototype.removeItems = function ( items ) {
17108 // Parent method
17109 OO.ui.MenuSelectWidget.parent.prototype.removeItems.call( this, items );
17110
17111 // Reevaluate clipping
17112 this.clip();
17113
17114 return this;
17115 };
17116
17117 /**
17118 * @inheritdoc
17119 */
17120 OO.ui.MenuSelectWidget.prototype.clearItems = function () {
17121 // Parent method
17122 OO.ui.MenuSelectWidget.parent.prototype.clearItems.call( this );
17123
17124 // Reevaluate clipping
17125 this.clip();
17126
17127 return this;
17128 };
17129
17130 /**
17131 * @inheritdoc
17132 */
17133 OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
17134 visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
17135
17136 var i, len,
17137 change = visible !== this.isVisible();
17138
17139 // Parent method
17140 OO.ui.MenuSelectWidget.parent.prototype.toggle.call( this, visible );
17141
17142 if ( change ) {
17143 if ( visible ) {
17144 this.bindKeyDownListener();
17145 this.bindKeyPressListener();
17146
17147 if ( this.newItems && this.newItems.length ) {
17148 for ( i = 0, len = this.newItems.length; i < len; i++ ) {
17149 this.newItems[ i ].fitLabel();
17150 }
17151 this.newItems = null;
17152 }
17153 this.toggleClipping( true );
17154
17155 // Auto-hide
17156 if ( this.autoHide ) {
17157 this.getElementDocument().addEventListener(
17158 'mousedown', this.onDocumentMouseDownHandler, true
17159 );
17160 }
17161 } else {
17162 this.unbindKeyDownListener();
17163 this.unbindKeyPressListener();
17164 this.getElementDocument().removeEventListener(
17165 'mousedown', this.onDocumentMouseDownHandler, true
17166 );
17167 this.toggleClipping( false );
17168 }
17169 }
17170
17171 return this;
17172 };
17173
17174 /**
17175 * TextInputMenuSelectWidget is a menu that is specially designed to be positioned beneath
17176 * a {@link OO.ui.TextInputWidget text input} field. The menu's position is automatically
17177 * calculated and maintained when the menu is toggled or the window is resized.
17178 * See OO.ui.ComboBoxWidget for an example of a widget that uses this class.
17179 *
17180 * @class
17181 * @extends OO.ui.MenuSelectWidget
17182 *
17183 * @constructor
17184 * @param {OO.ui.TextInputWidget} inputWidget Text input widget to provide menu for
17185 * @param {Object} [config] Configuration options
17186 * @cfg {jQuery} [$container=input.$element] Element to render menu under
17187 */
17188 OO.ui.TextInputMenuSelectWidget = function OoUiTextInputMenuSelectWidget( inputWidget, config ) {
17189 // Allow passing positional parameters inside the config object
17190 if ( OO.isPlainObject( inputWidget ) && config === undefined ) {
17191 config = inputWidget;
17192 inputWidget = config.inputWidget;
17193 }
17194
17195 // Configuration initialization
17196 config = config || {};
17197
17198 // Parent constructor
17199 OO.ui.TextInputMenuSelectWidget.parent.call( this, config );
17200
17201 // Properties
17202 this.inputWidget = inputWidget;
17203 this.$container = config.$container || this.inputWidget.$element;
17204 this.onWindowResizeHandler = this.onWindowResize.bind( this );
17205
17206 // Initialization
17207 this.$element.addClass( 'oo-ui-textInputMenuSelectWidget' );
17208 };
17209
17210 /* Setup */
17211
17212 OO.inheritClass( OO.ui.TextInputMenuSelectWidget, OO.ui.MenuSelectWidget );
17213
17214 /* Methods */
17215
17216 /**
17217 * Handle window resize event.
17218 *
17219 * @private
17220 * @param {jQuery.Event} e Window resize event
17221 */
17222 OO.ui.TextInputMenuSelectWidget.prototype.onWindowResize = function () {
17223 this.position();
17224 };
17225
17226 /**
17227 * @inheritdoc
17228 */
17229 OO.ui.TextInputMenuSelectWidget.prototype.toggle = function ( visible ) {
17230 visible = visible === undefined ? !this.isVisible() : !!visible;
17231
17232 var change = visible !== this.isVisible();
17233
17234 if ( change && visible ) {
17235 // Make sure the width is set before the parent method runs.
17236 // After this we have to call this.position(); again to actually
17237 // position ourselves correctly.
17238 this.position();
17239 }
17240
17241 // Parent method
17242 OO.ui.TextInputMenuSelectWidget.parent.prototype.toggle.call( this, visible );
17243
17244 if ( change ) {
17245 if ( this.isVisible() ) {
17246 this.position();
17247 $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
17248 } else {
17249 $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
17250 }
17251 }
17252
17253 return this;
17254 };
17255
17256 /**
17257 * Position the menu.
17258 *
17259 * @private
17260 * @chainable
17261 */
17262 OO.ui.TextInputMenuSelectWidget.prototype.position = function () {
17263 var $container = this.$container,
17264 pos = OO.ui.Element.static.getRelativePosition( $container, this.$element.offsetParent() );
17265
17266 // Position under input
17267 pos.top += $container.height();
17268 this.$element.css( pos );
17269
17270 // Set width
17271 this.setIdealSize( $container.width() );
17272 // We updated the position, so re-evaluate the clipping state
17273 this.clip();
17274
17275 return this;
17276 };
17277
17278 /**
17279 * OutlineSelectWidget is a structured list that contains {@link OO.ui.OutlineOptionWidget outline options}
17280 * A set of controls can be provided with an {@link OO.ui.OutlineControlsWidget outline controls} widget.
17281 *
17282 * **Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.**
17283 *
17284 * @class
17285 * @extends OO.ui.SelectWidget
17286 * @mixins OO.ui.mixin.TabIndexedElement
17287 *
17288 * @constructor
17289 * @param {Object} [config] Configuration options
17290 */
17291 OO.ui.OutlineSelectWidget = function OoUiOutlineSelectWidget( config ) {
17292 // Parent constructor
17293 OO.ui.OutlineSelectWidget.parent.call( this, config );
17294
17295 // Mixin constructors
17296 OO.ui.mixin.TabIndexedElement.call( this, config );
17297
17298 // Events
17299 this.$element.on( {
17300 focus: this.bindKeyDownListener.bind( this ),
17301 blur: this.unbindKeyDownListener.bind( this )
17302 } );
17303
17304 // Initialization
17305 this.$element.addClass( 'oo-ui-outlineSelectWidget' );
17306 };
17307
17308 /* Setup */
17309
17310 OO.inheritClass( OO.ui.OutlineSelectWidget, OO.ui.SelectWidget );
17311 OO.mixinClass( OO.ui.OutlineSelectWidget, OO.ui.mixin.TabIndexedElement );
17312
17313 /**
17314 * TabSelectWidget is a list that contains {@link OO.ui.TabOptionWidget tab options}
17315 *
17316 * **Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}.**
17317 *
17318 * @class
17319 * @extends OO.ui.SelectWidget
17320 * @mixins OO.ui.mixin.TabIndexedElement
17321 *
17322 * @constructor
17323 * @param {Object} [config] Configuration options
17324 */
17325 OO.ui.TabSelectWidget = function OoUiTabSelectWidget( config ) {
17326 // Parent constructor
17327 OO.ui.TabSelectWidget.parent.call( this, config );
17328
17329 // Mixin constructors
17330 OO.ui.mixin.TabIndexedElement.call( this, config );
17331
17332 // Events
17333 this.$element.on( {
17334 focus: this.bindKeyDownListener.bind( this ),
17335 blur: this.unbindKeyDownListener.bind( this )
17336 } );
17337
17338 // Initialization
17339 this.$element.addClass( 'oo-ui-tabSelectWidget' );
17340 };
17341
17342 /* Setup */
17343
17344 OO.inheritClass( OO.ui.TabSelectWidget, OO.ui.SelectWidget );
17345 OO.mixinClass( OO.ui.TabSelectWidget, OO.ui.mixin.TabIndexedElement );
17346
17347 /**
17348 * NumberInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
17349 * can be entered manually) and two {@link OO.ui.ButtonWidget button widgets}
17350 * (to adjust the value in increments) to allow the user to enter a number.
17351 *
17352 * @example
17353 * // Example: A NumberInputWidget.
17354 * var numberInput = new OO.ui.NumberInputWidget( {
17355 * label: 'NumberInputWidget',
17356 * input: { value: 5, min: 1, max: 10 }
17357 * } );
17358 * $( 'body' ).append( numberInput.$element );
17359 *
17360 * @class
17361 * @extends OO.ui.Widget
17362 *
17363 * @constructor
17364 * @param {Object} [config] Configuration options
17365 * @cfg {Object} [input] Configuration options to pass to the {@link OO.ui.TextInputWidget text input widget}.
17366 * @cfg {Object} [minusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget decrementing button widget}.
17367 * @cfg {Object} [plusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget incrementing button widget}.
17368 * @cfg {boolean} [isInteger=false] Whether the field accepts only integer values.
17369 * @cfg {number} [min=-Infinity] Minimum allowed value
17370 * @cfg {number} [max=Infinity] Maximum allowed value
17371 * @cfg {number} [step=1] Delta when using the buttons or up/down arrow keys
17372 * @cfg {number|null} [pageStep] Delta when using the page-up/page-down keys. Defaults to 10 times #step.
17373 */
17374 OO.ui.NumberInputWidget = function OoUiNumberInputWidget( config ) {
17375 // Configuration initialization
17376 config = $.extend( {
17377 isInteger: false,
17378 min: -Infinity,
17379 max: Infinity,
17380 step: 1,
17381 pageStep: null
17382 }, config );
17383
17384 // Parent constructor
17385 OO.ui.NumberInputWidget.parent.call( this, config );
17386
17387 // Properties
17388 this.input = new OO.ui.TextInputWidget( $.extend(
17389 {
17390 disabled: this.isDisabled()
17391 },
17392 config.input
17393 ) );
17394 this.minusButton = new OO.ui.ButtonWidget( $.extend(
17395 {
17396 disabled: this.isDisabled(),
17397 tabIndex: -1
17398 },
17399 config.minusButton,
17400 {
17401 classes: [ 'oo-ui-numberInputWidget-minusButton' ],
17402 label: '−'
17403 }
17404 ) );
17405 this.plusButton = new OO.ui.ButtonWidget( $.extend(
17406 {
17407 disabled: this.isDisabled(),
17408 tabIndex: -1
17409 },
17410 config.plusButton,
17411 {
17412 classes: [ 'oo-ui-numberInputWidget-plusButton' ],
17413 label: '+'
17414 }
17415 ) );
17416
17417 // Events
17418 this.input.connect( this, {
17419 change: this.emit.bind( this, 'change' ),
17420 enter: this.emit.bind( this, 'enter' )
17421 } );
17422 this.input.$input.on( {
17423 keydown: this.onKeyDown.bind( this ),
17424 'wheel mousewheel DOMMouseScroll': this.onWheel.bind( this )
17425 } );
17426 this.plusButton.connect( this, {
17427 click: [ 'onButtonClick', +1 ]
17428 } );
17429 this.minusButton.connect( this, {
17430 click: [ 'onButtonClick', -1 ]
17431 } );
17432
17433 // Initialization
17434 this.setIsInteger( !!config.isInteger );
17435 this.setRange( config.min, config.max );
17436 this.setStep( config.step, config.pageStep );
17437
17438 this.$field = $( '<div>' ).addClass( 'oo-ui-numberInputWidget-field' )
17439 .append(
17440 this.minusButton.$element,
17441 this.input.$element,
17442 this.plusButton.$element
17443 );
17444 this.$element.addClass( 'oo-ui-numberInputWidget' ).append( this.$field );
17445 this.input.setValidation( this.validateNumber.bind( this ) );
17446 };
17447
17448 /* Setup */
17449
17450 OO.inheritClass( OO.ui.NumberInputWidget, OO.ui.Widget );
17451
17452 /* Events */
17453
17454 /**
17455 * A `change` event is emitted when the value of the input changes.
17456 *
17457 * @event change
17458 */
17459
17460 /**
17461 * An `enter` event is emitted when the user presses 'enter' inside the text box.
17462 *
17463 * @event enter
17464 */
17465
17466 /* Methods */
17467
17468 /**
17469 * Set whether only integers are allowed
17470 * @param {boolean} flag
17471 */
17472 OO.ui.NumberInputWidget.prototype.setIsInteger = function ( flag ) {
17473 this.isInteger = !!flag;
17474 this.input.setValidityFlag();
17475 };
17476
17477 /**
17478 * Get whether only integers are allowed
17479 * @return {boolean} Flag value
17480 */
17481 OO.ui.NumberInputWidget.prototype.getIsInteger = function () {
17482 return this.isInteger;
17483 };
17484
17485 /**
17486 * Set the range of allowed values
17487 * @param {number} min Minimum allowed value
17488 * @param {number} max Maximum allowed value
17489 */
17490 OO.ui.NumberInputWidget.prototype.setRange = function ( min, max ) {
17491 if ( min > max ) {
17492 throw new Error( 'Minimum (' + min + ') must not be greater than maximum (' + max + ')' );
17493 }
17494 this.min = min;
17495 this.max = max;
17496 this.input.setValidityFlag();
17497 };
17498
17499 /**
17500 * Get the current range
17501 * @return {number[]} Minimum and maximum values
17502 */
17503 OO.ui.NumberInputWidget.prototype.getRange = function () {
17504 return [ this.min, this.max ];
17505 };
17506
17507 /**
17508 * Set the stepping deltas
17509 * @param {number} step Normal step
17510 * @param {number|null} pageStep Page step. If null, 10 * step will be used.
17511 */
17512 OO.ui.NumberInputWidget.prototype.setStep = function ( step, pageStep ) {
17513 if ( step <= 0 ) {
17514 throw new Error( 'Step value must be positive' );
17515 }
17516 if ( pageStep === null ) {
17517 pageStep = step * 10;
17518 } else if ( pageStep <= 0 ) {
17519 throw new Error( 'Page step value must be positive' );
17520 }
17521 this.step = step;
17522 this.pageStep = pageStep;
17523 };
17524
17525 /**
17526 * Get the current stepping values
17527 * @return {number[]} Step and page step
17528 */
17529 OO.ui.NumberInputWidget.prototype.getStep = function () {
17530 return [ this.step, this.pageStep ];
17531 };
17532
17533 /**
17534 * Get the current value of the widget
17535 * @return {string}
17536 */
17537 OO.ui.NumberInputWidget.prototype.getValue = function () {
17538 return this.input.getValue();
17539 };
17540
17541 /**
17542 * Get the current value of the widget as a number
17543 * @return {number} May be NaN, or an invalid number
17544 */
17545 OO.ui.NumberInputWidget.prototype.getNumericValue = function () {
17546 return +this.input.getValue();
17547 };
17548
17549 /**
17550 * Set the value of the widget
17551 * @param {string} value Invalid values are allowed
17552 */
17553 OO.ui.NumberInputWidget.prototype.setValue = function ( value ) {
17554 this.input.setValue( value );
17555 };
17556
17557 /**
17558 * Adjust the value of the widget
17559 * @param {number} delta Adjustment amount
17560 */
17561 OO.ui.NumberInputWidget.prototype.adjustValue = function ( delta ) {
17562 var n, v = this.getNumericValue();
17563
17564 delta = +delta;
17565 if ( isNaN( delta ) || !isFinite( delta ) ) {
17566 throw new Error( 'Delta must be a finite number' );
17567 }
17568
17569 if ( isNaN( v ) ) {
17570 n = 0;
17571 } else {
17572 n = v + delta;
17573 n = Math.max( Math.min( n, this.max ), this.min );
17574 if ( this.isInteger ) {
17575 n = Math.round( n );
17576 }
17577 }
17578
17579 if ( n !== v ) {
17580 this.setValue( n );
17581 }
17582 };
17583
17584 /**
17585 * Validate input
17586 * @private
17587 * @param {string} value Field value
17588 * @return {boolean}
17589 */
17590 OO.ui.NumberInputWidget.prototype.validateNumber = function ( value ) {
17591 var n = +value;
17592 if ( isNaN( n ) || !isFinite( n ) ) {
17593 return false;
17594 }
17595
17596 /*jshint bitwise: false */
17597 if ( this.isInteger && ( n | 0 ) !== n ) {
17598 return false;
17599 }
17600 /*jshint bitwise: true */
17601
17602 if ( n < this.min || n > this.max ) {
17603 return false;
17604 }
17605
17606 return true;
17607 };
17608
17609 /**
17610 * Handle mouse click events.
17611 *
17612 * @private
17613 * @param {number} dir +1 or -1
17614 */
17615 OO.ui.NumberInputWidget.prototype.onButtonClick = function ( dir ) {
17616 this.adjustValue( dir * this.step );
17617 };
17618
17619 /**
17620 * Handle mouse wheel events.
17621 *
17622 * @private
17623 * @param {jQuery.Event} event
17624 */
17625 OO.ui.NumberInputWidget.prototype.onWheel = function ( event ) {
17626 var delta = 0;
17627
17628 // Standard 'wheel' event
17629 if ( event.originalEvent.deltaMode !== undefined ) {
17630 this.sawWheelEvent = true;
17631 }
17632 if ( event.originalEvent.deltaY ) {
17633 delta = -event.originalEvent.deltaY;
17634 } else if ( event.originalEvent.deltaX ) {
17635 delta = event.originalEvent.deltaX;
17636 }
17637
17638 // Non-standard events
17639 if ( !this.sawWheelEvent ) {
17640 if ( event.originalEvent.wheelDeltaX ) {
17641 delta = -event.originalEvent.wheelDeltaX;
17642 } else if ( event.originalEvent.wheelDeltaY ) {
17643 delta = event.originalEvent.wheelDeltaY;
17644 } else if ( event.originalEvent.wheelDelta ) {
17645 delta = event.originalEvent.wheelDelta;
17646 } else if ( event.originalEvent.detail ) {
17647 delta = -event.originalEvent.detail;
17648 }
17649 }
17650
17651 if ( delta ) {
17652 delta = delta < 0 ? -1 : 1;
17653 this.adjustValue( delta * this.step );
17654 }
17655
17656 return false;
17657 };
17658
17659 /**
17660 * Handle key down events.
17661 *
17662 *
17663 * @private
17664 * @param {jQuery.Event} e Key down event
17665 */
17666 OO.ui.NumberInputWidget.prototype.onKeyDown = function ( e ) {
17667 if ( !this.isDisabled() ) {
17668 switch ( e.which ) {
17669 case OO.ui.Keys.UP:
17670 this.adjustValue( this.step );
17671 return false;
17672 case OO.ui.Keys.DOWN:
17673 this.adjustValue( -this.step );
17674 return false;
17675 case OO.ui.Keys.PAGEUP:
17676 this.adjustValue( this.pageStep );
17677 return false;
17678 case OO.ui.Keys.PAGEDOWN:
17679 this.adjustValue( -this.pageStep );
17680 return false;
17681 }
17682 }
17683 };
17684
17685 /**
17686 * @inheritdoc
17687 */
17688 OO.ui.NumberInputWidget.prototype.setDisabled = function ( disabled ) {
17689 // Parent method
17690 OO.ui.NumberInputWidget.parent.prototype.setDisabled.call( this, disabled );
17691
17692 if ( this.input ) {
17693 this.input.setDisabled( this.isDisabled() );
17694 }
17695 if ( this.minusButton ) {
17696 this.minusButton.setDisabled( this.isDisabled() );
17697 }
17698 if ( this.plusButton ) {
17699 this.plusButton.setDisabled( this.isDisabled() );
17700 }
17701
17702 return this;
17703 };
17704
17705 /**
17706 * ToggleSwitches are switches that slide on and off. Their state is represented by a Boolean
17707 * value (`true` for ‘on’, and `false` otherwise, the default). The ‘off’ state is represented
17708 * visually by a slider in the leftmost position.
17709 *
17710 * @example
17711 * // Toggle switches in the 'off' and 'on' position.
17712 * var toggleSwitch1 = new OO.ui.ToggleSwitchWidget();
17713 * var toggleSwitch2 = new OO.ui.ToggleSwitchWidget( {
17714 * value: true
17715 * } );
17716 *
17717 * // Create a FieldsetLayout to layout and label switches
17718 * var fieldset = new OO.ui.FieldsetLayout( {
17719 * label: 'Toggle switches'
17720 * } );
17721 * fieldset.addItems( [
17722 * new OO.ui.FieldLayout( toggleSwitch1, { label: 'Off', align: 'top' } ),
17723 * new OO.ui.FieldLayout( toggleSwitch2, { label: 'On', align: 'top' } )
17724 * ] );
17725 * $( 'body' ).append( fieldset.$element );
17726 *
17727 * @class
17728 * @extends OO.ui.ToggleWidget
17729 * @mixins OO.ui.mixin.TabIndexedElement
17730 *
17731 * @constructor
17732 * @param {Object} [config] Configuration options
17733 * @cfg {boolean} [value=false] The toggle switch’s initial on/off state.
17734 * By default, the toggle switch is in the 'off' position.
17735 */
17736 OO.ui.ToggleSwitchWidget = function OoUiToggleSwitchWidget( config ) {
17737 // Parent constructor
17738 OO.ui.ToggleSwitchWidget.parent.call( this, config );
17739
17740 // Mixin constructors
17741 OO.ui.mixin.TabIndexedElement.call( this, config );
17742
17743 // Properties
17744 this.dragging = false;
17745 this.dragStart = null;
17746 this.sliding = false;
17747 this.$glow = $( '<span>' );
17748 this.$grip = $( '<span>' );
17749
17750 // Events
17751 this.$element.on( {
17752 click: this.onClick.bind( this ),
17753 keypress: this.onKeyPress.bind( this )
17754 } );
17755
17756 // Initialization
17757 this.$glow.addClass( 'oo-ui-toggleSwitchWidget-glow' );
17758 this.$grip.addClass( 'oo-ui-toggleSwitchWidget-grip' );
17759 this.$element
17760 .addClass( 'oo-ui-toggleSwitchWidget' )
17761 .attr( 'role', 'checkbox' )
17762 .append( this.$glow, this.$grip );
17763 };
17764
17765 /* Setup */
17766
17767 OO.inheritClass( OO.ui.ToggleSwitchWidget, OO.ui.ToggleWidget );
17768 OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.mixin.TabIndexedElement );
17769
17770 /* Methods */
17771
17772 /**
17773 * Handle mouse click events.
17774 *
17775 * @private
17776 * @param {jQuery.Event} e Mouse click event
17777 */
17778 OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
17779 if ( !this.isDisabled() && e.which === 1 ) {
17780 this.setValue( !this.value );
17781 }
17782 return false;
17783 };
17784
17785 /**
17786 * Handle key press events.
17787 *
17788 * @private
17789 * @param {jQuery.Event} e Key press event
17790 */
17791 OO.ui.ToggleSwitchWidget.prototype.onKeyPress = function ( e ) {
17792 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
17793 this.setValue( !this.value );
17794 return false;
17795 }
17796 };
17797
17798 /*!
17799 * Deprecated aliases for classes in the `OO.ui.mixin` namespace.
17800 */
17801
17802 /**
17803 * @inheritdoc OO.ui.mixin.ButtonElement
17804 * @deprecated Use {@link OO.ui.mixin.ButtonElement} instead.
17805 */
17806 OO.ui.ButtonElement = OO.ui.mixin.ButtonElement;
17807
17808 /**
17809 * @inheritdoc OO.ui.mixin.ClippableElement
17810 * @deprecated Use {@link OO.ui.mixin.ClippableElement} instead.
17811 */
17812 OO.ui.ClippableElement = OO.ui.mixin.ClippableElement;
17813
17814 /**
17815 * @inheritdoc OO.ui.mixin.DraggableElement
17816 * @deprecated Use {@link OO.ui.mixin.DraggableElement} instead.
17817 */
17818 OO.ui.DraggableElement = OO.ui.mixin.DraggableElement;
17819
17820 /**
17821 * @inheritdoc OO.ui.mixin.DraggableGroupElement
17822 * @deprecated Use {@link OO.ui.mixin.DraggableGroupElement} instead.
17823 */
17824 OO.ui.DraggableGroupElement = OO.ui.mixin.DraggableGroupElement;
17825
17826 /**
17827 * @inheritdoc OO.ui.mixin.FlaggedElement
17828 * @deprecated Use {@link OO.ui.mixin.FlaggedElement} instead.
17829 */
17830 OO.ui.FlaggedElement = OO.ui.mixin.FlaggedElement;
17831
17832 /**
17833 * @inheritdoc OO.ui.mixin.GroupElement
17834 * @deprecated Use {@link OO.ui.mixin.GroupElement} instead.
17835 */
17836 OO.ui.GroupElement = OO.ui.mixin.GroupElement;
17837
17838 /**
17839 * @inheritdoc OO.ui.mixin.GroupWidget
17840 * @deprecated Use {@link OO.ui.mixin.GroupWidget} instead.
17841 */
17842 OO.ui.GroupWidget = OO.ui.mixin.GroupWidget;
17843
17844 /**
17845 * @inheritdoc OO.ui.mixin.IconElement
17846 * @deprecated Use {@link OO.ui.mixin.IconElement} instead.
17847 */
17848 OO.ui.IconElement = OO.ui.mixin.IconElement;
17849
17850 /**
17851 * @inheritdoc OO.ui.mixin.IndicatorElement
17852 * @deprecated Use {@link OO.ui.mixin.IndicatorElement} instead.
17853 */
17854 OO.ui.IndicatorElement = OO.ui.mixin.IndicatorElement;
17855
17856 /**
17857 * @inheritdoc OO.ui.mixin.ItemWidget
17858 * @deprecated Use {@link OO.ui.mixin.ItemWidget} instead.
17859 */
17860 OO.ui.ItemWidget = OO.ui.mixin.ItemWidget;
17861
17862 /**
17863 * @inheritdoc OO.ui.mixin.LabelElement
17864 * @deprecated Use {@link OO.ui.mixin.LabelElement} instead.
17865 */
17866 OO.ui.LabelElement = OO.ui.mixin.LabelElement;
17867
17868 /**
17869 * @inheritdoc OO.ui.mixin.LookupElement
17870 * @deprecated Use {@link OO.ui.mixin.LookupElement} instead.
17871 */
17872 OO.ui.LookupElement = OO.ui.mixin.LookupElement;
17873
17874 /**
17875 * @inheritdoc OO.ui.mixin.PendingElement
17876 * @deprecated Use {@link OO.ui.mixin.PendingElement} instead.
17877 */
17878 OO.ui.PendingElement = OO.ui.mixin.PendingElement;
17879
17880 /**
17881 * @inheritdoc OO.ui.mixin.PopupElement
17882 * @deprecated Use {@link OO.ui.mixin.PopupElement} instead.
17883 */
17884 OO.ui.PopupElement = OO.ui.mixin.PopupElement;
17885
17886 /**
17887 * @inheritdoc OO.ui.mixin.TabIndexedElement
17888 * @deprecated Use {@link OO.ui.mixin.TabIndexedElement} instead.
17889 */
17890 OO.ui.TabIndexedElement = OO.ui.mixin.TabIndexedElement;
17891
17892 /**
17893 * @inheritdoc OO.ui.mixin.TitledElement
17894 * @deprecated Use {@link OO.ui.mixin.TitledElement} instead.
17895 */
17896 OO.ui.TitledElement = OO.ui.mixin.TitledElement;
17897
17898 }( OO ) );