Update OOjs UI to v0.12.0
[lhc/web/wiklou.git] / resources / lib / oojs-ui / oojs-ui.js
1 /*!
2 * OOjs UI v0.12.0
3 * https://www.mediawiki.org/wiki/OOjs_UI
4 *
5 * Copyright 2011–2015 OOjs Team and other contributors.
6 * Released under the MIT license
7 * http://oojs.mit-license.org
8 *
9 * Date: 2015-07-13T23:47:04Z
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, parts, parent, 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 parts = data._.split( '.' );
1136 cls = OO.getProp.apply( OO, [ window ].concat( parts ) );
1137 if ( cls === undefined ) {
1138 // The PHP output might be old and not including the "OO.ui" prefix
1139 // TODO: Remove this back-compat after next major release
1140 cls = OO.getProp.apply( OO, [ OO.ui ].concat( parts ) );
1141 if ( cls === undefined ) {
1142 throw new Error( 'Unknown widget type: id: ' + id + ', class: ' + data._ );
1143 }
1144 }
1145
1146 // Verify that we're creating an OO.ui.Element instance
1147 parent = cls.parent;
1148
1149 while ( parent !== undefined ) {
1150 if ( parent === OO.ui.Element ) {
1151 // Safe
1152 break;
1153 }
1154
1155 parent = parent.parent;
1156 }
1157
1158 if ( parent !== OO.ui.Element ) {
1159 throw new Error( 'Unknown widget type: id: ' + id + ', class: ' + data._ );
1160 }
1161
1162 $elem.data( 'ooui-infused', true ); // prevent loops
1163 data.id = id; // implicit
1164 data = OO.copy( data, null, function deserialize( value ) {
1165 if ( OO.isPlainObject( value ) ) {
1166 if ( value.tag ) {
1167 return OO.ui.Element.static.unsafeInfuse( value.tag, false );
1168 }
1169 if ( value.html ) {
1170 return new OO.ui.HtmlSnippet( value.html );
1171 }
1172 }
1173 } );
1174 // jscs:disable requireCapitalizedConstructors
1175 obj = new cls( data ); // rebuild widget
1176 // now replace old DOM with this new DOM.
1177 if ( top ) {
1178 $elem.replaceWith( obj.$element );
1179 }
1180 obj.$element.data( 'ooui-infused', obj );
1181 // set the 'data-ooui' attribute so we can identify infused widgets
1182 obj.$element.attr( 'data-ooui', '' );
1183 return obj;
1184 };
1185
1186 /**
1187 * Get a jQuery function within a specific document.
1188 *
1189 * @static
1190 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
1191 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
1192 * not in an iframe
1193 * @return {Function} Bound jQuery function
1194 */
1195 OO.ui.Element.static.getJQuery = function ( context, $iframe ) {
1196 function wrapper( selector ) {
1197 return $( selector, wrapper.context );
1198 }
1199
1200 wrapper.context = this.getDocument( context );
1201
1202 if ( $iframe ) {
1203 wrapper.$iframe = $iframe;
1204 }
1205
1206 return wrapper;
1207 };
1208
1209 /**
1210 * Get the document of an element.
1211 *
1212 * @static
1213 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
1214 * @return {HTMLDocument|null} Document object
1215 */
1216 OO.ui.Element.static.getDocument = function ( obj ) {
1217 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
1218 return ( obj[ 0 ] && obj[ 0 ].ownerDocument ) ||
1219 // Empty jQuery selections might have a context
1220 obj.context ||
1221 // HTMLElement
1222 obj.ownerDocument ||
1223 // Window
1224 obj.document ||
1225 // HTMLDocument
1226 ( obj.nodeType === 9 && obj ) ||
1227 null;
1228 };
1229
1230 /**
1231 * Get the window of an element or document.
1232 *
1233 * @static
1234 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
1235 * @return {Window} Window object
1236 */
1237 OO.ui.Element.static.getWindow = function ( obj ) {
1238 var doc = this.getDocument( obj );
1239 return doc.parentWindow || doc.defaultView;
1240 };
1241
1242 /**
1243 * Get the direction of an element or document.
1244 *
1245 * @static
1246 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
1247 * @return {string} Text direction, either 'ltr' or 'rtl'
1248 */
1249 OO.ui.Element.static.getDir = function ( obj ) {
1250 var isDoc, isWin;
1251
1252 if ( obj instanceof jQuery ) {
1253 obj = obj[ 0 ];
1254 }
1255 isDoc = obj.nodeType === 9;
1256 isWin = obj.document !== undefined;
1257 if ( isDoc || isWin ) {
1258 if ( isWin ) {
1259 obj = obj.document;
1260 }
1261 obj = obj.body;
1262 }
1263 return $( obj ).css( 'direction' );
1264 };
1265
1266 /**
1267 * Get the offset between two frames.
1268 *
1269 * TODO: Make this function not use recursion.
1270 *
1271 * @static
1272 * @param {Window} from Window of the child frame
1273 * @param {Window} [to=window] Window of the parent frame
1274 * @param {Object} [offset] Offset to start with, used internally
1275 * @return {Object} Offset object, containing left and top properties
1276 */
1277 OO.ui.Element.static.getFrameOffset = function ( from, to, offset ) {
1278 var i, len, frames, frame, rect;
1279
1280 if ( !to ) {
1281 to = window;
1282 }
1283 if ( !offset ) {
1284 offset = { top: 0, left: 0 };
1285 }
1286 if ( from.parent === from ) {
1287 return offset;
1288 }
1289
1290 // Get iframe element
1291 frames = from.parent.document.getElementsByTagName( 'iframe' );
1292 for ( i = 0, len = frames.length; i < len; i++ ) {
1293 if ( frames[ i ].contentWindow === from ) {
1294 frame = frames[ i ];
1295 break;
1296 }
1297 }
1298
1299 // Recursively accumulate offset values
1300 if ( frame ) {
1301 rect = frame.getBoundingClientRect();
1302 offset.left += rect.left;
1303 offset.top += rect.top;
1304 if ( from !== to ) {
1305 this.getFrameOffset( from.parent, offset );
1306 }
1307 }
1308 return offset;
1309 };
1310
1311 /**
1312 * Get the offset between two elements.
1313 *
1314 * The two elements may be in a different frame, but in that case the frame $element is in must
1315 * be contained in the frame $anchor is in.
1316 *
1317 * @static
1318 * @param {jQuery} $element Element whose position to get
1319 * @param {jQuery} $anchor Element to get $element's position relative to
1320 * @return {Object} Translated position coordinates, containing top and left properties
1321 */
1322 OO.ui.Element.static.getRelativePosition = function ( $element, $anchor ) {
1323 var iframe, iframePos,
1324 pos = $element.offset(),
1325 anchorPos = $anchor.offset(),
1326 elementDocument = this.getDocument( $element ),
1327 anchorDocument = this.getDocument( $anchor );
1328
1329 // If $element isn't in the same document as $anchor, traverse up
1330 while ( elementDocument !== anchorDocument ) {
1331 iframe = elementDocument.defaultView.frameElement;
1332 if ( !iframe ) {
1333 throw new Error( '$element frame is not contained in $anchor frame' );
1334 }
1335 iframePos = $( iframe ).offset();
1336 pos.left += iframePos.left;
1337 pos.top += iframePos.top;
1338 elementDocument = iframe.ownerDocument;
1339 }
1340 pos.left -= anchorPos.left;
1341 pos.top -= anchorPos.top;
1342 return pos;
1343 };
1344
1345 /**
1346 * Get element border sizes.
1347 *
1348 * @static
1349 * @param {HTMLElement} el Element to measure
1350 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1351 */
1352 OO.ui.Element.static.getBorders = function ( el ) {
1353 var doc = el.ownerDocument,
1354 win = doc.parentWindow || doc.defaultView,
1355 style = win && win.getComputedStyle ?
1356 win.getComputedStyle( el, null ) :
1357 el.currentStyle,
1358 $el = $( el ),
1359 top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0,
1360 left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0,
1361 bottom = parseFloat( style ? style.borderBottomWidth : $el.css( 'borderBottomWidth' ) ) || 0,
1362 right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0;
1363
1364 return {
1365 top: top,
1366 left: left,
1367 bottom: bottom,
1368 right: right
1369 };
1370 };
1371
1372 /**
1373 * Get dimensions of an element or window.
1374 *
1375 * @static
1376 * @param {HTMLElement|Window} el Element to measure
1377 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1378 */
1379 OO.ui.Element.static.getDimensions = function ( el ) {
1380 var $el, $win,
1381 doc = el.ownerDocument || el.document,
1382 win = doc.parentWindow || doc.defaultView;
1383
1384 if ( win === el || el === doc.documentElement ) {
1385 $win = $( win );
1386 return {
1387 borders: { top: 0, left: 0, bottom: 0, right: 0 },
1388 scroll: {
1389 top: $win.scrollTop(),
1390 left: $win.scrollLeft()
1391 },
1392 scrollbar: { right: 0, bottom: 0 },
1393 rect: {
1394 top: 0,
1395 left: 0,
1396 bottom: $win.innerHeight(),
1397 right: $win.innerWidth()
1398 }
1399 };
1400 } else {
1401 $el = $( el );
1402 return {
1403 borders: this.getBorders( el ),
1404 scroll: {
1405 top: $el.scrollTop(),
1406 left: $el.scrollLeft()
1407 },
1408 scrollbar: {
1409 right: $el.innerWidth() - el.clientWidth,
1410 bottom: $el.innerHeight() - el.clientHeight
1411 },
1412 rect: el.getBoundingClientRect()
1413 };
1414 }
1415 };
1416
1417 /**
1418 * Get scrollable object parent
1419 *
1420 * documentElement can't be used to get or set the scrollTop
1421 * property on Blink. Changing and testing its value lets us
1422 * use 'body' or 'documentElement' based on what is working.
1423 *
1424 * https://code.google.com/p/chromium/issues/detail?id=303131
1425 *
1426 * @static
1427 * @param {HTMLElement} el Element to find scrollable parent for
1428 * @return {HTMLElement} Scrollable parent
1429 */
1430 OO.ui.Element.static.getRootScrollableElement = function ( el ) {
1431 var scrollTop, body;
1432
1433 if ( OO.ui.scrollableElement === undefined ) {
1434 body = el.ownerDocument.body;
1435 scrollTop = body.scrollTop;
1436 body.scrollTop = 1;
1437
1438 if ( body.scrollTop === 1 ) {
1439 body.scrollTop = scrollTop;
1440 OO.ui.scrollableElement = 'body';
1441 } else {
1442 OO.ui.scrollableElement = 'documentElement';
1443 }
1444 }
1445
1446 return el.ownerDocument[ OO.ui.scrollableElement ];
1447 };
1448
1449 /**
1450 * Get closest scrollable container.
1451 *
1452 * Traverses up until either a scrollable element or the root is reached, in which case the window
1453 * will be returned.
1454 *
1455 * @static
1456 * @param {HTMLElement} el Element to find scrollable container for
1457 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1458 * @return {HTMLElement} Closest scrollable container
1459 */
1460 OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) {
1461 var i, val,
1462 // props = [ 'overflow' ] doesn't work due to https://bugzilla.mozilla.org/show_bug.cgi?id=889091
1463 props = [ 'overflow-x', 'overflow-y' ],
1464 $parent = $( el ).parent();
1465
1466 if ( dimension === 'x' || dimension === 'y' ) {
1467 props = [ 'overflow-' + dimension ];
1468 }
1469
1470 while ( $parent.length ) {
1471 if ( $parent[ 0 ] === this.getRootScrollableElement( el ) ) {
1472 return $parent[ 0 ];
1473 }
1474 i = props.length;
1475 while ( i-- ) {
1476 val = $parent.css( props[ i ] );
1477 if ( val === 'auto' || val === 'scroll' ) {
1478 return $parent[ 0 ];
1479 }
1480 }
1481 $parent = $parent.parent();
1482 }
1483 return this.getDocument( el ).body;
1484 };
1485
1486 /**
1487 * Scroll element into view.
1488 *
1489 * @static
1490 * @param {HTMLElement} el Element to scroll into view
1491 * @param {Object} [config] Configuration options
1492 * @param {string} [config.duration] jQuery animation duration value
1493 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1494 * to scroll in both directions
1495 * @param {Function} [config.complete] Function to call when scrolling completes
1496 */
1497 OO.ui.Element.static.scrollIntoView = function ( el, config ) {
1498 // Configuration initialization
1499 config = config || {};
1500
1501 var rel, anim = {},
1502 callback = typeof config.complete === 'function' && config.complete,
1503 sc = this.getClosestScrollableContainer( el, config.direction ),
1504 $sc = $( sc ),
1505 eld = this.getDimensions( el ),
1506 scd = this.getDimensions( sc ),
1507 $win = $( this.getWindow( el ) );
1508
1509 // Compute the distances between the edges of el and the edges of the scroll viewport
1510 if ( $sc.is( 'html, body' ) ) {
1511 // If the scrollable container is the root, this is easy
1512 rel = {
1513 top: eld.rect.top,
1514 bottom: $win.innerHeight() - eld.rect.bottom,
1515 left: eld.rect.left,
1516 right: $win.innerWidth() - eld.rect.right
1517 };
1518 } else {
1519 // Otherwise, we have to subtract el's coordinates from sc's coordinates
1520 rel = {
1521 top: eld.rect.top - ( scd.rect.top + scd.borders.top ),
1522 bottom: scd.rect.bottom - scd.borders.bottom - scd.scrollbar.bottom - eld.rect.bottom,
1523 left: eld.rect.left - ( scd.rect.left + scd.borders.left ),
1524 right: scd.rect.right - scd.borders.right - scd.scrollbar.right - eld.rect.right
1525 };
1526 }
1527
1528 if ( !config.direction || config.direction === 'y' ) {
1529 if ( rel.top < 0 ) {
1530 anim.scrollTop = scd.scroll.top + rel.top;
1531 } else if ( rel.top > 0 && rel.bottom < 0 ) {
1532 anim.scrollTop = scd.scroll.top + Math.min( rel.top, -rel.bottom );
1533 }
1534 }
1535 if ( !config.direction || config.direction === 'x' ) {
1536 if ( rel.left < 0 ) {
1537 anim.scrollLeft = scd.scroll.left + rel.left;
1538 } else if ( rel.left > 0 && rel.right < 0 ) {
1539 anim.scrollLeft = scd.scroll.left + Math.min( rel.left, -rel.right );
1540 }
1541 }
1542 if ( !$.isEmptyObject( anim ) ) {
1543 $sc.stop( true ).animate( anim, config.duration || 'fast' );
1544 if ( callback ) {
1545 $sc.queue( function ( next ) {
1546 callback();
1547 next();
1548 } );
1549 }
1550 } else {
1551 if ( callback ) {
1552 callback();
1553 }
1554 }
1555 };
1556
1557 /**
1558 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1559 * and reserve space for them, because it probably doesn't.
1560 *
1561 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1562 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1563 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1564 * and then reattach (or show) them back.
1565 *
1566 * @static
1567 * @param {HTMLElement} el Element to reconsider the scrollbars on
1568 */
1569 OO.ui.Element.static.reconsiderScrollbars = function ( el ) {
1570 var i, len, scrollLeft, scrollTop, nodes = [];
1571 // Save scroll position
1572 scrollLeft = el.scrollLeft;
1573 scrollTop = el.scrollTop;
1574 // Detach all children
1575 while ( el.firstChild ) {
1576 nodes.push( el.firstChild );
1577 el.removeChild( el.firstChild );
1578 }
1579 // Force reflow
1580 void el.offsetHeight;
1581 // Reattach all children
1582 for ( i = 0, len = nodes.length; i < len; i++ ) {
1583 el.appendChild( nodes[ i ] );
1584 }
1585 // Restore scroll position (no-op if scrollbars disappeared)
1586 el.scrollLeft = scrollLeft;
1587 el.scrollTop = scrollTop;
1588 };
1589
1590 /* Methods */
1591
1592 /**
1593 * Toggle visibility of an element.
1594 *
1595 * @param {boolean} [show] Make element visible, omit to toggle visibility
1596 * @fires visible
1597 * @chainable
1598 */
1599 OO.ui.Element.prototype.toggle = function ( show ) {
1600 show = show === undefined ? !this.visible : !!show;
1601
1602 if ( show !== this.isVisible() ) {
1603 this.visible = show;
1604 this.$element.toggleClass( 'oo-ui-element-hidden', !this.visible );
1605 this.emit( 'toggle', show );
1606 }
1607
1608 return this;
1609 };
1610
1611 /**
1612 * Check if element is visible.
1613 *
1614 * @return {boolean} element is visible
1615 */
1616 OO.ui.Element.prototype.isVisible = function () {
1617 return this.visible;
1618 };
1619
1620 /**
1621 * Get element data.
1622 *
1623 * @return {Mixed} Element data
1624 */
1625 OO.ui.Element.prototype.getData = function () {
1626 return this.data;
1627 };
1628
1629 /**
1630 * Set element data.
1631 *
1632 * @param {Mixed} Element data
1633 * @chainable
1634 */
1635 OO.ui.Element.prototype.setData = function ( data ) {
1636 this.data = data;
1637 return this;
1638 };
1639
1640 /**
1641 * Check if element supports one or more methods.
1642 *
1643 * @param {string|string[]} methods Method or list of methods to check
1644 * @return {boolean} All methods are supported
1645 */
1646 OO.ui.Element.prototype.supports = function ( methods ) {
1647 var i, len,
1648 support = 0;
1649
1650 methods = Array.isArray( methods ) ? methods : [ methods ];
1651 for ( i = 0, len = methods.length; i < len; i++ ) {
1652 if ( $.isFunction( this[ methods[ i ] ] ) ) {
1653 support++;
1654 }
1655 }
1656
1657 return methods.length === support;
1658 };
1659
1660 /**
1661 * Update the theme-provided classes.
1662 *
1663 * @localdoc This is called in element mixins and widget classes any time state changes.
1664 * Updating is debounced, minimizing overhead of changing multiple attributes and
1665 * guaranteeing that theme updates do not occur within an element's constructor
1666 */
1667 OO.ui.Element.prototype.updateThemeClasses = function () {
1668 if ( !this.updateThemeClassesPending ) {
1669 this.updateThemeClassesPending = true;
1670 setTimeout( this.debouncedUpdateThemeClassesHandler );
1671 }
1672 };
1673
1674 /**
1675 * @private
1676 */
1677 OO.ui.Element.prototype.debouncedUpdateThemeClasses = function () {
1678 OO.ui.theme.updateElementClasses( this );
1679 this.updateThemeClassesPending = false;
1680 };
1681
1682 /**
1683 * Get the HTML tag name.
1684 *
1685 * Override this method to base the result on instance information.
1686 *
1687 * @return {string} HTML tag name
1688 */
1689 OO.ui.Element.prototype.getTagName = function () {
1690 return this.constructor.static.tagName;
1691 };
1692
1693 /**
1694 * Check if the element is attached to the DOM
1695 * @return {boolean} The element is attached to the DOM
1696 */
1697 OO.ui.Element.prototype.isElementAttached = function () {
1698 return $.contains( this.getElementDocument(), this.$element[ 0 ] );
1699 };
1700
1701 /**
1702 * Get the DOM document.
1703 *
1704 * @return {HTMLDocument} Document object
1705 */
1706 OO.ui.Element.prototype.getElementDocument = function () {
1707 // Don't cache this in other ways either because subclasses could can change this.$element
1708 return OO.ui.Element.static.getDocument( this.$element );
1709 };
1710
1711 /**
1712 * Get the DOM window.
1713 *
1714 * @return {Window} Window object
1715 */
1716 OO.ui.Element.prototype.getElementWindow = function () {
1717 return OO.ui.Element.static.getWindow( this.$element );
1718 };
1719
1720 /**
1721 * Get closest scrollable container.
1722 */
1723 OO.ui.Element.prototype.getClosestScrollableElementContainer = function () {
1724 return OO.ui.Element.static.getClosestScrollableContainer( this.$element[ 0 ] );
1725 };
1726
1727 /**
1728 * Get group element is in.
1729 *
1730 * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1731 */
1732 OO.ui.Element.prototype.getElementGroup = function () {
1733 return this.elementGroup;
1734 };
1735
1736 /**
1737 * Set group element is in.
1738 *
1739 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1740 * @chainable
1741 */
1742 OO.ui.Element.prototype.setElementGroup = function ( group ) {
1743 this.elementGroup = group;
1744 return this;
1745 };
1746
1747 /**
1748 * Scroll element into view.
1749 *
1750 * @param {Object} [config] Configuration options
1751 */
1752 OO.ui.Element.prototype.scrollElementIntoView = function ( config ) {
1753 return OO.ui.Element.static.scrollIntoView( this.$element[ 0 ], config );
1754 };
1755
1756 /**
1757 * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way
1758 * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined.
1759 * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout},
1760 * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1761 * and {@link OO.ui.BookletLayout BookletLayout} for more information and examples.
1762 *
1763 * @abstract
1764 * @class
1765 * @extends OO.ui.Element
1766 * @mixins OO.EventEmitter
1767 *
1768 * @constructor
1769 * @param {Object} [config] Configuration options
1770 */
1771 OO.ui.Layout = function OoUiLayout( config ) {
1772 // Configuration initialization
1773 config = config || {};
1774
1775 // Parent constructor
1776 OO.ui.Layout.parent.call( this, config );
1777
1778 // Mixin constructors
1779 OO.EventEmitter.call( this );
1780
1781 // Initialization
1782 this.$element.addClass( 'oo-ui-layout' );
1783 };
1784
1785 /* Setup */
1786
1787 OO.inheritClass( OO.ui.Layout, OO.ui.Element );
1788 OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
1789
1790 /**
1791 * Widgets are compositions of one or more OOjs UI elements that users can both view
1792 * and interact with. All widgets can be configured and modified via a standard API,
1793 * and their state can change dynamically according to a model.
1794 *
1795 * @abstract
1796 * @class
1797 * @extends OO.ui.Element
1798 * @mixins OO.EventEmitter
1799 *
1800 * @constructor
1801 * @param {Object} [config] Configuration options
1802 * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1803 * appearance reflects this state.
1804 */
1805 OO.ui.Widget = function OoUiWidget( config ) {
1806 // Initialize config
1807 config = $.extend( { disabled: false }, config );
1808
1809 // Parent constructor
1810 OO.ui.Widget.parent.call( this, config );
1811
1812 // Mixin constructors
1813 OO.EventEmitter.call( this );
1814
1815 // Properties
1816 this.disabled = null;
1817 this.wasDisabled = null;
1818
1819 // Initialization
1820 this.$element.addClass( 'oo-ui-widget' );
1821 this.setDisabled( !!config.disabled );
1822 };
1823
1824 /* Setup */
1825
1826 OO.inheritClass( OO.ui.Widget, OO.ui.Element );
1827 OO.mixinClass( OO.ui.Widget, OO.EventEmitter );
1828
1829 /* Static Properties */
1830
1831 /**
1832 * Whether this widget will behave reasonably when wrapped in a HTML `<label>`. If this is true,
1833 * wrappers such as OO.ui.FieldLayout may use a `<label>` instead of implementing own label click
1834 * handling.
1835 *
1836 * @static
1837 * @inheritable
1838 * @property {boolean}
1839 */
1840 OO.ui.Widget.static.supportsSimpleLabel = false;
1841
1842 /* Events */
1843
1844 /**
1845 * @event disable
1846 *
1847 * A 'disable' event is emitted when a widget is disabled.
1848 *
1849 * @param {boolean} disabled Widget is disabled
1850 */
1851
1852 /**
1853 * @event toggle
1854 *
1855 * A 'toggle' event is emitted when the visibility of the widget changes.
1856 *
1857 * @param {boolean} visible Widget is visible
1858 */
1859
1860 /* Methods */
1861
1862 /**
1863 * Check if the widget is disabled.
1864 *
1865 * @return {boolean} Widget is disabled
1866 */
1867 OO.ui.Widget.prototype.isDisabled = function () {
1868 return this.disabled;
1869 };
1870
1871 /**
1872 * Set the 'disabled' state of the widget.
1873 *
1874 * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1875 *
1876 * @param {boolean} disabled Disable widget
1877 * @chainable
1878 */
1879 OO.ui.Widget.prototype.setDisabled = function ( disabled ) {
1880 var isDisabled;
1881
1882 this.disabled = !!disabled;
1883 isDisabled = this.isDisabled();
1884 if ( isDisabled !== this.wasDisabled ) {
1885 this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled );
1886 this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled );
1887 this.$element.attr( 'aria-disabled', isDisabled.toString() );
1888 this.emit( 'disable', isDisabled );
1889 this.updateThemeClasses();
1890 }
1891 this.wasDisabled = isDisabled;
1892
1893 return this;
1894 };
1895
1896 /**
1897 * Update the disabled state, in case of changes in parent widget.
1898 *
1899 * @chainable
1900 */
1901 OO.ui.Widget.prototype.updateDisabled = function () {
1902 this.setDisabled( this.disabled );
1903 return this;
1904 };
1905
1906 /**
1907 * A window is a container for elements that are in a child frame. They are used with
1908 * a window manager (OO.ui.WindowManager), which is used to open and close the window and control
1909 * its presentation. The size of a window is specified using a symbolic name (e.g., ‘small’, ‘medium’,
1910 * ‘large’), which is interpreted by the window manager. If the requested size is not recognized,
1911 * the window manager will choose a sensible fallback.
1912 *
1913 * The lifecycle of a window has three primary stages (opening, opened, and closing) in which
1914 * different processes are executed:
1915 *
1916 * **opening**: The opening stage begins when the window manager's {@link OO.ui.WindowManager#openWindow
1917 * openWindow} or the window's {@link #open open} methods are used, and the window manager begins to open
1918 * the window.
1919 *
1920 * - {@link #getSetupProcess} method is called and its result executed
1921 * - {@link #getReadyProcess} method is called and its result executed
1922 *
1923 * **opened**: The window is now open
1924 *
1925 * **closing**: The closing stage begins when the window manager's
1926 * {@link OO.ui.WindowManager#closeWindow closeWindow}
1927 * or the window's {@link #close} methods are used, and the window manager begins to close the window.
1928 *
1929 * - {@link #getHoldProcess} method is called and its result executed
1930 * - {@link #getTeardownProcess} method is called and its result executed. The window is now closed
1931 *
1932 * Each of the window's processes (setup, ready, hold, and teardown) can be extended in subclasses
1933 * by overriding the window's #getSetupProcess, #getReadyProcess, #getHoldProcess and #getTeardownProcess
1934 * methods. Note that each {@link OO.ui.Process process} is executed in series, so asynchronous
1935 * processing can complete. Always assume window processes are executed asynchronously.
1936 *
1937 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
1938 *
1939 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows
1940 *
1941 * @abstract
1942 * @class
1943 * @extends OO.ui.Element
1944 * @mixins OO.EventEmitter
1945 *
1946 * @constructor
1947 * @param {Object} [config] Configuration options
1948 * @cfg {string} [size] Symbolic name of the dialog size: `small`, `medium`, `large`, `larger` or
1949 * `full`. If omitted, the value of the {@link #static-size static size} property will be used.
1950 */
1951 OO.ui.Window = function OoUiWindow( config ) {
1952 // Configuration initialization
1953 config = config || {};
1954
1955 // Parent constructor
1956 OO.ui.Window.parent.call( this, config );
1957
1958 // Mixin constructors
1959 OO.EventEmitter.call( this );
1960
1961 // Properties
1962 this.manager = null;
1963 this.size = config.size || this.constructor.static.size;
1964 this.$frame = $( '<div>' );
1965 this.$overlay = $( '<div>' );
1966 this.$content = $( '<div>' );
1967
1968 // Initialization
1969 this.$overlay.addClass( 'oo-ui-window-overlay' );
1970 this.$content
1971 .addClass( 'oo-ui-window-content' )
1972 .attr( 'tabindex', 0 );
1973 this.$frame
1974 .addClass( 'oo-ui-window-frame' )
1975 .append( this.$content );
1976
1977 this.$element
1978 .addClass( 'oo-ui-window' )
1979 .append( this.$frame, this.$overlay );
1980
1981 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
1982 // that reference properties not initialized at that time of parent class construction
1983 // TODO: Find a better way to handle post-constructor setup
1984 this.visible = false;
1985 this.$element.addClass( 'oo-ui-element-hidden' );
1986 };
1987
1988 /* Setup */
1989
1990 OO.inheritClass( OO.ui.Window, OO.ui.Element );
1991 OO.mixinClass( OO.ui.Window, OO.EventEmitter );
1992
1993 /* Static Properties */
1994
1995 /**
1996 * Symbolic name of the window size: `small`, `medium`, `large`, `larger` or `full`.
1997 *
1998 * The static size is used if no #size is configured during construction.
1999 *
2000 * @static
2001 * @inheritable
2002 * @property {string}
2003 */
2004 OO.ui.Window.static.size = 'medium';
2005
2006 /* Methods */
2007
2008 /**
2009 * Handle mouse down events.
2010 *
2011 * @private
2012 * @param {jQuery.Event} e Mouse down event
2013 */
2014 OO.ui.Window.prototype.onMouseDown = function ( e ) {
2015 // Prevent clicking on the click-block from stealing focus
2016 if ( e.target === this.$element[ 0 ] ) {
2017 return false;
2018 }
2019 };
2020
2021 /**
2022 * Check if the window has been initialized.
2023 *
2024 * Initialization occurs when a window is added to a manager.
2025 *
2026 * @return {boolean} Window has been initialized
2027 */
2028 OO.ui.Window.prototype.isInitialized = function () {
2029 return !!this.manager;
2030 };
2031
2032 /**
2033 * Check if the window is visible.
2034 *
2035 * @return {boolean} Window is visible
2036 */
2037 OO.ui.Window.prototype.isVisible = function () {
2038 return this.visible;
2039 };
2040
2041 /**
2042 * Check if the window is opening.
2043 *
2044 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpening isOpening}
2045 * method.
2046 *
2047 * @return {boolean} Window is opening
2048 */
2049 OO.ui.Window.prototype.isOpening = function () {
2050 return this.manager.isOpening( this );
2051 };
2052
2053 /**
2054 * Check if the window is closing.
2055 *
2056 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isClosing isClosing} method.
2057 *
2058 * @return {boolean} Window is closing
2059 */
2060 OO.ui.Window.prototype.isClosing = function () {
2061 return this.manager.isClosing( this );
2062 };
2063
2064 /**
2065 * Check if the window is opened.
2066 *
2067 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpened isOpened} method.
2068 *
2069 * @return {boolean} Window is opened
2070 */
2071 OO.ui.Window.prototype.isOpened = function () {
2072 return this.manager.isOpened( this );
2073 };
2074
2075 /**
2076 * Get the window manager.
2077 *
2078 * All windows must be attached to a window manager, which is used to open
2079 * and close the window and control its presentation.
2080 *
2081 * @return {OO.ui.WindowManager} Manager of window
2082 */
2083 OO.ui.Window.prototype.getManager = function () {
2084 return this.manager;
2085 };
2086
2087 /**
2088 * Get the symbolic name of the window size (e.g., `small` or `medium`).
2089 *
2090 * @return {string} Symbolic name of the size: `small`, `medium`, `large`, `larger`, `full`
2091 */
2092 OO.ui.Window.prototype.getSize = function () {
2093 return this.size;
2094 };
2095
2096 /**
2097 * Disable transitions on window's frame for the duration of the callback function, then enable them
2098 * back.
2099 *
2100 * @private
2101 * @param {Function} callback Function to call while transitions are disabled
2102 */
2103 OO.ui.Window.prototype.withoutSizeTransitions = function ( callback ) {
2104 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
2105 // Disable transitions first, otherwise we'll get values from when the window was animating.
2106 var oldTransition,
2107 styleObj = this.$frame[ 0 ].style;
2108 oldTransition = styleObj.transition || styleObj.OTransition || styleObj.MsTransition ||
2109 styleObj.MozTransition || styleObj.WebkitTransition;
2110 styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
2111 styleObj.MozTransition = styleObj.WebkitTransition = 'none';
2112 callback();
2113 // Force reflow to make sure the style changes done inside callback really are not transitioned
2114 this.$frame.height();
2115 styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
2116 styleObj.MozTransition = styleObj.WebkitTransition = oldTransition;
2117 };
2118
2119 /**
2120 * Get the height of the full window contents (i.e., the window head, body and foot together).
2121 *
2122 * What consistitutes the head, body, and foot varies depending on the window type.
2123 * A {@link OO.ui.MessageDialog message dialog} displays a title and message in its body,
2124 * and any actions in the foot. A {@link OO.ui.ProcessDialog process dialog} displays a title
2125 * and special actions in the head, and dialog content in the body.
2126 *
2127 * To get just the height of the dialog body, use the #getBodyHeight method.
2128 *
2129 * @return {number} The height of the window contents (the dialog head, body and foot) in pixels
2130 */
2131 OO.ui.Window.prototype.getContentHeight = function () {
2132 var bodyHeight,
2133 win = this,
2134 bodyStyleObj = this.$body[ 0 ].style,
2135 frameStyleObj = this.$frame[ 0 ].style;
2136
2137 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
2138 // Disable transitions first, otherwise we'll get values from when the window was animating.
2139 this.withoutSizeTransitions( function () {
2140 var oldHeight = frameStyleObj.height,
2141 oldPosition = bodyStyleObj.position;
2142 frameStyleObj.height = '1px';
2143 // Force body to resize to new width
2144 bodyStyleObj.position = 'relative';
2145 bodyHeight = win.getBodyHeight();
2146 frameStyleObj.height = oldHeight;
2147 bodyStyleObj.position = oldPosition;
2148 } );
2149
2150 return (
2151 // Add buffer for border
2152 ( this.$frame.outerHeight() - this.$frame.innerHeight() ) +
2153 // Use combined heights of children
2154 ( this.$head.outerHeight( true ) + bodyHeight + this.$foot.outerHeight( true ) )
2155 );
2156 };
2157
2158 /**
2159 * Get the height of the window body.
2160 *
2161 * To get the height of the full window contents (the window body, head, and foot together),
2162 * use #getContentHeight.
2163 *
2164 * When this function is called, the window will temporarily have been resized
2165 * to height=1px, so .scrollHeight measurements can be taken accurately.
2166 *
2167 * @return {number} Height of the window body in pixels
2168 */
2169 OO.ui.Window.prototype.getBodyHeight = function () {
2170 return this.$body[ 0 ].scrollHeight;
2171 };
2172
2173 /**
2174 * Get the directionality of the frame (right-to-left or left-to-right).
2175 *
2176 * @return {string} Directionality: `'ltr'` or `'rtl'`
2177 */
2178 OO.ui.Window.prototype.getDir = function () {
2179 return this.dir;
2180 };
2181
2182 /**
2183 * Get the 'setup' process.
2184 *
2185 * The setup process is used to set up a window for use in a particular context,
2186 * based on the `data` argument. This method is called during the opening phase of the window’s
2187 * lifecycle.
2188 *
2189 * Override this method to add additional steps to the ‘setup’ process the parent method provides
2190 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2191 * of OO.ui.Process.
2192 *
2193 * To add window content that persists between openings, you may wish to use the #initialize method
2194 * instead.
2195 *
2196 * @abstract
2197 * @param {Object} [data] Window opening data
2198 * @return {OO.ui.Process} Setup process
2199 */
2200 OO.ui.Window.prototype.getSetupProcess = function () {
2201 return new OO.ui.Process();
2202 };
2203
2204 /**
2205 * Get the ‘ready’ process.
2206 *
2207 * The ready process is used to ready a window for use in a particular
2208 * context, based on the `data` argument. This method is called during the opening phase of
2209 * the window’s lifecycle, after the window has been {@link #getSetupProcess setup}.
2210 *
2211 * Override this method to add additional steps to the ‘ready’ process the parent method
2212 * provides using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next}
2213 * methods of OO.ui.Process.
2214 *
2215 * @abstract
2216 * @param {Object} [data] Window opening data
2217 * @return {OO.ui.Process} Ready process
2218 */
2219 OO.ui.Window.prototype.getReadyProcess = function () {
2220 return new OO.ui.Process();
2221 };
2222
2223 /**
2224 * Get the 'hold' process.
2225 *
2226 * The hold proccess is used to keep a window from being used in a particular context,
2227 * based on the `data` argument. This method is called during the closing phase of the window’s
2228 * lifecycle.
2229 *
2230 * Override this method to add additional steps to the 'hold' process the parent method provides
2231 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2232 * of OO.ui.Process.
2233 *
2234 * @abstract
2235 * @param {Object} [data] Window closing data
2236 * @return {OO.ui.Process} Hold process
2237 */
2238 OO.ui.Window.prototype.getHoldProcess = function () {
2239 return new OO.ui.Process();
2240 };
2241
2242 /**
2243 * Get the ‘teardown’ process.
2244 *
2245 * The teardown process is used to teardown a window after use. During teardown,
2246 * user interactions within the window are conveyed and the window is closed, based on the `data`
2247 * argument. This method is called during the closing phase of the window’s lifecycle.
2248 *
2249 * Override this method to add additional steps to the ‘teardown’ process the parent method provides
2250 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2251 * of OO.ui.Process.
2252 *
2253 * @abstract
2254 * @param {Object} [data] Window closing data
2255 * @return {OO.ui.Process} Teardown process
2256 */
2257 OO.ui.Window.prototype.getTeardownProcess = function () {
2258 return new OO.ui.Process();
2259 };
2260
2261 /**
2262 * Set the window manager.
2263 *
2264 * This will cause the window to initialize. Calling it more than once will cause an error.
2265 *
2266 * @param {OO.ui.WindowManager} manager Manager for this window
2267 * @throws {Error} An error is thrown if the method is called more than once
2268 * @chainable
2269 */
2270 OO.ui.Window.prototype.setManager = function ( manager ) {
2271 if ( this.manager ) {
2272 throw new Error( 'Cannot set window manager, window already has a manager' );
2273 }
2274
2275 this.manager = manager;
2276 this.initialize();
2277
2278 return this;
2279 };
2280
2281 /**
2282 * Set the window size by symbolic name (e.g., 'small' or 'medium')
2283 *
2284 * @param {string} size Symbolic name of size: `small`, `medium`, `large`, `larger` or
2285 * `full`
2286 * @chainable
2287 */
2288 OO.ui.Window.prototype.setSize = function ( size ) {
2289 this.size = size;
2290 this.updateSize();
2291 return this;
2292 };
2293
2294 /**
2295 * Update the window size.
2296 *
2297 * @throws {Error} An error is thrown if the window is not attached to a window manager
2298 * @chainable
2299 */
2300 OO.ui.Window.prototype.updateSize = function () {
2301 if ( !this.manager ) {
2302 throw new Error( 'Cannot update window size, must be attached to a manager' );
2303 }
2304
2305 this.manager.updateWindowSize( this );
2306
2307 return this;
2308 };
2309
2310 /**
2311 * Set window dimensions. This method is called by the {@link OO.ui.WindowManager window manager}
2312 * when the window is opening. In general, setDimensions should not be called directly.
2313 *
2314 * To set the size of the window, use the #setSize method.
2315 *
2316 * @param {Object} dim CSS dimension properties
2317 * @param {string|number} [dim.width] Width
2318 * @param {string|number} [dim.minWidth] Minimum width
2319 * @param {string|number} [dim.maxWidth] Maximum width
2320 * @param {string|number} [dim.width] Height, omit to set based on height of contents
2321 * @param {string|number} [dim.minWidth] Minimum height
2322 * @param {string|number} [dim.maxWidth] Maximum height
2323 * @chainable
2324 */
2325 OO.ui.Window.prototype.setDimensions = function ( dim ) {
2326 var height,
2327 win = this,
2328 styleObj = this.$frame[ 0 ].style;
2329
2330 // Calculate the height we need to set using the correct width
2331 if ( dim.height === undefined ) {
2332 this.withoutSizeTransitions( function () {
2333 var oldWidth = styleObj.width;
2334 win.$frame.css( 'width', dim.width || '' );
2335 height = win.getContentHeight();
2336 styleObj.width = oldWidth;
2337 } );
2338 } else {
2339 height = dim.height;
2340 }
2341
2342 this.$frame.css( {
2343 width: dim.width || '',
2344 minWidth: dim.minWidth || '',
2345 maxWidth: dim.maxWidth || '',
2346 height: height || '',
2347 minHeight: dim.minHeight || '',
2348 maxHeight: dim.maxHeight || ''
2349 } );
2350
2351 return this;
2352 };
2353
2354 /**
2355 * Initialize window contents.
2356 *
2357 * Before the window is opened for the first time, #initialize is called so that content that
2358 * persists between openings can be added to the window.
2359 *
2360 * To set up a window with new content each time the window opens, use #getSetupProcess.
2361 *
2362 * @throws {Error} An error is thrown if the window is not attached to a window manager
2363 * @chainable
2364 */
2365 OO.ui.Window.prototype.initialize = function () {
2366 if ( !this.manager ) {
2367 throw new Error( 'Cannot initialize window, must be attached to a manager' );
2368 }
2369
2370 // Properties
2371 this.$head = $( '<div>' );
2372 this.$body = $( '<div>' );
2373 this.$foot = $( '<div>' );
2374 this.dir = OO.ui.Element.static.getDir( this.$content ) || 'ltr';
2375 this.$document = $( this.getElementDocument() );
2376
2377 // Events
2378 this.$element.on( 'mousedown', this.onMouseDown.bind( this ) );
2379
2380 // Initialization
2381 this.$head.addClass( 'oo-ui-window-head' );
2382 this.$body.addClass( 'oo-ui-window-body' );
2383 this.$foot.addClass( 'oo-ui-window-foot' );
2384 this.$content.append( this.$head, this.$body, this.$foot );
2385
2386 return this;
2387 };
2388
2389 /**
2390 * Open the window.
2391 *
2392 * This method is a wrapper around a call to the window manager’s {@link OO.ui.WindowManager#openWindow openWindow}
2393 * method, which returns a promise resolved when the window is done opening.
2394 *
2395 * To customize the window each time it opens, use #getSetupProcess or #getReadyProcess.
2396 *
2397 * @param {Object} [data] Window opening data
2398 * @return {jQuery.Promise} Promise resolved with a value when the window is opened, or rejected
2399 * if the window fails to open. When the promise is resolved successfully, the first argument of the
2400 * value is a new promise, which is resolved when the window begins closing.
2401 * @throws {Error} An error is thrown if the window is not attached to a window manager
2402 */
2403 OO.ui.Window.prototype.open = function ( data ) {
2404 if ( !this.manager ) {
2405 throw new Error( 'Cannot open window, must be attached to a manager' );
2406 }
2407
2408 return this.manager.openWindow( this, data );
2409 };
2410
2411 /**
2412 * Close the window.
2413 *
2414 * This method is a wrapper around a call to the window
2415 * manager’s {@link OO.ui.WindowManager#closeWindow closeWindow} method,
2416 * which returns a closing promise resolved when the window is done closing.
2417 *
2418 * The window's #getHoldProcess and #getTeardownProcess methods are called during the closing
2419 * phase of the window’s lifecycle and can be used to specify closing behavior each time
2420 * the window closes.
2421 *
2422 * @param {Object} [data] Window closing data
2423 * @return {jQuery.Promise} Promise resolved when window is closed
2424 * @throws {Error} An error is thrown if the window is not attached to a window manager
2425 */
2426 OO.ui.Window.prototype.close = function ( data ) {
2427 if ( !this.manager ) {
2428 throw new Error( 'Cannot close window, must be attached to a manager' );
2429 }
2430
2431 return this.manager.closeWindow( this, data );
2432 };
2433
2434 /**
2435 * Setup window.
2436 *
2437 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2438 * by other systems.
2439 *
2440 * @param {Object} [data] Window opening data
2441 * @return {jQuery.Promise} Promise resolved when window is setup
2442 */
2443 OO.ui.Window.prototype.setup = function ( data ) {
2444 var win = this,
2445 deferred = $.Deferred();
2446
2447 this.toggle( true );
2448
2449 this.getSetupProcess( data ).execute().done( function () {
2450 // Force redraw by asking the browser to measure the elements' widths
2451 win.$element.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2452 win.$content.addClass( 'oo-ui-window-content-setup' ).width();
2453 deferred.resolve();
2454 } );
2455
2456 return deferred.promise();
2457 };
2458
2459 /**
2460 * Ready window.
2461 *
2462 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2463 * by other systems.
2464 *
2465 * @param {Object} [data] Window opening data
2466 * @return {jQuery.Promise} Promise resolved when window is ready
2467 */
2468 OO.ui.Window.prototype.ready = function ( data ) {
2469 var win = this,
2470 deferred = $.Deferred();
2471
2472 this.$content.focus();
2473 this.getReadyProcess( data ).execute().done( function () {
2474 // Force redraw by asking the browser to measure the elements' widths
2475 win.$element.addClass( 'oo-ui-window-ready' ).width();
2476 win.$content.addClass( 'oo-ui-window-content-ready' ).width();
2477 deferred.resolve();
2478 } );
2479
2480 return deferred.promise();
2481 };
2482
2483 /**
2484 * Hold window.
2485 *
2486 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2487 * by other systems.
2488 *
2489 * @param {Object} [data] Window closing data
2490 * @return {jQuery.Promise} Promise resolved when window is held
2491 */
2492 OO.ui.Window.prototype.hold = function ( data ) {
2493 var win = this,
2494 deferred = $.Deferred();
2495
2496 this.getHoldProcess( data ).execute().done( function () {
2497 // Get the focused element within the window's content
2498 var $focus = win.$content.find( OO.ui.Element.static.getDocument( win.$content ).activeElement );
2499
2500 // Blur the focused element
2501 if ( $focus.length ) {
2502 $focus[ 0 ].blur();
2503 }
2504
2505 // Force redraw by asking the browser to measure the elements' widths
2506 win.$element.removeClass( 'oo-ui-window-ready' ).width();
2507 win.$content.removeClass( 'oo-ui-window-content-ready' ).width();
2508 deferred.resolve();
2509 } );
2510
2511 return deferred.promise();
2512 };
2513
2514 /**
2515 * Teardown window.
2516 *
2517 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2518 * by other systems.
2519 *
2520 * @param {Object} [data] Window closing data
2521 * @return {jQuery.Promise} Promise resolved when window is torn down
2522 */
2523 OO.ui.Window.prototype.teardown = function ( data ) {
2524 var win = this;
2525
2526 return this.getTeardownProcess( data ).execute()
2527 .done( function () {
2528 // Force redraw by asking the browser to measure the elements' widths
2529 win.$element.removeClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2530 win.$content.removeClass( 'oo-ui-window-content-setup' ).width();
2531 win.toggle( false );
2532 } );
2533 };
2534
2535 /**
2536 * The Dialog class serves as the base class for the other types of dialogs.
2537 * Unless extended to include controls, the rendered dialog box is a simple window
2538 * that users can close by hitting the ‘Esc’ key. Dialog windows are used with OO.ui.WindowManager,
2539 * which opens, closes, and controls the presentation of the window. See the
2540 * [OOjs UI documentation on MediaWiki] [1] for more information.
2541 *
2542 * @example
2543 * // A simple dialog window.
2544 * function MyDialog( config ) {
2545 * MyDialog.parent.call( this, config );
2546 * }
2547 * OO.inheritClass( MyDialog, OO.ui.Dialog );
2548 * MyDialog.prototype.initialize = function () {
2549 * MyDialog.parent.prototype.initialize.call( this );
2550 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
2551 * this.content.$element.append( '<p>A simple dialog window. Press \'Esc\' to close.</p>' );
2552 * this.$body.append( this.content.$element );
2553 * };
2554 * MyDialog.prototype.getBodyHeight = function () {
2555 * return this.content.$element.outerHeight( true );
2556 * };
2557 * var myDialog = new MyDialog( {
2558 * size: 'medium'
2559 * } );
2560 * // Create and append a window manager, which opens and closes the window.
2561 * var windowManager = new OO.ui.WindowManager();
2562 * $( 'body' ).append( windowManager.$element );
2563 * windowManager.addWindows( [ myDialog ] );
2564 * // Open the window!
2565 * windowManager.openWindow( myDialog );
2566 *
2567 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Dialogs
2568 *
2569 * @abstract
2570 * @class
2571 * @extends OO.ui.Window
2572 * @mixins OO.ui.mixin.PendingElement
2573 *
2574 * @constructor
2575 * @param {Object} [config] Configuration options
2576 */
2577 OO.ui.Dialog = function OoUiDialog( config ) {
2578 // Parent constructor
2579 OO.ui.Dialog.parent.call( this, config );
2580
2581 // Mixin constructors
2582 OO.ui.mixin.PendingElement.call( this );
2583
2584 // Properties
2585 this.actions = new OO.ui.ActionSet();
2586 this.attachedActions = [];
2587 this.currentAction = null;
2588 this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
2589
2590 // Events
2591 this.actions.connect( this, {
2592 click: 'onActionClick',
2593 resize: 'onActionResize',
2594 change: 'onActionsChange'
2595 } );
2596
2597 // Initialization
2598 this.$element
2599 .addClass( 'oo-ui-dialog' )
2600 .attr( 'role', 'dialog' );
2601 };
2602
2603 /* Setup */
2604
2605 OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
2606 OO.mixinClass( OO.ui.Dialog, OO.ui.mixin.PendingElement );
2607
2608 /* Static Properties */
2609
2610 /**
2611 * Symbolic name of dialog.
2612 *
2613 * The dialog class must have a symbolic name in order to be registered with OO.Factory.
2614 * Please see the [OOjs UI documentation on MediaWiki] [3] for more information.
2615 *
2616 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
2617 *
2618 * @abstract
2619 * @static
2620 * @inheritable
2621 * @property {string}
2622 */
2623 OO.ui.Dialog.static.name = '';
2624
2625 /**
2626 * The dialog title.
2627 *
2628 * The title can be specified as a plaintext string, a {@link OO.ui.mixin.LabelElement Label} node, or a function
2629 * that will produce a Label node or string. The title can also be specified with data passed to the
2630 * constructor (see #getSetupProcess). In this case, the static value will be overriden.
2631 *
2632 * @abstract
2633 * @static
2634 * @inheritable
2635 * @property {jQuery|string|Function}
2636 */
2637 OO.ui.Dialog.static.title = '';
2638
2639 /**
2640 * An array of configured {@link OO.ui.ActionWidget action widgets}.
2641 *
2642 * Actions can also be specified with data passed to the constructor (see #getSetupProcess). In this case, the static
2643 * value will be overriden.
2644 *
2645 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
2646 *
2647 * @static
2648 * @inheritable
2649 * @property {Object[]}
2650 */
2651 OO.ui.Dialog.static.actions = [];
2652
2653 /**
2654 * Close the dialog when the 'Esc' key is pressed.
2655 *
2656 * @static
2657 * @abstract
2658 * @inheritable
2659 * @property {boolean}
2660 */
2661 OO.ui.Dialog.static.escapable = true;
2662
2663 /* Methods */
2664
2665 /**
2666 * Handle frame document key down events.
2667 *
2668 * @private
2669 * @param {jQuery.Event} e Key down event
2670 */
2671 OO.ui.Dialog.prototype.onDocumentKeyDown = function ( e ) {
2672 if ( e.which === OO.ui.Keys.ESCAPE ) {
2673 this.close();
2674 e.preventDefault();
2675 e.stopPropagation();
2676 }
2677 };
2678
2679 /**
2680 * Handle action resized events.
2681 *
2682 * @private
2683 * @param {OO.ui.ActionWidget} action Action that was resized
2684 */
2685 OO.ui.Dialog.prototype.onActionResize = function () {
2686 // Override in subclass
2687 };
2688
2689 /**
2690 * Handle action click events.
2691 *
2692 * @private
2693 * @param {OO.ui.ActionWidget} action Action that was clicked
2694 */
2695 OO.ui.Dialog.prototype.onActionClick = function ( action ) {
2696 if ( !this.isPending() ) {
2697 this.executeAction( action.getAction() );
2698 }
2699 };
2700
2701 /**
2702 * Handle actions change event.
2703 *
2704 * @private
2705 */
2706 OO.ui.Dialog.prototype.onActionsChange = function () {
2707 this.detachActions();
2708 if ( !this.isClosing() ) {
2709 this.attachActions();
2710 }
2711 };
2712
2713 /**
2714 * Get the set of actions used by the dialog.
2715 *
2716 * @return {OO.ui.ActionSet}
2717 */
2718 OO.ui.Dialog.prototype.getActions = function () {
2719 return this.actions;
2720 };
2721
2722 /**
2723 * Get a process for taking action.
2724 *
2725 * When you override this method, you can create a new OO.ui.Process and return it, or add additional
2726 * accept steps to the process the parent method provides using the {@link OO.ui.Process#first 'first'}
2727 * and {@link OO.ui.Process#next 'next'} methods of OO.ui.Process.
2728 *
2729 * @abstract
2730 * @param {string} [action] Symbolic name of action
2731 * @return {OO.ui.Process} Action process
2732 */
2733 OO.ui.Dialog.prototype.getActionProcess = function ( action ) {
2734 return new OO.ui.Process()
2735 .next( function () {
2736 if ( !action ) {
2737 // An empty action always closes the dialog without data, which should always be
2738 // safe and make no changes
2739 this.close();
2740 }
2741 }, this );
2742 };
2743
2744 /**
2745 * @inheritdoc
2746 *
2747 * @param {Object} [data] Dialog opening data
2748 * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use
2749 * the {@link #static-title static title}
2750 * @param {Object[]} [data.actions] List of configuration options for each
2751 * {@link OO.ui.ActionWidget action widget}, omit to use {@link #static-actions static actions}.
2752 */
2753 OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
2754 data = data || {};
2755
2756 // Parent method
2757 return OO.ui.Dialog.parent.prototype.getSetupProcess.call( this, data )
2758 .next( function () {
2759 var config = this.constructor.static,
2760 actions = data.actions !== undefined ? data.actions : config.actions;
2761
2762 this.title.setLabel(
2763 data.title !== undefined ? data.title : this.constructor.static.title
2764 );
2765 this.actions.add( this.getActionWidgets( actions ) );
2766
2767 if ( this.constructor.static.escapable ) {
2768 this.$document.on( 'keydown', this.onDocumentKeyDownHandler );
2769 }
2770 }, this );
2771 };
2772
2773 /**
2774 * @inheritdoc
2775 */
2776 OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
2777 // Parent method
2778 return OO.ui.Dialog.parent.prototype.getTeardownProcess.call( this, data )
2779 .first( function () {
2780 if ( this.constructor.static.escapable ) {
2781 this.$document.off( 'keydown', this.onDocumentKeyDownHandler );
2782 }
2783
2784 this.actions.clear();
2785 this.currentAction = null;
2786 }, this );
2787 };
2788
2789 /**
2790 * @inheritdoc
2791 */
2792 OO.ui.Dialog.prototype.initialize = function () {
2793 // Parent method
2794 OO.ui.Dialog.parent.prototype.initialize.call( this );
2795
2796 var titleId = OO.ui.generateElementId();
2797
2798 // Properties
2799 this.title = new OO.ui.LabelWidget( {
2800 id: titleId
2801 } );
2802
2803 // Initialization
2804 this.$content.addClass( 'oo-ui-dialog-content' );
2805 this.$element.attr( 'aria-labelledby', titleId );
2806 this.setPendingElement( this.$head );
2807 };
2808
2809 /**
2810 * Get action widgets from a list of configs
2811 *
2812 * @param {Object[]} actions Action widget configs
2813 * @return {OO.ui.ActionWidget[]} Action widgets
2814 */
2815 OO.ui.Dialog.prototype.getActionWidgets = function ( actions ) {
2816 var i, len, widgets = [];
2817 for ( i = 0, len = actions.length; i < len; i++ ) {
2818 widgets.push(
2819 new OO.ui.ActionWidget( actions[ i ] )
2820 );
2821 }
2822 return widgets;
2823 };
2824
2825 /**
2826 * Attach action actions.
2827 *
2828 * @protected
2829 */
2830 OO.ui.Dialog.prototype.attachActions = function () {
2831 // Remember the list of potentially attached actions
2832 this.attachedActions = this.actions.get();
2833 };
2834
2835 /**
2836 * Detach action actions.
2837 *
2838 * @protected
2839 * @chainable
2840 */
2841 OO.ui.Dialog.prototype.detachActions = function () {
2842 var i, len;
2843
2844 // Detach all actions that may have been previously attached
2845 for ( i = 0, len = this.attachedActions.length; i < len; i++ ) {
2846 this.attachedActions[ i ].$element.detach();
2847 }
2848 this.attachedActions = [];
2849 };
2850
2851 /**
2852 * Execute an action.
2853 *
2854 * @param {string} action Symbolic name of action to execute
2855 * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
2856 */
2857 OO.ui.Dialog.prototype.executeAction = function ( action ) {
2858 this.pushPending();
2859 this.currentAction = action;
2860 return this.getActionProcess( action ).execute()
2861 .always( this.popPending.bind( this ) );
2862 };
2863
2864 /**
2865 * Window managers are used to open and close {@link OO.ui.Window windows} and control their presentation.
2866 * Managed windows are mutually exclusive. If a new window is opened while a current window is opening
2867 * or is opened, the current window will be closed and any ongoing {@link OO.ui.Process process} will be cancelled. Windows
2868 * themselves are persistent and—rather than being torn down when closed—can be repopulated with the
2869 * pertinent data and reused.
2870 *
2871 * Over the lifecycle of a window, the window manager makes available three promises: `opening`,
2872 * `opened`, and `closing`, which represent the primary stages of the cycle:
2873 *
2874 * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s
2875 * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window.
2876 *
2877 * - an `opening` event is emitted with an `opening` promise
2878 * - the #getSetupDelay method is called and the returned value is used to time a pause in execution before
2879 * the window’s {@link OO.ui.Window#getSetupProcess getSetupProcess} method is called on the
2880 * window and its result executed
2881 * - a `setup` progress notification is emitted from the `opening` promise
2882 * - the #getReadyDelay method is called the returned value is used to time a pause in execution before
2883 * the window’s {@link OO.ui.Window#getReadyProcess getReadyProcess} method is called on the
2884 * window and its result executed
2885 * - a `ready` progress notification is emitted from the `opening` promise
2886 * - the `opening` promise is resolved with an `opened` promise
2887 *
2888 * **Opened**: the window is now open.
2889 *
2890 * **Closing**: the closing stage begins when the window manager's #closeWindow or the
2891 * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins
2892 * to close the window.
2893 *
2894 * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted
2895 * - the #getHoldDelay method is called and the returned value is used to time a pause in execution before
2896 * the window's {@link OO.ui.Window#getHoldProcess getHoldProces} method is called on the
2897 * window and its result executed
2898 * - a `hold` progress notification is emitted from the `closing` promise
2899 * - the #getTeardownDelay() method is called and the returned value is used to time a pause in execution before
2900 * the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method is called on the
2901 * window and its result executed
2902 * - a `teardown` progress notification is emitted from the `closing` promise
2903 * - the `closing` promise is resolved. The window is now closed
2904 *
2905 * See the [OOjs UI documentation on MediaWiki][1] for more information.
2906 *
2907 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
2908 *
2909 * @class
2910 * @extends OO.ui.Element
2911 * @mixins OO.EventEmitter
2912 *
2913 * @constructor
2914 * @param {Object} [config] Configuration options
2915 * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
2916 * Note that window classes that are instantiated with a factory must have
2917 * a {@link OO.ui.Dialog#static-name static name} property that specifies a symbolic name.
2918 * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
2919 */
2920 OO.ui.WindowManager = function OoUiWindowManager( config ) {
2921 // Configuration initialization
2922 config = config || {};
2923
2924 // Parent constructor
2925 OO.ui.WindowManager.parent.call( this, config );
2926
2927 // Mixin constructors
2928 OO.EventEmitter.call( this );
2929
2930 // Properties
2931 this.factory = config.factory;
2932 this.modal = config.modal === undefined || !!config.modal;
2933 this.windows = {};
2934 this.opening = null;
2935 this.opened = null;
2936 this.closing = null;
2937 this.preparingToOpen = null;
2938 this.preparingToClose = null;
2939 this.currentWindow = null;
2940 this.globalEvents = false;
2941 this.$ariaHidden = null;
2942 this.onWindowResizeTimeout = null;
2943 this.onWindowResizeHandler = this.onWindowResize.bind( this );
2944 this.afterWindowResizeHandler = this.afterWindowResize.bind( this );
2945
2946 // Initialization
2947 this.$element
2948 .addClass( 'oo-ui-windowManager' )
2949 .toggleClass( 'oo-ui-windowManager-modal', this.modal );
2950 };
2951
2952 /* Setup */
2953
2954 OO.inheritClass( OO.ui.WindowManager, OO.ui.Element );
2955 OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter );
2956
2957 /* Events */
2958
2959 /**
2960 * An 'opening' event is emitted when the window begins to be opened.
2961 *
2962 * @event opening
2963 * @param {OO.ui.Window} win Window that's being opened
2964 * @param {jQuery.Promise} opening An `opening` promise resolved with a value when the window is opened successfully.
2965 * When the `opening` promise is resolved, the first argument of the value is an 'opened' promise, the second argument
2966 * is the opening data. The `opening` promise emits `setup` and `ready` notifications when those processes are complete.
2967 * @param {Object} data Window opening data
2968 */
2969
2970 /**
2971 * A 'closing' event is emitted when the window begins to be closed.
2972 *
2973 * @event closing
2974 * @param {OO.ui.Window} win Window that's being closed
2975 * @param {jQuery.Promise} closing A `closing` promise is resolved with a value when the window
2976 * is closed successfully. The promise emits `hold` and `teardown` notifications when those
2977 * processes are complete. When the `closing` promise is resolved, the first argument of its value
2978 * is the closing data.
2979 * @param {Object} data Window closing data
2980 */
2981
2982 /**
2983 * A 'resize' event is emitted when a window is resized.
2984 *
2985 * @event resize
2986 * @param {OO.ui.Window} win Window that was resized
2987 */
2988
2989 /* Static Properties */
2990
2991 /**
2992 * Map of the symbolic name of each window size and its CSS properties.
2993 *
2994 * @static
2995 * @inheritable
2996 * @property {Object}
2997 */
2998 OO.ui.WindowManager.static.sizes = {
2999 small: {
3000 width: 300
3001 },
3002 medium: {
3003 width: 500
3004 },
3005 large: {
3006 width: 700
3007 },
3008 larger: {
3009 width: 900
3010 },
3011 full: {
3012 // These can be non-numeric because they are never used in calculations
3013 width: '100%',
3014 height: '100%'
3015 }
3016 };
3017
3018 /**
3019 * Symbolic name of the default window size.
3020 *
3021 * The default size is used if the window's requested size is not recognized.
3022 *
3023 * @static
3024 * @inheritable
3025 * @property {string}
3026 */
3027 OO.ui.WindowManager.static.defaultSize = 'medium';
3028
3029 /* Methods */
3030
3031 /**
3032 * Handle window resize events.
3033 *
3034 * @private
3035 * @param {jQuery.Event} e Window resize event
3036 */
3037 OO.ui.WindowManager.prototype.onWindowResize = function () {
3038 clearTimeout( this.onWindowResizeTimeout );
3039 this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 );
3040 };
3041
3042 /**
3043 * Handle window resize events.
3044 *
3045 * @private
3046 * @param {jQuery.Event} e Window resize event
3047 */
3048 OO.ui.WindowManager.prototype.afterWindowResize = function () {
3049 if ( this.currentWindow ) {
3050 this.updateWindowSize( this.currentWindow );
3051 }
3052 };
3053
3054 /**
3055 * Check if window is opening.
3056 *
3057 * @return {boolean} Window is opening
3058 */
3059 OO.ui.WindowManager.prototype.isOpening = function ( win ) {
3060 return win === this.currentWindow && !!this.opening && this.opening.state() === 'pending';
3061 };
3062
3063 /**
3064 * Check if window is closing.
3065 *
3066 * @return {boolean} Window is closing
3067 */
3068 OO.ui.WindowManager.prototype.isClosing = function ( win ) {
3069 return win === this.currentWindow && !!this.closing && this.closing.state() === 'pending';
3070 };
3071
3072 /**
3073 * Check if window is opened.
3074 *
3075 * @return {boolean} Window is opened
3076 */
3077 OO.ui.WindowManager.prototype.isOpened = function ( win ) {
3078 return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending';
3079 };
3080
3081 /**
3082 * Check if a window is being managed.
3083 *
3084 * @param {OO.ui.Window} win Window to check
3085 * @return {boolean} Window is being managed
3086 */
3087 OO.ui.WindowManager.prototype.hasWindow = function ( win ) {
3088 var name;
3089
3090 for ( name in this.windows ) {
3091 if ( this.windows[ name ] === win ) {
3092 return true;
3093 }
3094 }
3095
3096 return false;
3097 };
3098
3099 /**
3100 * Get the number of milliseconds to wait after opening begins before executing the ‘setup’ process.
3101 *
3102 * @param {OO.ui.Window} win Window being opened
3103 * @param {Object} [data] Window opening data
3104 * @return {number} Milliseconds to wait
3105 */
3106 OO.ui.WindowManager.prototype.getSetupDelay = function () {
3107 return 0;
3108 };
3109
3110 /**
3111 * Get the number of milliseconds to wait after setup has finished before executing the ‘ready’ process.
3112 *
3113 * @param {OO.ui.Window} win Window being opened
3114 * @param {Object} [data] Window opening data
3115 * @return {number} Milliseconds to wait
3116 */
3117 OO.ui.WindowManager.prototype.getReadyDelay = function () {
3118 return 0;
3119 };
3120
3121 /**
3122 * Get the number of milliseconds to wait after closing has begun before executing the 'hold' process.
3123 *
3124 * @param {OO.ui.Window} win Window being closed
3125 * @param {Object} [data] Window closing data
3126 * @return {number} Milliseconds to wait
3127 */
3128 OO.ui.WindowManager.prototype.getHoldDelay = function () {
3129 return 0;
3130 };
3131
3132 /**
3133 * Get the number of milliseconds to wait after the ‘hold’ process has finished before
3134 * executing the ‘teardown’ process.
3135 *
3136 * @param {OO.ui.Window} win Window being closed
3137 * @param {Object} [data] Window closing data
3138 * @return {number} Milliseconds to wait
3139 */
3140 OO.ui.WindowManager.prototype.getTeardownDelay = function () {
3141 return this.modal ? 250 : 0;
3142 };
3143
3144 /**
3145 * Get a window by its symbolic name.
3146 *
3147 * If the window is not yet instantiated and its symbolic name is recognized by a factory, it will be
3148 * instantiated and added to the window manager automatically. Please see the [OOjs UI documentation on MediaWiki][3]
3149 * for more information about using factories.
3150 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
3151 *
3152 * @param {string} name Symbolic name of the window
3153 * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
3154 * @throws {Error} An error is thrown if the symbolic name is not recognized by the factory.
3155 * @throws {Error} An error is thrown if the named window is not recognized as a managed window.
3156 */
3157 OO.ui.WindowManager.prototype.getWindow = function ( name ) {
3158 var deferred = $.Deferred(),
3159 win = this.windows[ name ];
3160
3161 if ( !( win instanceof OO.ui.Window ) ) {
3162 if ( this.factory ) {
3163 if ( !this.factory.lookup( name ) ) {
3164 deferred.reject( new OO.ui.Error(
3165 'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
3166 ) );
3167 } else {
3168 win = this.factory.create( name );
3169 this.addWindows( [ win ] );
3170 deferred.resolve( win );
3171 }
3172 } else {
3173 deferred.reject( new OO.ui.Error(
3174 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
3175 ) );
3176 }
3177 } else {
3178 deferred.resolve( win );
3179 }
3180
3181 return deferred.promise();
3182 };
3183
3184 /**
3185 * Get current window.
3186 *
3187 * @return {OO.ui.Window|null} Currently opening/opened/closing window
3188 */
3189 OO.ui.WindowManager.prototype.getCurrentWindow = function () {
3190 return this.currentWindow;
3191 };
3192
3193 /**
3194 * Open a window.
3195 *
3196 * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
3197 * @param {Object} [data] Window opening data
3198 * @return {jQuery.Promise} An `opening` promise resolved when the window is done opening.
3199 * See {@link #event-opening 'opening' event} for more information about `opening` promises.
3200 * @fires opening
3201 */
3202 OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
3203 var manager = this,
3204 opening = $.Deferred();
3205
3206 // Argument handling
3207 if ( typeof win === 'string' ) {
3208 return this.getWindow( win ).then( function ( win ) {
3209 return manager.openWindow( win, data );
3210 } );
3211 }
3212
3213 // Error handling
3214 if ( !this.hasWindow( win ) ) {
3215 opening.reject( new OO.ui.Error(
3216 'Cannot open window: window is not attached to manager'
3217 ) );
3218 } else if ( this.preparingToOpen || this.opening || this.opened ) {
3219 opening.reject( new OO.ui.Error(
3220 'Cannot open window: another window is opening or open'
3221 ) );
3222 }
3223
3224 // Window opening
3225 if ( opening.state() !== 'rejected' ) {
3226 // If a window is currently closing, wait for it to complete
3227 this.preparingToOpen = $.when( this.closing );
3228 // Ensure handlers get called after preparingToOpen is set
3229 this.preparingToOpen.done( function () {
3230 if ( manager.modal ) {
3231 manager.toggleGlobalEvents( true );
3232 manager.toggleAriaIsolation( true );
3233 }
3234 manager.currentWindow = win;
3235 manager.opening = opening;
3236 manager.preparingToOpen = null;
3237 manager.emit( 'opening', win, opening, data );
3238 setTimeout( function () {
3239 win.setup( data ).then( function () {
3240 manager.updateWindowSize( win );
3241 manager.opening.notify( { state: 'setup' } );
3242 setTimeout( function () {
3243 win.ready( data ).then( function () {
3244 manager.opening.notify( { state: 'ready' } );
3245 manager.opening = null;
3246 manager.opened = $.Deferred();
3247 opening.resolve( manager.opened.promise(), data );
3248 } );
3249 }, manager.getReadyDelay() );
3250 } );
3251 }, manager.getSetupDelay() );
3252 } );
3253 }
3254
3255 return opening.promise();
3256 };
3257
3258 /**
3259 * Close a window.
3260 *
3261 * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
3262 * @param {Object} [data] Window closing data
3263 * @return {jQuery.Promise} A `closing` promise resolved when the window is done closing.
3264 * See {@link #event-closing 'closing' event} for more information about closing promises.
3265 * @throws {Error} An error is thrown if the window is not managed by the window manager.
3266 * @fires closing
3267 */
3268 OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
3269 var manager = this,
3270 closing = $.Deferred(),
3271 opened;
3272
3273 // Argument handling
3274 if ( typeof win === 'string' ) {
3275 win = this.windows[ win ];
3276 } else if ( !this.hasWindow( win ) ) {
3277 win = null;
3278 }
3279
3280 // Error handling
3281 if ( !win ) {
3282 closing.reject( new OO.ui.Error(
3283 'Cannot close window: window is not attached to manager'
3284 ) );
3285 } else if ( win !== this.currentWindow ) {
3286 closing.reject( new OO.ui.Error(
3287 'Cannot close window: window already closed with different data'
3288 ) );
3289 } else if ( this.preparingToClose || this.closing ) {
3290 closing.reject( new OO.ui.Error(
3291 'Cannot close window: window already closing with different data'
3292 ) );
3293 }
3294
3295 // Window closing
3296 if ( closing.state() !== 'rejected' ) {
3297 // If the window is currently opening, close it when it's done
3298 this.preparingToClose = $.when( this.opening );
3299 // Ensure handlers get called after preparingToClose is set
3300 this.preparingToClose.done( function () {
3301 manager.closing = closing;
3302 manager.preparingToClose = null;
3303 manager.emit( 'closing', win, closing, data );
3304 opened = manager.opened;
3305 manager.opened = null;
3306 opened.resolve( closing.promise(), data );
3307 setTimeout( function () {
3308 win.hold( data ).then( function () {
3309 closing.notify( { state: 'hold' } );
3310 setTimeout( function () {
3311 win.teardown( data ).then( function () {
3312 closing.notify( { state: 'teardown' } );
3313 if ( manager.modal ) {
3314 manager.toggleGlobalEvents( false );
3315 manager.toggleAriaIsolation( false );
3316 }
3317 manager.closing = null;
3318 manager.currentWindow = null;
3319 closing.resolve( data );
3320 } );
3321 }, manager.getTeardownDelay() );
3322 } );
3323 }, manager.getHoldDelay() );
3324 } );
3325 }
3326
3327 return closing.promise();
3328 };
3329
3330 /**
3331 * Add windows to the window manager.
3332 *
3333 * Windows can be added by reference, symbolic name, or explicitly defined symbolic names.
3334 * See the [OOjs ui documentation on MediaWiki] [2] for examples.
3335 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
3336 *
3337 * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows An array of window objects specified
3338 * by reference, symbolic name, or explicitly defined symbolic names.
3339 * @throws {Error} An error is thrown if a window is added by symbolic name, but has neither an
3340 * explicit nor a statically configured symbolic name.
3341 */
3342 OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
3343 var i, len, win, name, list;
3344
3345 if ( Array.isArray( windows ) ) {
3346 // Convert to map of windows by looking up symbolic names from static configuration
3347 list = {};
3348 for ( i = 0, len = windows.length; i < len; i++ ) {
3349 name = windows[ i ].constructor.static.name;
3350 if ( typeof name !== 'string' ) {
3351 throw new Error( 'Cannot add window' );
3352 }
3353 list[ name ] = windows[ i ];
3354 }
3355 } else if ( OO.isPlainObject( windows ) ) {
3356 list = windows;
3357 }
3358
3359 // Add windows
3360 for ( name in list ) {
3361 win = list[ name ];
3362 this.windows[ name ] = win.toggle( false );
3363 this.$element.append( win.$element );
3364 win.setManager( this );
3365 }
3366 };
3367
3368 /**
3369 * Remove the specified windows from the windows manager.
3370 *
3371 * Windows will be closed before they are removed. If you wish to remove all windows, you may wish to use
3372 * the #clearWindows method instead. If you no longer need the window manager and want to ensure that it no
3373 * longer listens to events, use the #destroy method.
3374 *
3375 * @param {string[]} names Symbolic names of windows to remove
3376 * @return {jQuery.Promise} Promise resolved when window is closed and removed
3377 * @throws {Error} An error is thrown if the named windows are not managed by the window manager.
3378 */
3379 OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
3380 var i, len, win, name, cleanupWindow,
3381 manager = this,
3382 promises = [],
3383 cleanup = function ( name, win ) {
3384 delete manager.windows[ name ];
3385 win.$element.detach();
3386 };
3387
3388 for ( i = 0, len = names.length; i < len; i++ ) {
3389 name = names[ i ];
3390 win = this.windows[ name ];
3391 if ( !win ) {
3392 throw new Error( 'Cannot remove window' );
3393 }
3394 cleanupWindow = cleanup.bind( null, name, win );
3395 promises.push( this.closeWindow( name ).then( cleanupWindow, cleanupWindow ) );
3396 }
3397
3398 return $.when.apply( $, promises );
3399 };
3400
3401 /**
3402 * Remove all windows from the window manager.
3403 *
3404 * Windows will be closed before they are removed. Note that the window manager, though not in use, will still
3405 * listen to events. If the window manager will not be used again, you may wish to use the #destroy method instead.
3406 * To remove just a subset of windows, use the #removeWindows method.
3407 *
3408 * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
3409 */
3410 OO.ui.WindowManager.prototype.clearWindows = function () {
3411 return this.removeWindows( Object.keys( this.windows ) );
3412 };
3413
3414 /**
3415 * Set dialog size. In general, this method should not be called directly.
3416 *
3417 * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
3418 *
3419 * @chainable
3420 */
3421 OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) {
3422 // Bypass for non-current, and thus invisible, windows
3423 if ( win !== this.currentWindow ) {
3424 return;
3425 }
3426
3427 var viewport = OO.ui.Element.static.getDimensions( win.getElementWindow() ),
3428 sizes = this.constructor.static.sizes,
3429 size = win.getSize();
3430
3431 if ( !sizes[ size ] ) {
3432 size = this.constructor.static.defaultSize;
3433 }
3434 if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[ size ].width ) {
3435 size = 'full';
3436 }
3437
3438 this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', size === 'full' );
3439 this.$element.toggleClass( 'oo-ui-windowManager-floating', size !== 'full' );
3440 win.setDimensions( sizes[ size ] );
3441
3442 this.emit( 'resize', win );
3443
3444 return this;
3445 };
3446
3447 /**
3448 * Bind or unbind global events for scrolling.
3449 *
3450 * @private
3451 * @param {boolean} [on] Bind global events
3452 * @chainable
3453 */
3454 OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) {
3455 on = on === undefined ? !!this.globalEvents : !!on;
3456
3457 var scrollWidth, bodyMargin,
3458 $body = $( this.getElementDocument().body ),
3459 // We could have multiple window managers open so only modify
3460 // the body css at the bottom of the stack
3461 stackDepth = $body.data( 'windowManagerGlobalEvents' ) || 0 ;
3462
3463 if ( on ) {
3464 if ( !this.globalEvents ) {
3465 $( this.getElementWindow() ).on( {
3466 // Start listening for top-level window dimension changes
3467 'orientationchange resize': this.onWindowResizeHandler
3468 } );
3469 if ( stackDepth === 0 ) {
3470 scrollWidth = window.innerWidth - document.documentElement.clientWidth;
3471 bodyMargin = parseFloat( $body.css( 'margin-right' ) ) || 0;
3472 $body.css( {
3473 overflow: 'hidden',
3474 'margin-right': bodyMargin + scrollWidth
3475 } );
3476 }
3477 stackDepth++;
3478 this.globalEvents = true;
3479 }
3480 } else if ( this.globalEvents ) {
3481 $( this.getElementWindow() ).off( {
3482 // Stop listening for top-level window dimension changes
3483 'orientationchange resize': this.onWindowResizeHandler
3484 } );
3485 stackDepth--;
3486 if ( stackDepth === 0 ) {
3487 $body.css( {
3488 overflow: '',
3489 'margin-right': ''
3490 } );
3491 }
3492 this.globalEvents = false;
3493 }
3494 $body.data( 'windowManagerGlobalEvents', stackDepth );
3495
3496 return this;
3497 };
3498
3499 /**
3500 * Toggle screen reader visibility of content other than the window manager.
3501 *
3502 * @private
3503 * @param {boolean} [isolate] Make only the window manager visible to screen readers
3504 * @chainable
3505 */
3506 OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) {
3507 isolate = isolate === undefined ? !this.$ariaHidden : !!isolate;
3508
3509 if ( isolate ) {
3510 if ( !this.$ariaHidden ) {
3511 // Hide everything other than the window manager from screen readers
3512 this.$ariaHidden = $( 'body' )
3513 .children()
3514 .not( this.$element.parentsUntil( 'body' ).last() )
3515 .attr( 'aria-hidden', '' );
3516 }
3517 } else if ( this.$ariaHidden ) {
3518 // Restore screen reader visibility
3519 this.$ariaHidden.removeAttr( 'aria-hidden' );
3520 this.$ariaHidden = null;
3521 }
3522
3523 return this;
3524 };
3525
3526 /**
3527 * Destroy the window manager.
3528 *
3529 * Destroying the window manager ensures that it will no longer listen to events. If you would like to
3530 * continue using the window manager, but wish to remove all windows from it, use the #clearWindows method
3531 * instead.
3532 */
3533 OO.ui.WindowManager.prototype.destroy = function () {
3534 this.toggleGlobalEvents( false );
3535 this.toggleAriaIsolation( false );
3536 this.clearWindows();
3537 this.$element.remove();
3538 };
3539
3540 /**
3541 * Errors contain a required message (either a string or jQuery selection) that is used to describe what went wrong
3542 * in a {@link OO.ui.Process process}. The error's #recoverable and #warning configurations are used to customize the
3543 * appearance and functionality of the error interface.
3544 *
3545 * The basic error interface contains a formatted error message as well as two buttons: 'Dismiss' and 'Try again' (i.e., the error
3546 * is 'recoverable' by default). If the error is not recoverable, the 'Try again' button will not be rendered and the widget
3547 * that initiated the failed process will be disabled.
3548 *
3549 * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button, which will try the
3550 * process again.
3551 *
3552 * For an example of error interfaces, please see the [OOjs UI documentation on MediaWiki][1].
3553 *
3554 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Processes_and_errors
3555 *
3556 * @class
3557 *
3558 * @constructor
3559 * @param {string|jQuery} message Description of error
3560 * @param {Object} [config] Configuration options
3561 * @cfg {boolean} [recoverable=true] Error is recoverable.
3562 * By default, errors are recoverable, and users can try the process again.
3563 * @cfg {boolean} [warning=false] Error is a warning.
3564 * If the error is a warning, the error interface will include a
3565 * 'Dismiss' and a 'Continue' button. It is the responsibility of the developer to ensure that the warning
3566 * is not triggered a second time if the user chooses to continue.
3567 */
3568 OO.ui.Error = function OoUiError( message, config ) {
3569 // Allow passing positional parameters inside the config object
3570 if ( OO.isPlainObject( message ) && config === undefined ) {
3571 config = message;
3572 message = config.message;
3573 }
3574
3575 // Configuration initialization
3576 config = config || {};
3577
3578 // Properties
3579 this.message = message instanceof jQuery ? message : String( message );
3580 this.recoverable = config.recoverable === undefined || !!config.recoverable;
3581 this.warning = !!config.warning;
3582 };
3583
3584 /* Setup */
3585
3586 OO.initClass( OO.ui.Error );
3587
3588 /* Methods */
3589
3590 /**
3591 * Check if the error is recoverable.
3592 *
3593 * If the error is recoverable, users are able to try the process again.
3594 *
3595 * @return {boolean} Error is recoverable
3596 */
3597 OO.ui.Error.prototype.isRecoverable = function () {
3598 return this.recoverable;
3599 };
3600
3601 /**
3602 * Check if the error is a warning.
3603 *
3604 * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button.
3605 *
3606 * @return {boolean} Error is warning
3607 */
3608 OO.ui.Error.prototype.isWarning = function () {
3609 return this.warning;
3610 };
3611
3612 /**
3613 * Get error message as DOM nodes.
3614 *
3615 * @return {jQuery} Error message in DOM nodes
3616 */
3617 OO.ui.Error.prototype.getMessage = function () {
3618 return this.message instanceof jQuery ?
3619 this.message.clone() :
3620 $( '<div>' ).text( this.message ).contents();
3621 };
3622
3623 /**
3624 * Get the error message text.
3625 *
3626 * @return {string} Error message
3627 */
3628 OO.ui.Error.prototype.getMessageText = function () {
3629 return this.message instanceof jQuery ? this.message.text() : this.message;
3630 };
3631
3632 /**
3633 * Wraps an HTML snippet for use with configuration values which default
3634 * to strings. This bypasses the default html-escaping done to string
3635 * values.
3636 *
3637 * @class
3638 *
3639 * @constructor
3640 * @param {string} [content] HTML content
3641 */
3642 OO.ui.HtmlSnippet = function OoUiHtmlSnippet( content ) {
3643 // Properties
3644 this.content = content;
3645 };
3646
3647 /* Setup */
3648
3649 OO.initClass( OO.ui.HtmlSnippet );
3650
3651 /* Methods */
3652
3653 /**
3654 * Render into HTML.
3655 *
3656 * @return {string} Unchanged HTML snippet.
3657 */
3658 OO.ui.HtmlSnippet.prototype.toString = function () {
3659 return this.content;
3660 };
3661
3662 /**
3663 * A Process is a list of steps that are called in sequence. The step can be a number, a jQuery promise,
3664 * or a function:
3665 *
3666 * - **number**: the process will wait for the specified number of milliseconds before proceeding.
3667 * - **promise**: the process will continue to the next step when the promise is successfully resolved
3668 * or stop if the promise is rejected.
3669 * - **function**: the process will execute the function. The process will stop if the function returns
3670 * either a boolean `false` or a promise that is rejected; if the function returns a number, the process
3671 * will wait for that number of milliseconds before proceeding.
3672 *
3673 * If the process fails, an {@link OO.ui.Error error} is generated. Depending on how the error is
3674 * configured, users can dismiss the error and try the process again, or not. If a process is stopped,
3675 * its remaining steps will not be performed.
3676 *
3677 * @class
3678 *
3679 * @constructor
3680 * @param {number|jQuery.Promise|Function} step Number of miliseconds to wait before proceeding, promise
3681 * that must be resolved before proceeding, or a function to execute. See #createStep for more information. see #createStep for more information
3682 * @param {Object} [context=null] Execution context of the function. The context is ignored if the step is
3683 * a number or promise.
3684 * @return {Object} Step object, with `callback` and `context` properties
3685 */
3686 OO.ui.Process = function ( step, context ) {
3687 // Properties
3688 this.steps = [];
3689
3690 // Initialization
3691 if ( step !== undefined ) {
3692 this.next( step, context );
3693 }
3694 };
3695
3696 /* Setup */
3697
3698 OO.initClass( OO.ui.Process );
3699
3700 /* Methods */
3701
3702 /**
3703 * Start the process.
3704 *
3705 * @return {jQuery.Promise} Promise that is resolved when all steps have successfully completed.
3706 * If any of the steps return a promise that is rejected or a boolean false, this promise is rejected
3707 * and any remaining steps are not performed.
3708 */
3709 OO.ui.Process.prototype.execute = function () {
3710 var i, len, promise;
3711
3712 /**
3713 * Continue execution.
3714 *
3715 * @ignore
3716 * @param {Array} step A function and the context it should be called in
3717 * @return {Function} Function that continues the process
3718 */
3719 function proceed( step ) {
3720 return function () {
3721 // Execute step in the correct context
3722 var deferred,
3723 result = step.callback.call( step.context );
3724
3725 if ( result === false ) {
3726 // Use rejected promise for boolean false results
3727 return $.Deferred().reject( [] ).promise();
3728 }
3729 if ( typeof result === 'number' ) {
3730 if ( result < 0 ) {
3731 throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
3732 }
3733 // Use a delayed promise for numbers, expecting them to be in milliseconds
3734 deferred = $.Deferred();
3735 setTimeout( deferred.resolve, result );
3736 return deferred.promise();
3737 }
3738 if ( result instanceof OO.ui.Error ) {
3739 // Use rejected promise for error
3740 return $.Deferred().reject( [ result ] ).promise();
3741 }
3742 if ( Array.isArray( result ) && result.length && result[ 0 ] instanceof OO.ui.Error ) {
3743 // Use rejected promise for list of errors
3744 return $.Deferred().reject( result ).promise();
3745 }
3746 // Duck-type the object to see if it can produce a promise
3747 if ( result && $.isFunction( result.promise ) ) {
3748 // Use a promise generated from the result
3749 return result.promise();
3750 }
3751 // Use resolved promise for other results
3752 return $.Deferred().resolve().promise();
3753 };
3754 }
3755
3756 if ( this.steps.length ) {
3757 // Generate a chain reaction of promises
3758 promise = proceed( this.steps[ 0 ] )();
3759 for ( i = 1, len = this.steps.length; i < len; i++ ) {
3760 promise = promise.then( proceed( this.steps[ i ] ) );
3761 }
3762 } else {
3763 promise = $.Deferred().resolve().promise();
3764 }
3765
3766 return promise;
3767 };
3768
3769 /**
3770 * Create a process step.
3771 *
3772 * @private
3773 * @param {number|jQuery.Promise|Function} step
3774 *
3775 * - Number of milliseconds to wait before proceeding
3776 * - Promise that must be resolved before proceeding
3777 * - Function to execute
3778 * - If the function returns a boolean false the process will stop
3779 * - If the function returns a promise, the process will continue to the next
3780 * step when the promise is resolved or stop if the promise is rejected
3781 * - If the function returns a number, the process will wait for that number of
3782 * milliseconds before proceeding
3783 * @param {Object} [context=null] Execution context of the function. The context is
3784 * ignored if the step is a number or promise.
3785 * @return {Object} Step object, with `callback` and `context` properties
3786 */
3787 OO.ui.Process.prototype.createStep = function ( step, context ) {
3788 if ( typeof step === 'number' || $.isFunction( step.promise ) ) {
3789 return {
3790 callback: function () {
3791 return step;
3792 },
3793 context: null
3794 };
3795 }
3796 if ( $.isFunction( step ) ) {
3797 return {
3798 callback: step,
3799 context: context
3800 };
3801 }
3802 throw new Error( 'Cannot create process step: number, promise or function expected' );
3803 };
3804
3805 /**
3806 * Add step to the beginning of the process.
3807 *
3808 * @inheritdoc #createStep
3809 * @return {OO.ui.Process} this
3810 * @chainable
3811 */
3812 OO.ui.Process.prototype.first = function ( step, context ) {
3813 this.steps.unshift( this.createStep( step, context ) );
3814 return this;
3815 };
3816
3817 /**
3818 * Add step to the end of the process.
3819 *
3820 * @inheritdoc #createStep
3821 * @return {OO.ui.Process} this
3822 * @chainable
3823 */
3824 OO.ui.Process.prototype.next = function ( step, context ) {
3825 this.steps.push( this.createStep( step, context ) );
3826 return this;
3827 };
3828
3829 /**
3830 * A ToolFactory creates tools on demand. All tools ({@link OO.ui.Tool Tools}, {@link OO.ui.PopupTool PopupTools},
3831 * and {@link OO.ui.ToolGroupTool ToolGroupTools}) must be registered with a tool factory. Tools are
3832 * registered by their symbolic name. See {@link OO.ui.Toolbar toolbars} for an example.
3833 *
3834 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
3835 *
3836 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
3837 *
3838 * @class
3839 * @extends OO.Factory
3840 * @constructor
3841 */
3842 OO.ui.ToolFactory = function OoUiToolFactory() {
3843 // Parent constructor
3844 OO.ui.ToolFactory.parent.call( this );
3845 };
3846
3847 /* Setup */
3848
3849 OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
3850
3851 /* Methods */
3852
3853 /**
3854 * Get tools from the factory
3855 *
3856 * @param {Array} include Included tools
3857 * @param {Array} exclude Excluded tools
3858 * @param {Array} promote Promoted tools
3859 * @param {Array} demote Demoted tools
3860 * @return {string[]} List of tools
3861 */
3862 OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
3863 var i, len, included, promoted, demoted,
3864 auto = [],
3865 used = {};
3866
3867 // Collect included and not excluded tools
3868 included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
3869
3870 // Promotion
3871 promoted = this.extract( promote, used );
3872 demoted = this.extract( demote, used );
3873
3874 // Auto
3875 for ( i = 0, len = included.length; i < len; i++ ) {
3876 if ( !used[ included[ i ] ] ) {
3877 auto.push( included[ i ] );
3878 }
3879 }
3880
3881 return promoted.concat( auto ).concat( demoted );
3882 };
3883
3884 /**
3885 * Get a flat list of names from a list of names or groups.
3886 *
3887 * Tools can be specified in the following ways:
3888 *
3889 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
3890 * - All tools in a group: `{ group: 'group-name' }`
3891 * - All tools: `'*'`
3892 *
3893 * @private
3894 * @param {Array|string} collection List of tools
3895 * @param {Object} [used] Object with names that should be skipped as properties; extracted
3896 * names will be added as properties
3897 * @return {string[]} List of extracted names
3898 */
3899 OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
3900 var i, len, item, name, tool,
3901 names = [];
3902
3903 if ( collection === '*' ) {
3904 for ( name in this.registry ) {
3905 tool = this.registry[ name ];
3906 if (
3907 // Only add tools by group name when auto-add is enabled
3908 tool.static.autoAddToCatchall &&
3909 // Exclude already used tools
3910 ( !used || !used[ name ] )
3911 ) {
3912 names.push( name );
3913 if ( used ) {
3914 used[ name ] = true;
3915 }
3916 }
3917 }
3918 } else if ( Array.isArray( collection ) ) {
3919 for ( i = 0, len = collection.length; i < len; i++ ) {
3920 item = collection[ i ];
3921 // Allow plain strings as shorthand for named tools
3922 if ( typeof item === 'string' ) {
3923 item = { name: item };
3924 }
3925 if ( OO.isPlainObject( item ) ) {
3926 if ( item.group ) {
3927 for ( name in this.registry ) {
3928 tool = this.registry[ name ];
3929 if (
3930 // Include tools with matching group
3931 tool.static.group === item.group &&
3932 // Only add tools by group name when auto-add is enabled
3933 tool.static.autoAddToGroup &&
3934 // Exclude already used tools
3935 ( !used || !used[ name ] )
3936 ) {
3937 names.push( name );
3938 if ( used ) {
3939 used[ name ] = true;
3940 }
3941 }
3942 }
3943 // Include tools with matching name and exclude already used tools
3944 } else if ( item.name && ( !used || !used[ item.name ] ) ) {
3945 names.push( item.name );
3946 if ( used ) {
3947 used[ item.name ] = true;
3948 }
3949 }
3950 }
3951 }
3952 }
3953 return names;
3954 };
3955
3956 /**
3957 * ToolGroupFactories create {@link OO.ui.ToolGroup toolgroups} on demand. The toolgroup classes must
3958 * specify a symbolic name and be registered with the factory. The following classes are registered by
3959 * default:
3960 *
3961 * - {@link OO.ui.BarToolGroup BarToolGroups} (‘bar’)
3962 * - {@link OO.ui.MenuToolGroup MenuToolGroups} (‘menu’)
3963 * - {@link OO.ui.ListToolGroup ListToolGroups} (‘list’)
3964 *
3965 * See {@link OO.ui.Toolbar toolbars} for an example.
3966 *
3967 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
3968 *
3969 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
3970 * @class
3971 * @extends OO.Factory
3972 * @constructor
3973 */
3974 OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() {
3975 // Parent constructor
3976 OO.Factory.call( this );
3977
3978 var i, l,
3979 defaultClasses = this.constructor.static.getDefaultClasses();
3980
3981 // Register default toolgroups
3982 for ( i = 0, l = defaultClasses.length; i < l; i++ ) {
3983 this.register( defaultClasses[ i ] );
3984 }
3985 };
3986
3987 /* Setup */
3988
3989 OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory );
3990
3991 /* Static Methods */
3992
3993 /**
3994 * Get a default set of classes to be registered on construction.
3995 *
3996 * @return {Function[]} Default classes
3997 */
3998 OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
3999 return [
4000 OO.ui.BarToolGroup,
4001 OO.ui.ListToolGroup,
4002 OO.ui.MenuToolGroup
4003 ];
4004 };
4005
4006 /**
4007 * Theme logic.
4008 *
4009 * @abstract
4010 * @class
4011 *
4012 * @constructor
4013 * @param {Object} [config] Configuration options
4014 */
4015 OO.ui.Theme = function OoUiTheme( config ) {
4016 // Configuration initialization
4017 config = config || {};
4018 };
4019
4020 /* Setup */
4021
4022 OO.initClass( OO.ui.Theme );
4023
4024 /* Methods */
4025
4026 /**
4027 * Get a list of classes to be applied to a widget.
4028 *
4029 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
4030 * otherwise state transitions will not work properly.
4031 *
4032 * @param {OO.ui.Element} element Element for which to get classes
4033 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
4034 */
4035 OO.ui.Theme.prototype.getElementClasses = function ( /* element */ ) {
4036 return { on: [], off: [] };
4037 };
4038
4039 /**
4040 * Update CSS classes provided by the theme.
4041 *
4042 * For elements with theme logic hooks, this should be called any time there's a state change.
4043 *
4044 * @param {OO.ui.Element} element Element for which to update classes
4045 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
4046 */
4047 OO.ui.Theme.prototype.updateElementClasses = function ( element ) {
4048 var classes = this.getElementClasses( element );
4049
4050 element.$element
4051 .removeClass( classes.off.join( ' ' ) )
4052 .addClass( classes.on.join( ' ' ) );
4053 };
4054
4055 /**
4056 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
4057 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
4058 * order in which users will navigate through the focusable elements via the "tab" key.
4059 *
4060 * @example
4061 * // TabIndexedElement is mixed into the ButtonWidget class
4062 * // to provide a tabIndex property.
4063 * var button1 = new OO.ui.ButtonWidget( {
4064 * label: 'fourth',
4065 * tabIndex: 4
4066 * } );
4067 * var button2 = new OO.ui.ButtonWidget( {
4068 * label: 'second',
4069 * tabIndex: 2
4070 * } );
4071 * var button3 = new OO.ui.ButtonWidget( {
4072 * label: 'third',
4073 * tabIndex: 3
4074 * } );
4075 * var button4 = new OO.ui.ButtonWidget( {
4076 * label: 'first',
4077 * tabIndex: 1
4078 * } );
4079 * $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
4080 *
4081 * @abstract
4082 * @class
4083 *
4084 * @constructor
4085 * @param {Object} [config] Configuration options
4086 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
4087 * the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
4088 * functionality will be applied to it instead.
4089 * @cfg {number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
4090 * order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
4091 * to remove the element from the tab-navigation flow.
4092 */
4093 OO.ui.mixin.TabIndexedElement = function OoUiMixinTabIndexedElement( config ) {
4094 // Configuration initialization
4095 config = $.extend( { tabIndex: 0 }, config );
4096
4097 // Properties
4098 this.$tabIndexed = null;
4099 this.tabIndex = null;
4100
4101 // Events
4102 this.connect( this, { disable: 'onTabIndexedElementDisable' } );
4103
4104 // Initialization
4105 this.setTabIndex( config.tabIndex );
4106 this.setTabIndexedElement( config.$tabIndexed || this.$element );
4107 };
4108
4109 /* Setup */
4110
4111 OO.initClass( OO.ui.mixin.TabIndexedElement );
4112
4113 /* Methods */
4114
4115 /**
4116 * Set the element that should use the tabindex functionality.
4117 *
4118 * This method is used to retarget a tabindex mixin so that its functionality applies
4119 * to the specified element. If an element is currently using the functionality, the mixin’s
4120 * effect on that element is removed before the new element is set up.
4121 *
4122 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
4123 * @chainable
4124 */
4125 OO.ui.mixin.TabIndexedElement.prototype.setTabIndexedElement = function ( $tabIndexed ) {
4126 var tabIndex = this.tabIndex;
4127 // Remove attributes from old $tabIndexed
4128 this.setTabIndex( null );
4129 // Force update of new $tabIndexed
4130 this.$tabIndexed = $tabIndexed;
4131 this.tabIndex = tabIndex;
4132 return this.updateTabIndex();
4133 };
4134
4135 /**
4136 * Set the value of the tabindex.
4137 *
4138 * @param {number|null} tabIndex Tabindex value, or `null` for no tabindex
4139 * @chainable
4140 */
4141 OO.ui.mixin.TabIndexedElement.prototype.setTabIndex = function ( tabIndex ) {
4142 tabIndex = typeof tabIndex === 'number' ? tabIndex : null;
4143
4144 if ( this.tabIndex !== tabIndex ) {
4145 this.tabIndex = tabIndex;
4146 this.updateTabIndex();
4147 }
4148
4149 return this;
4150 };
4151
4152 /**
4153 * Update the `tabindex` attribute, in case of changes to tab index or
4154 * disabled state.
4155 *
4156 * @private
4157 * @chainable
4158 */
4159 OO.ui.mixin.TabIndexedElement.prototype.updateTabIndex = function () {
4160 if ( this.$tabIndexed ) {
4161 if ( this.tabIndex !== null ) {
4162 // Do not index over disabled elements
4163 this.$tabIndexed.attr( {
4164 tabindex: this.isDisabled() ? -1 : this.tabIndex,
4165 // ChromeVox and NVDA do not seem to inherit this from parent elements
4166 'aria-disabled': this.isDisabled().toString()
4167 } );
4168 } else {
4169 this.$tabIndexed.removeAttr( 'tabindex aria-disabled' );
4170 }
4171 }
4172 return this;
4173 };
4174
4175 /**
4176 * Handle disable events.
4177 *
4178 * @private
4179 * @param {boolean} disabled Element is disabled
4180 */
4181 OO.ui.mixin.TabIndexedElement.prototype.onTabIndexedElementDisable = function () {
4182 this.updateTabIndex();
4183 };
4184
4185 /**
4186 * Get the value of the tabindex.
4187 *
4188 * @return {number|null} Tabindex value
4189 */
4190 OO.ui.mixin.TabIndexedElement.prototype.getTabIndex = function () {
4191 return this.tabIndex;
4192 };
4193
4194 /**
4195 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
4196 * interface element that can be configured with access keys for accessibility.
4197 * See the [OOjs UI documentation on MediaWiki] [1] for examples.
4198 *
4199 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
4200 * @abstract
4201 * @class
4202 *
4203 * @constructor
4204 * @param {Object} [config] Configuration options
4205 * @cfg {jQuery} [$button] The button element created by the class.
4206 * If this configuration is omitted, the button element will use a generated `<a>`.
4207 * @cfg {boolean} [framed=true] Render the button with a frame
4208 * @cfg {string} [accessKey] Button's access key
4209 */
4210 OO.ui.mixin.ButtonElement = function OoUiMixinButtonElement( config ) {
4211 // Configuration initialization
4212 config = config || {};
4213
4214 // Properties
4215 this.$button = null;
4216 this.framed = null;
4217 this.accessKey = null;
4218 this.active = false;
4219 this.onMouseUpHandler = this.onMouseUp.bind( this );
4220 this.onMouseDownHandler = this.onMouseDown.bind( this );
4221 this.onKeyDownHandler = this.onKeyDown.bind( this );
4222 this.onKeyUpHandler = this.onKeyUp.bind( this );
4223 this.onClickHandler = this.onClick.bind( this );
4224 this.onKeyPressHandler = this.onKeyPress.bind( this );
4225
4226 // Initialization
4227 this.$element.addClass( 'oo-ui-buttonElement' );
4228 this.toggleFramed( config.framed === undefined || config.framed );
4229 this.setAccessKey( config.accessKey );
4230 this.setButtonElement( config.$button || $( '<a>' ) );
4231 };
4232
4233 /* Setup */
4234
4235 OO.initClass( OO.ui.mixin.ButtonElement );
4236
4237 /* Static Properties */
4238
4239 /**
4240 * Cancel mouse down events.
4241 *
4242 * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
4243 * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
4244 * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
4245 * parent widget.
4246 *
4247 * @static
4248 * @inheritable
4249 * @property {boolean}
4250 */
4251 OO.ui.mixin.ButtonElement.static.cancelButtonMouseDownEvents = true;
4252
4253 /* Events */
4254
4255 /**
4256 * A 'click' event is emitted when the button element is clicked.
4257 *
4258 * @event click
4259 */
4260
4261 /* Methods */
4262
4263 /**
4264 * Set the button element.
4265 *
4266 * This method is used to retarget a button mixin so that its functionality applies to
4267 * the specified button element instead of the one created by the class. If a button element
4268 * is already set, the method will remove the mixin’s effect on that element.
4269 *
4270 * @param {jQuery} $button Element to use as button
4271 */
4272 OO.ui.mixin.ButtonElement.prototype.setButtonElement = function ( $button ) {
4273 if ( this.$button ) {
4274 this.$button
4275 .removeClass( 'oo-ui-buttonElement-button' )
4276 .removeAttr( 'role accesskey' )
4277 .off( {
4278 mousedown: this.onMouseDownHandler,
4279 keydown: this.onKeyDownHandler,
4280 click: this.onClickHandler,
4281 keypress: this.onKeyPressHandler
4282 } );
4283 }
4284
4285 this.$button = $button
4286 .addClass( 'oo-ui-buttonElement-button' )
4287 .attr( { role: 'button', accesskey: this.accessKey } )
4288 .on( {
4289 mousedown: this.onMouseDownHandler,
4290 keydown: this.onKeyDownHandler,
4291 click: this.onClickHandler,
4292 keypress: this.onKeyPressHandler
4293 } );
4294 };
4295
4296 /**
4297 * Handles mouse down events.
4298 *
4299 * @protected
4300 * @param {jQuery.Event} e Mouse down event
4301 */
4302 OO.ui.mixin.ButtonElement.prototype.onMouseDown = function ( e ) {
4303 if ( this.isDisabled() || e.which !== 1 ) {
4304 return;
4305 }
4306 this.$element.addClass( 'oo-ui-buttonElement-pressed' );
4307 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
4308 // reliably remove the pressed class
4309 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
4310 // Prevent change of focus unless specifically configured otherwise
4311 if ( this.constructor.static.cancelButtonMouseDownEvents ) {
4312 return false;
4313 }
4314 };
4315
4316 /**
4317 * Handles mouse up events.
4318 *
4319 * @protected
4320 * @param {jQuery.Event} e Mouse up event
4321 */
4322 OO.ui.mixin.ButtonElement.prototype.onMouseUp = function ( e ) {
4323 if ( this.isDisabled() || e.which !== 1 ) {
4324 return;
4325 }
4326 this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
4327 // Stop listening for mouseup, since we only needed this once
4328 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
4329 };
4330
4331 /**
4332 * Handles mouse click events.
4333 *
4334 * @protected
4335 * @param {jQuery.Event} e Mouse click event
4336 * @fires click
4337 */
4338 OO.ui.mixin.ButtonElement.prototype.onClick = function ( e ) {
4339 if ( !this.isDisabled() && e.which === 1 ) {
4340 if ( this.emit( 'click' ) ) {
4341 return false;
4342 }
4343 }
4344 };
4345
4346 /**
4347 * Handles key down events.
4348 *
4349 * @protected
4350 * @param {jQuery.Event} e Key down event
4351 */
4352 OO.ui.mixin.ButtonElement.prototype.onKeyDown = function ( e ) {
4353 if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
4354 return;
4355 }
4356 this.$element.addClass( 'oo-ui-buttonElement-pressed' );
4357 // Run the keyup handler no matter where the key is when the button is let go, so we can
4358 // reliably remove the pressed class
4359 this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler, true );
4360 };
4361
4362 /**
4363 * Handles key up events.
4364 *
4365 * @protected
4366 * @param {jQuery.Event} e Key up event
4367 */
4368 OO.ui.mixin.ButtonElement.prototype.onKeyUp = function ( e ) {
4369 if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
4370 return;
4371 }
4372 this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
4373 // Stop listening for keyup, since we only needed this once
4374 this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler, true );
4375 };
4376
4377 /**
4378 * Handles key press events.
4379 *
4380 * @protected
4381 * @param {jQuery.Event} e Key press event
4382 * @fires click
4383 */
4384 OO.ui.mixin.ButtonElement.prototype.onKeyPress = function ( e ) {
4385 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
4386 if ( this.emit( 'click' ) ) {
4387 return false;
4388 }
4389 }
4390 };
4391
4392 /**
4393 * Check if button has a frame.
4394 *
4395 * @return {boolean} Button is framed
4396 */
4397 OO.ui.mixin.ButtonElement.prototype.isFramed = function () {
4398 return this.framed;
4399 };
4400
4401 /**
4402 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
4403 *
4404 * @param {boolean} [framed] Make button framed, omit to toggle
4405 * @chainable
4406 */
4407 OO.ui.mixin.ButtonElement.prototype.toggleFramed = function ( framed ) {
4408 framed = framed === undefined ? !this.framed : !!framed;
4409 if ( framed !== this.framed ) {
4410 this.framed = framed;
4411 this.$element
4412 .toggleClass( 'oo-ui-buttonElement-frameless', !framed )
4413 .toggleClass( 'oo-ui-buttonElement-framed', framed );
4414 this.updateThemeClasses();
4415 }
4416
4417 return this;
4418 };
4419
4420 /**
4421 * Set the button's access key.
4422 *
4423 * @param {string} accessKey Button's access key, use empty string to remove
4424 * @chainable
4425 */
4426 OO.ui.mixin.ButtonElement.prototype.setAccessKey = function ( accessKey ) {
4427 accessKey = typeof accessKey === 'string' && accessKey.length ? accessKey : null;
4428
4429 if ( this.accessKey !== accessKey ) {
4430 if ( this.$button ) {
4431 if ( accessKey !== null ) {
4432 this.$button.attr( 'accesskey', accessKey );
4433 } else {
4434 this.$button.removeAttr( 'accesskey' );
4435 }
4436 }
4437 this.accessKey = accessKey;
4438 }
4439
4440 return this;
4441 };
4442
4443 /**
4444 * Set the button to its 'active' state.
4445 *
4446 * The active state occurs when a {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} or
4447 * a {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} is pressed. This method does nothing
4448 * for other button types.
4449 *
4450 * @param {boolean} [value] Make button active
4451 * @chainable
4452 */
4453 OO.ui.mixin.ButtonElement.prototype.setActive = function ( value ) {
4454 this.$element.toggleClass( 'oo-ui-buttonElement-active', !!value );
4455 return this;
4456 };
4457
4458 /**
4459 * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
4460 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
4461 * items from the group is done through the interface the class provides.
4462 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
4463 *
4464 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
4465 *
4466 * @abstract
4467 * @class
4468 *
4469 * @constructor
4470 * @param {Object} [config] Configuration options
4471 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
4472 * is omitted, the group element will use a generated `<div>`.
4473 */
4474 OO.ui.mixin.GroupElement = function OoUiMixinGroupElement( config ) {
4475 // Configuration initialization
4476 config = config || {};
4477
4478 // Properties
4479 this.$group = null;
4480 this.items = [];
4481 this.aggregateItemEvents = {};
4482
4483 // Initialization
4484 this.setGroupElement( config.$group || $( '<div>' ) );
4485 };
4486
4487 /* Methods */
4488
4489 /**
4490 * Set the group element.
4491 *
4492 * If an element is already set, items will be moved to the new element.
4493 *
4494 * @param {jQuery} $group Element to use as group
4495 */
4496 OO.ui.mixin.GroupElement.prototype.setGroupElement = function ( $group ) {
4497 var i, len;
4498
4499 this.$group = $group;
4500 for ( i = 0, len = this.items.length; i < len; i++ ) {
4501 this.$group.append( this.items[ i ].$element );
4502 }
4503 };
4504
4505 /**
4506 * Check if a group contains no items.
4507 *
4508 * @return {boolean} Group is empty
4509 */
4510 OO.ui.mixin.GroupElement.prototype.isEmpty = function () {
4511 return !this.items.length;
4512 };
4513
4514 /**
4515 * Get all items in the group.
4516 *
4517 * The method returns an array of item references (e.g., [button1, button2, button3]) and is useful
4518 * when synchronizing groups of items, or whenever the references are required (e.g., when removing items
4519 * from a group).
4520 *
4521 * @return {OO.ui.Element[]} An array of items.
4522 */
4523 OO.ui.mixin.GroupElement.prototype.getItems = function () {
4524 return this.items.slice( 0 );
4525 };
4526
4527 /**
4528 * Get an item by its data.
4529 *
4530 * Only the first item with matching data will be returned. To return all matching items,
4531 * use the #getItemsFromData method.
4532 *
4533 * @param {Object} data Item data to search for
4534 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
4535 */
4536 OO.ui.mixin.GroupElement.prototype.getItemFromData = function ( data ) {
4537 var i, len, item,
4538 hash = OO.getHash( data );
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 return item;
4544 }
4545 }
4546
4547 return null;
4548 };
4549
4550 /**
4551 * Get items by their data.
4552 *
4553 * All items with matching data will be returned. To return only the first match, use the #getItemFromData method instead.
4554 *
4555 * @param {Object} data Item data to search for
4556 * @return {OO.ui.Element[]} Items with equivalent data
4557 */
4558 OO.ui.mixin.GroupElement.prototype.getItemsFromData = function ( data ) {
4559 var i, len, item,
4560 hash = OO.getHash( data ),
4561 items = [];
4562
4563 for ( i = 0, len = this.items.length; i < len; i++ ) {
4564 item = this.items[ i ];
4565 if ( hash === OO.getHash( item.getData() ) ) {
4566 items.push( item );
4567 }
4568 }
4569
4570 return items;
4571 };
4572
4573 /**
4574 * Aggregate the events emitted by the group.
4575 *
4576 * When events are aggregated, the group will listen to all contained items for the event,
4577 * and then emit the event under a new name. The new event will contain an additional leading
4578 * parameter containing the item that emitted the original event. Other arguments emitted from
4579 * the original event are passed through.
4580 *
4581 * @param {Object.<string,string|null>} events An object keyed by the name of the event that should be
4582 * aggregated (e.g., ‘click’) and the value of the new name to use (e.g., ‘groupClick’).
4583 * A `null` value will remove aggregated events.
4584
4585 * @throws {Error} An error is thrown if aggregation already exists.
4586 */
4587 OO.ui.mixin.GroupElement.prototype.aggregate = function ( events ) {
4588 var i, len, item, add, remove, itemEvent, groupEvent;
4589
4590 for ( itemEvent in events ) {
4591 groupEvent = events[ itemEvent ];
4592
4593 // Remove existing aggregated event
4594 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4595 // Don't allow duplicate aggregations
4596 if ( groupEvent ) {
4597 throw new Error( 'Duplicate item event aggregation for ' + itemEvent );
4598 }
4599 // Remove event aggregation from existing items
4600 for ( i = 0, len = this.items.length; i < len; i++ ) {
4601 item = this.items[ i ];
4602 if ( item.connect && item.disconnect ) {
4603 remove = {};
4604 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[itemEvent], item ];
4605 item.disconnect( this, remove );
4606 }
4607 }
4608 // Prevent future items from aggregating event
4609 delete this.aggregateItemEvents[ itemEvent ];
4610 }
4611
4612 // Add new aggregate event
4613 if ( groupEvent ) {
4614 // Make future items aggregate event
4615 this.aggregateItemEvents[ itemEvent ] = groupEvent;
4616 // Add event aggregation to existing items
4617 for ( i = 0, len = this.items.length; i < len; i++ ) {
4618 item = this.items[ i ];
4619 if ( item.connect && item.disconnect ) {
4620 add = {};
4621 add[ itemEvent ] = [ 'emit', groupEvent, item ];
4622 item.connect( this, add );
4623 }
4624 }
4625 }
4626 }
4627 };
4628
4629 /**
4630 * Add items to the group.
4631 *
4632 * Items will be added to the end of the group array unless the optional `index` parameter specifies
4633 * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
4634 *
4635 * @param {OO.ui.Element[]} items An array of items to add to the group
4636 * @param {number} [index] Index of the insertion point
4637 * @chainable
4638 */
4639 OO.ui.mixin.GroupElement.prototype.addItems = function ( items, index ) {
4640 var i, len, item, event, events, currentIndex,
4641 itemElements = [];
4642
4643 for ( i = 0, len = items.length; i < len; i++ ) {
4644 item = items[ i ];
4645
4646 // Check if item exists then remove it first, effectively "moving" it
4647 currentIndex = $.inArray( item, this.items );
4648 if ( currentIndex >= 0 ) {
4649 this.removeItems( [ item ] );
4650 // Adjust index to compensate for removal
4651 if ( currentIndex < index ) {
4652 index--;
4653 }
4654 }
4655 // Add the item
4656 if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
4657 events = {};
4658 for ( event in this.aggregateItemEvents ) {
4659 events[ event ] = [ 'emit', this.aggregateItemEvents[ event ], item ];
4660 }
4661 item.connect( this, events );
4662 }
4663 item.setElementGroup( this );
4664 itemElements.push( item.$element.get( 0 ) );
4665 }
4666
4667 if ( index === undefined || index < 0 || index >= this.items.length ) {
4668 this.$group.append( itemElements );
4669 this.items.push.apply( this.items, items );
4670 } else if ( index === 0 ) {
4671 this.$group.prepend( itemElements );
4672 this.items.unshift.apply( this.items, items );
4673 } else {
4674 this.items[ index ].$element.before( itemElements );
4675 this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
4676 }
4677
4678 return this;
4679 };
4680
4681 /**
4682 * Remove the specified items from a group.
4683 *
4684 * Removed items are detached (not removed) from the DOM so that they may be reused.
4685 * To remove all items from a group, you may wish to use the #clearItems method instead.
4686 *
4687 * @param {OO.ui.Element[]} items An array of items to remove
4688 * @chainable
4689 */
4690 OO.ui.mixin.GroupElement.prototype.removeItems = function ( items ) {
4691 var i, len, item, index, remove, itemEvent;
4692
4693 // Remove specific items
4694 for ( i = 0, len = items.length; i < len; i++ ) {
4695 item = items[ i ];
4696 index = $.inArray( item, this.items );
4697 if ( index !== -1 ) {
4698 if (
4699 item.connect && item.disconnect &&
4700 !$.isEmptyObject( this.aggregateItemEvents )
4701 ) {
4702 remove = {};
4703 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4704 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
4705 }
4706 item.disconnect( this, remove );
4707 }
4708 item.setElementGroup( null );
4709 this.items.splice( index, 1 );
4710 item.$element.detach();
4711 }
4712 }
4713
4714 return this;
4715 };
4716
4717 /**
4718 * Clear all items from the group.
4719 *
4720 * Cleared items are detached from the DOM, not removed, so that they may be reused.
4721 * To remove only a subset of items from a group, use the #removeItems method.
4722 *
4723 * @chainable
4724 */
4725 OO.ui.mixin.GroupElement.prototype.clearItems = function () {
4726 var i, len, item, remove, itemEvent;
4727
4728 // Remove all items
4729 for ( i = 0, len = this.items.length; i < len; i++ ) {
4730 item = this.items[ i ];
4731 if (
4732 item.connect && item.disconnect &&
4733 !$.isEmptyObject( this.aggregateItemEvents )
4734 ) {
4735 remove = {};
4736 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4737 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
4738 }
4739 item.disconnect( this, remove );
4740 }
4741 item.setElementGroup( null );
4742 item.$element.detach();
4743 }
4744
4745 this.items = [];
4746 return this;
4747 };
4748
4749 /**
4750 * DraggableElement is a mixin class used to create elements that can be clicked
4751 * and dragged by a mouse to a new position within a group. This class must be used
4752 * in conjunction with OO.ui.mixin.DraggableGroupElement, which provides a container for
4753 * the draggable elements.
4754 *
4755 * @abstract
4756 * @class
4757 *
4758 * @constructor
4759 */
4760 OO.ui.mixin.DraggableElement = function OoUiMixinDraggableElement() {
4761 // Properties
4762 this.index = null;
4763
4764 // Initialize and events
4765 this.$element
4766 .attr( 'draggable', true )
4767 .addClass( 'oo-ui-draggableElement' )
4768 .on( {
4769 dragstart: this.onDragStart.bind( this ),
4770 dragover: this.onDragOver.bind( this ),
4771 dragend: this.onDragEnd.bind( this ),
4772 drop: this.onDrop.bind( this )
4773 } );
4774 };
4775
4776 OO.initClass( OO.ui.mixin.DraggableElement );
4777
4778 /* Events */
4779
4780 /**
4781 * @event dragstart
4782 *
4783 * A dragstart event is emitted when the user clicks and begins dragging an item.
4784 * @param {OO.ui.mixin.DraggableElement} item The item the user has clicked and is dragging with the mouse.
4785 */
4786
4787 /**
4788 * @event dragend
4789 * A dragend event is emitted when the user drags an item and releases the mouse,
4790 * thus terminating the drag operation.
4791 */
4792
4793 /**
4794 * @event drop
4795 * A drop event is emitted when the user drags an item and then releases the mouse button
4796 * over a valid target.
4797 */
4798
4799 /* Static Properties */
4800
4801 /**
4802 * @inheritdoc OO.ui.mixin.ButtonElement
4803 */
4804 OO.ui.mixin.DraggableElement.static.cancelButtonMouseDownEvents = false;
4805
4806 /* Methods */
4807
4808 /**
4809 * Respond to dragstart event.
4810 *
4811 * @private
4812 * @param {jQuery.Event} event jQuery event
4813 * @fires dragstart
4814 */
4815 OO.ui.mixin.DraggableElement.prototype.onDragStart = function ( e ) {
4816 var dataTransfer = e.originalEvent.dataTransfer;
4817 // Define drop effect
4818 dataTransfer.dropEffect = 'none';
4819 dataTransfer.effectAllowed = 'move';
4820 // We must set up a dataTransfer data property or Firefox seems to
4821 // ignore the fact the element is draggable.
4822 try {
4823 dataTransfer.setData( 'application-x/OOjs-UI-draggable', this.getIndex() );
4824 } catch ( err ) {
4825 // The above is only for firefox. No need to set a catch clause
4826 // if it fails, move on.
4827 }
4828 // Add dragging class
4829 this.$element.addClass( 'oo-ui-draggableElement-dragging' );
4830 // Emit event
4831 this.emit( 'dragstart', this );
4832 return true;
4833 };
4834
4835 /**
4836 * Respond to dragend event.
4837 *
4838 * @private
4839 * @fires dragend
4840 */
4841 OO.ui.mixin.DraggableElement.prototype.onDragEnd = function () {
4842 this.$element.removeClass( 'oo-ui-draggableElement-dragging' );
4843 this.emit( 'dragend' );
4844 };
4845
4846 /**
4847 * Handle drop event.
4848 *
4849 * @private
4850 * @param {jQuery.Event} event jQuery event
4851 * @fires drop
4852 */
4853 OO.ui.mixin.DraggableElement.prototype.onDrop = function ( e ) {
4854 e.preventDefault();
4855 this.emit( 'drop', e );
4856 };
4857
4858 /**
4859 * In order for drag/drop to work, the dragover event must
4860 * return false and stop propogation.
4861 *
4862 * @private
4863 */
4864 OO.ui.mixin.DraggableElement.prototype.onDragOver = function ( e ) {
4865 e.preventDefault();
4866 };
4867
4868 /**
4869 * Set item index.
4870 * Store it in the DOM so we can access from the widget drag event
4871 *
4872 * @private
4873 * @param {number} Item index
4874 */
4875 OO.ui.mixin.DraggableElement.prototype.setIndex = function ( index ) {
4876 if ( this.index !== index ) {
4877 this.index = index;
4878 this.$element.data( 'index', index );
4879 }
4880 };
4881
4882 /**
4883 * Get item index
4884 *
4885 * @private
4886 * @return {number} Item index
4887 */
4888 OO.ui.mixin.DraggableElement.prototype.getIndex = function () {
4889 return this.index;
4890 };
4891
4892 /**
4893 * DraggableGroupElement is a mixin class used to create a group element to
4894 * contain draggable elements, which are items that can be clicked and dragged by a mouse.
4895 * The class is used with OO.ui.mixin.DraggableElement.
4896 *
4897 * @abstract
4898 * @class
4899 * @mixins OO.ui.mixin.GroupElement
4900 *
4901 * @constructor
4902 * @param {Object} [config] Configuration options
4903 * @cfg {string} [orientation] Item orientation: 'horizontal' or 'vertical'. The orientation
4904 * should match the layout of the items. Items displayed in a single row
4905 * or in several rows should use horizontal orientation. The vertical orientation should only be
4906 * used when the items are displayed in a single column. Defaults to 'vertical'
4907 */
4908 OO.ui.mixin.DraggableGroupElement = function OoUiMixinDraggableGroupElement( config ) {
4909 // Configuration initialization
4910 config = config || {};
4911
4912 // Parent constructor
4913 OO.ui.mixin.GroupElement.call( this, config );
4914
4915 // Properties
4916 this.orientation = config.orientation || 'vertical';
4917 this.dragItem = null;
4918 this.itemDragOver = null;
4919 this.itemKeys = {};
4920 this.sideInsertion = '';
4921
4922 // Events
4923 this.aggregate( {
4924 dragstart: 'itemDragStart',
4925 dragend: 'itemDragEnd',
4926 drop: 'itemDrop'
4927 } );
4928 this.connect( this, {
4929 itemDragStart: 'onItemDragStart',
4930 itemDrop: 'onItemDrop',
4931 itemDragEnd: 'onItemDragEnd'
4932 } );
4933 this.$element.on( {
4934 dragover: $.proxy( this.onDragOver, this ),
4935 dragleave: $.proxy( this.onDragLeave, this )
4936 } );
4937
4938 // Initialize
4939 if ( Array.isArray( config.items ) ) {
4940 this.addItems( config.items );
4941 }
4942 this.$placeholder = $( '<div>' )
4943 .addClass( 'oo-ui-draggableGroupElement-placeholder' );
4944 this.$element
4945 .addClass( 'oo-ui-draggableGroupElement' )
4946 .append( this.$status )
4947 .toggleClass( 'oo-ui-draggableGroupElement-horizontal', this.orientation === 'horizontal' )
4948 .prepend( this.$placeholder );
4949 };
4950
4951 /* Setup */
4952 OO.mixinClass( OO.ui.mixin.DraggableGroupElement, OO.ui.mixin.GroupElement );
4953
4954 /* Events */
4955
4956 /**
4957 * A 'reorder' event is emitted when the order of items in the group changes.
4958 *
4959 * @event reorder
4960 * @param {OO.ui.mixin.DraggableElement} item Reordered item
4961 * @param {number} [newIndex] New index for the item
4962 */
4963
4964 /* Methods */
4965
4966 /**
4967 * Respond to item drag start event
4968 *
4969 * @private
4970 * @param {OO.ui.mixin.DraggableElement} item Dragged item
4971 */
4972 OO.ui.mixin.DraggableGroupElement.prototype.onItemDragStart = function ( item ) {
4973 var i, len;
4974
4975 // Map the index of each object
4976 for ( i = 0, len = this.items.length; i < len; i++ ) {
4977 this.items[ i ].setIndex( i );
4978 }
4979
4980 if ( this.orientation === 'horizontal' ) {
4981 // Set the height of the indicator
4982 this.$placeholder.css( {
4983 height: item.$element.outerHeight(),
4984 width: 2
4985 } );
4986 } else {
4987 // Set the width of the indicator
4988 this.$placeholder.css( {
4989 height: 2,
4990 width: item.$element.outerWidth()
4991 } );
4992 }
4993 this.setDragItem( item );
4994 };
4995
4996 /**
4997 * Respond to item drag end event
4998 *
4999 * @private
5000 */
5001 OO.ui.mixin.DraggableGroupElement.prototype.onItemDragEnd = function () {
5002 this.unsetDragItem();
5003 return false;
5004 };
5005
5006 /**
5007 * Handle drop event and switch the order of the items accordingly
5008 *
5009 * @private
5010 * @param {OO.ui.mixin.DraggableElement} item Dropped item
5011 * @fires reorder
5012 */
5013 OO.ui.mixin.DraggableGroupElement.prototype.onItemDrop = function ( item ) {
5014 var toIndex = item.getIndex();
5015 // Check if the dropped item is from the current group
5016 // TODO: Figure out a way to configure a list of legally droppable
5017 // elements even if they are not yet in the list
5018 if ( this.getDragItem() ) {
5019 // If the insertion point is 'after', the insertion index
5020 // is shifted to the right (or to the left in RTL, hence 'after')
5021 if ( this.sideInsertion === 'after' ) {
5022 toIndex++;
5023 }
5024 // Emit change event
5025 this.emit( 'reorder', this.getDragItem(), toIndex );
5026 }
5027 this.unsetDragItem();
5028 // Return false to prevent propogation
5029 return false;
5030 };
5031
5032 /**
5033 * Handle dragleave event.
5034 *
5035 * @private
5036 */
5037 OO.ui.mixin.DraggableGroupElement.prototype.onDragLeave = function () {
5038 // This means the item was dragged outside the widget
5039 this.$placeholder
5040 .css( 'left', 0 )
5041 .addClass( 'oo-ui-element-hidden' );
5042 };
5043
5044 /**
5045 * Respond to dragover event
5046 *
5047 * @private
5048 * @param {jQuery.Event} event Event details
5049 */
5050 OO.ui.mixin.DraggableGroupElement.prototype.onDragOver = function ( e ) {
5051 var dragOverObj, $optionWidget, itemOffset, itemMidpoint, itemBoundingRect,
5052 itemSize, cssOutput, dragPosition, itemIndex, itemPosition,
5053 clientX = e.originalEvent.clientX,
5054 clientY = e.originalEvent.clientY;
5055
5056 // Get the OptionWidget item we are dragging over
5057 dragOverObj = this.getElementDocument().elementFromPoint( clientX, clientY );
5058 $optionWidget = $( dragOverObj ).closest( '.oo-ui-draggableElement' );
5059 if ( $optionWidget[ 0 ] ) {
5060 itemOffset = $optionWidget.offset();
5061 itemBoundingRect = $optionWidget[ 0 ].getBoundingClientRect();
5062 itemPosition = $optionWidget.position();
5063 itemIndex = $optionWidget.data( 'index' );
5064 }
5065
5066 if (
5067 itemOffset &&
5068 this.isDragging() &&
5069 itemIndex !== this.getDragItem().getIndex()
5070 ) {
5071 if ( this.orientation === 'horizontal' ) {
5072 // Calculate where the mouse is relative to the item width
5073 itemSize = itemBoundingRect.width;
5074 itemMidpoint = itemBoundingRect.left + itemSize / 2;
5075 dragPosition = clientX;
5076 // Which side of the item we hover over will dictate
5077 // where the placeholder will appear, on the left or
5078 // on the right
5079 cssOutput = {
5080 left: dragPosition < itemMidpoint ? itemPosition.left : itemPosition.left + itemSize,
5081 top: itemPosition.top
5082 };
5083 } else {
5084 // Calculate where the mouse is relative to the item height
5085 itemSize = itemBoundingRect.height;
5086 itemMidpoint = itemBoundingRect.top + itemSize / 2;
5087 dragPosition = clientY;
5088 // Which side of the item we hover over will dictate
5089 // where the placeholder will appear, on the top or
5090 // on the bottom
5091 cssOutput = {
5092 top: dragPosition < itemMidpoint ? itemPosition.top : itemPosition.top + itemSize,
5093 left: itemPosition.left
5094 };
5095 }
5096 // Store whether we are before or after an item to rearrange
5097 // For horizontal layout, we need to account for RTL, as this is flipped
5098 if ( this.orientation === 'horizontal' && this.$element.css( 'direction' ) === 'rtl' ) {
5099 this.sideInsertion = dragPosition < itemMidpoint ? 'after' : 'before';
5100 } else {
5101 this.sideInsertion = dragPosition < itemMidpoint ? 'before' : 'after';
5102 }
5103 // Add drop indicator between objects
5104 this.$placeholder
5105 .css( cssOutput )
5106 .removeClass( 'oo-ui-element-hidden' );
5107 } else {
5108 // This means the item was dragged outside the widget
5109 this.$placeholder
5110 .css( 'left', 0 )
5111 .addClass( 'oo-ui-element-hidden' );
5112 }
5113 // Prevent default
5114 e.preventDefault();
5115 };
5116
5117 /**
5118 * Set a dragged item
5119 *
5120 * @param {OO.ui.mixin.DraggableElement} item Dragged item
5121 */
5122 OO.ui.mixin.DraggableGroupElement.prototype.setDragItem = function ( item ) {
5123 this.dragItem = item;
5124 };
5125
5126 /**
5127 * Unset the current dragged item
5128 */
5129 OO.ui.mixin.DraggableGroupElement.prototype.unsetDragItem = function () {
5130 this.dragItem = null;
5131 this.itemDragOver = null;
5132 this.$placeholder.addClass( 'oo-ui-element-hidden' );
5133 this.sideInsertion = '';
5134 };
5135
5136 /**
5137 * Get the item that is currently being dragged.
5138 *
5139 * @return {OO.ui.mixin.DraggableElement|null} The currently dragged item, or `null` if no item is being dragged
5140 */
5141 OO.ui.mixin.DraggableGroupElement.prototype.getDragItem = function () {
5142 return this.dragItem;
5143 };
5144
5145 /**
5146 * Check if an item in the group is currently being dragged.
5147 *
5148 * @return {Boolean} Item is being dragged
5149 */
5150 OO.ui.mixin.DraggableGroupElement.prototype.isDragging = function () {
5151 return this.getDragItem() !== null;
5152 };
5153
5154 /**
5155 * IconElement is often mixed into other classes to generate an icon.
5156 * Icons are graphics, about the size of normal text. They are used to aid the user
5157 * in locating a control or to convey information in a space-efficient way. See the
5158 * [OOjs UI documentation on MediaWiki] [1] for a list of icons
5159 * included in the library.
5160 *
5161 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
5162 *
5163 * @abstract
5164 * @class
5165 *
5166 * @constructor
5167 * @param {Object} [config] Configuration options
5168 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
5169 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
5170 * the icon element be set to an existing icon instead of the one generated by this class, set a
5171 * value using a jQuery selection. For example:
5172 *
5173 * // Use a <div> tag instead of a <span>
5174 * $icon: $("<div>")
5175 * // Use an existing icon element instead of the one generated by the class
5176 * $icon: this.$element
5177 * // Use an icon element from a child widget
5178 * $icon: this.childwidget.$element
5179 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
5180 * symbolic names. A map is used for i18n purposes and contains a `default` icon
5181 * name and additional names keyed by language code. The `default` name is used when no icon is keyed
5182 * by the user's language.
5183 *
5184 * Example of an i18n map:
5185 *
5186 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
5187 * See the [OOjs UI documentation on MediaWiki] [2] for a list of icons included in the library.
5188 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
5189 * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
5190 * text. The icon title is displayed when users move the mouse over the icon.
5191 */
5192 OO.ui.mixin.IconElement = function OoUiMixinIconElement( config ) {
5193 // Configuration initialization
5194 config = config || {};
5195
5196 // Properties
5197 this.$icon = null;
5198 this.icon = null;
5199 this.iconTitle = null;
5200
5201 // Initialization
5202 this.setIcon( config.icon || this.constructor.static.icon );
5203 this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle );
5204 this.setIconElement( config.$icon || $( '<span>' ) );
5205 };
5206
5207 /* Setup */
5208
5209 OO.initClass( OO.ui.mixin.IconElement );
5210
5211 /* Static Properties */
5212
5213 /**
5214 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
5215 * for i18n purposes and contains a `default` icon name and additional names keyed by
5216 * language code. The `default` name is used when no icon is keyed by the user's language.
5217 *
5218 * Example of an i18n map:
5219 *
5220 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
5221 *
5222 * Note: the static property will be overridden if the #icon configuration is used.
5223 *
5224 * @static
5225 * @inheritable
5226 * @property {Object|string}
5227 */
5228 OO.ui.mixin.IconElement.static.icon = null;
5229
5230 /**
5231 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
5232 * function that returns title text, or `null` for no title.
5233 *
5234 * The static property will be overridden if the #iconTitle configuration is used.
5235 *
5236 * @static
5237 * @inheritable
5238 * @property {string|Function|null}
5239 */
5240 OO.ui.mixin.IconElement.static.iconTitle = null;
5241
5242 /* Methods */
5243
5244 /**
5245 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
5246 * applies to the specified icon element instead of the one created by the class. If an icon
5247 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
5248 * and mixin methods will no longer affect the element.
5249 *
5250 * @param {jQuery} $icon Element to use as icon
5251 */
5252 OO.ui.mixin.IconElement.prototype.setIconElement = function ( $icon ) {
5253 if ( this.$icon ) {
5254 this.$icon
5255 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon )
5256 .removeAttr( 'title' );
5257 }
5258
5259 this.$icon = $icon
5260 .addClass( 'oo-ui-iconElement-icon' )
5261 .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon );
5262 if ( this.iconTitle !== null ) {
5263 this.$icon.attr( 'title', this.iconTitle );
5264 }
5265 };
5266
5267 /**
5268 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
5269 * The icon parameter can also be set to a map of icon names. See the #icon config setting
5270 * for an example.
5271 *
5272 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
5273 * by language code, or `null` to remove the icon.
5274 * @chainable
5275 */
5276 OO.ui.mixin.IconElement.prototype.setIcon = function ( icon ) {
5277 icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
5278 icon = typeof icon === 'string' && icon.trim().length ? icon.trim() : null;
5279
5280 if ( this.icon !== icon ) {
5281 if ( this.$icon ) {
5282 if ( this.icon !== null ) {
5283 this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
5284 }
5285 if ( icon !== null ) {
5286 this.$icon.addClass( 'oo-ui-icon-' + icon );
5287 }
5288 }
5289 this.icon = icon;
5290 }
5291
5292 this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon );
5293 this.updateThemeClasses();
5294
5295 return this;
5296 };
5297
5298 /**
5299 * Set the icon title. Use `null` to remove the title.
5300 *
5301 * @param {string|Function|null} iconTitle A text string used as the icon title,
5302 * a function that returns title text, or `null` for no title.
5303 * @chainable
5304 */
5305 OO.ui.mixin.IconElement.prototype.setIconTitle = function ( iconTitle ) {
5306 iconTitle = typeof iconTitle === 'function' ||
5307 ( typeof iconTitle === 'string' && iconTitle.length ) ?
5308 OO.ui.resolveMsg( iconTitle ) : null;
5309
5310 if ( this.iconTitle !== iconTitle ) {
5311 this.iconTitle = iconTitle;
5312 if ( this.$icon ) {
5313 if ( this.iconTitle !== null ) {
5314 this.$icon.attr( 'title', iconTitle );
5315 } else {
5316 this.$icon.removeAttr( 'title' );
5317 }
5318 }
5319 }
5320
5321 return this;
5322 };
5323
5324 /**
5325 * Get the symbolic name of the icon.
5326 *
5327 * @return {string} Icon name
5328 */
5329 OO.ui.mixin.IconElement.prototype.getIcon = function () {
5330 return this.icon;
5331 };
5332
5333 /**
5334 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
5335 *
5336 * @return {string} Icon title text
5337 */
5338 OO.ui.mixin.IconElement.prototype.getIconTitle = function () {
5339 return this.iconTitle;
5340 };
5341
5342 /**
5343 * IndicatorElement is often mixed into other classes to generate an indicator.
5344 * Indicators are small graphics that are generally used in two ways:
5345 *
5346 * - To draw attention to the status of an item. For example, an indicator might be
5347 * used to show that an item in a list has errors that need to be resolved.
5348 * - To clarify the function of a control that acts in an exceptional way (a button
5349 * that opens a menu instead of performing an action directly, for example).
5350 *
5351 * For a list of indicators included in the library, please see the
5352 * [OOjs UI documentation on MediaWiki] [1].
5353 *
5354 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
5355 *
5356 * @abstract
5357 * @class
5358 *
5359 * @constructor
5360 * @param {Object} [config] Configuration options
5361 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
5362 * configuration is omitted, the indicator element will use a generated `<span>`.
5363 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
5364 * See the [OOjs UI documentation on MediaWiki][2] for a list of indicators included
5365 * in the library.
5366 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
5367 * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
5368 * or a function that returns title text. The indicator title is displayed when users move
5369 * the mouse over the indicator.
5370 */
5371 OO.ui.mixin.IndicatorElement = function OoUiMixinIndicatorElement( config ) {
5372 // Configuration initialization
5373 config = config || {};
5374
5375 // Properties
5376 this.$indicator = null;
5377 this.indicator = null;
5378 this.indicatorTitle = null;
5379
5380 // Initialization
5381 this.setIndicator( config.indicator || this.constructor.static.indicator );
5382 this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle );
5383 this.setIndicatorElement( config.$indicator || $( '<span>' ) );
5384 };
5385
5386 /* Setup */
5387
5388 OO.initClass( OO.ui.mixin.IndicatorElement );
5389
5390 /* Static Properties */
5391
5392 /**
5393 * Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
5394 * The static property will be overridden if the #indicator configuration is used.
5395 *
5396 * @static
5397 * @inheritable
5398 * @property {string|null}
5399 */
5400 OO.ui.mixin.IndicatorElement.static.indicator = null;
5401
5402 /**
5403 * A text string used as the indicator title, a function that returns title text, or `null`
5404 * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
5405 *
5406 * @static
5407 * @inheritable
5408 * @property {string|Function|null}
5409 */
5410 OO.ui.mixin.IndicatorElement.static.indicatorTitle = null;
5411
5412 /* Methods */
5413
5414 /**
5415 * Set the indicator element.
5416 *
5417 * If an element is already set, it will be cleaned up before setting up the new element.
5418 *
5419 * @param {jQuery} $indicator Element to use as indicator
5420 */
5421 OO.ui.mixin.IndicatorElement.prototype.setIndicatorElement = function ( $indicator ) {
5422 if ( this.$indicator ) {
5423 this.$indicator
5424 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator )
5425 .removeAttr( 'title' );
5426 }
5427
5428 this.$indicator = $indicator
5429 .addClass( 'oo-ui-indicatorElement-indicator' )
5430 .toggleClass( 'oo-ui-indicator-' + this.indicator, !!this.indicator );
5431 if ( this.indicatorTitle !== null ) {
5432 this.$indicator.attr( 'title', this.indicatorTitle );
5433 }
5434 };
5435
5436 /**
5437 * Set the indicator by its symbolic name: ‘alert’, ‘down’, ‘next’, ‘previous’, ‘required’, ‘up’. Use `null` to remove the indicator.
5438 *
5439 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
5440 * @chainable
5441 */
5442 OO.ui.mixin.IndicatorElement.prototype.setIndicator = function ( indicator ) {
5443 indicator = typeof indicator === 'string' && indicator.length ? indicator.trim() : null;
5444
5445 if ( this.indicator !== indicator ) {
5446 if ( this.$indicator ) {
5447 if ( this.indicator !== null ) {
5448 this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
5449 }
5450 if ( indicator !== null ) {
5451 this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
5452 }
5453 }
5454 this.indicator = indicator;
5455 }
5456
5457 this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator );
5458 this.updateThemeClasses();
5459
5460 return this;
5461 };
5462
5463 /**
5464 * Set the indicator title.
5465 *
5466 * The title is displayed when a user moves the mouse over the indicator.
5467 *
5468 * @param {string|Function|null} indicator Indicator title text, a function that returns text, or
5469 * `null` for no indicator title
5470 * @chainable
5471 */
5472 OO.ui.mixin.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
5473 indicatorTitle = typeof indicatorTitle === 'function' ||
5474 ( typeof indicatorTitle === 'string' && indicatorTitle.length ) ?
5475 OO.ui.resolveMsg( indicatorTitle ) : null;
5476
5477 if ( this.indicatorTitle !== indicatorTitle ) {
5478 this.indicatorTitle = indicatorTitle;
5479 if ( this.$indicator ) {
5480 if ( this.indicatorTitle !== null ) {
5481 this.$indicator.attr( 'title', indicatorTitle );
5482 } else {
5483 this.$indicator.removeAttr( 'title' );
5484 }
5485 }
5486 }
5487
5488 return this;
5489 };
5490
5491 /**
5492 * Get the symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
5493 *
5494 * @return {string} Symbolic name of indicator
5495 */
5496 OO.ui.mixin.IndicatorElement.prototype.getIndicator = function () {
5497 return this.indicator;
5498 };
5499
5500 /**
5501 * Get the indicator title.
5502 *
5503 * The title is displayed when a user moves the mouse over the indicator.
5504 *
5505 * @return {string} Indicator title text
5506 */
5507 OO.ui.mixin.IndicatorElement.prototype.getIndicatorTitle = function () {
5508 return this.indicatorTitle;
5509 };
5510
5511 /**
5512 * LabelElement is often mixed into other classes to generate a label, which
5513 * helps identify the function of an interface element.
5514 * See the [OOjs UI documentation on MediaWiki] [1] for more information.
5515 *
5516 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
5517 *
5518 * @abstract
5519 * @class
5520 *
5521 * @constructor
5522 * @param {Object} [config] Configuration options
5523 * @cfg {jQuery} [$label] The label element created by the class. If this
5524 * configuration is omitted, the label element will use a generated `<span>`.
5525 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified
5526 * as a plaintext string, a jQuery selection of elements, or a function that will produce a string
5527 * in the future. See the [OOjs UI documentation on MediaWiki] [2] for examples.
5528 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
5529 * @cfg {boolean} [autoFitLabel=true] Fit the label to the width of the parent element.
5530 * The label will be truncated to fit if necessary.
5531 */
5532 OO.ui.mixin.LabelElement = function OoUiMixinLabelElement( config ) {
5533 // Configuration initialization
5534 config = config || {};
5535
5536 // Properties
5537 this.$label = null;
5538 this.label = null;
5539 this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel;
5540
5541 // Initialization
5542 this.setLabel( config.label || this.constructor.static.label );
5543 this.setLabelElement( config.$label || $( '<span>' ) );
5544 };
5545
5546 /* Setup */
5547
5548 OO.initClass( OO.ui.mixin.LabelElement );
5549
5550 /* Events */
5551
5552 /**
5553 * @event labelChange
5554 * @param {string} value
5555 */
5556
5557 /* Static Properties */
5558
5559 /**
5560 * The label text. The label can be specified as a plaintext string, a function that will
5561 * produce a string in the future, or `null` for no label. The static value will
5562 * be overridden if a label is specified with the #label config option.
5563 *
5564 * @static
5565 * @inheritable
5566 * @property {string|Function|null}
5567 */
5568 OO.ui.mixin.LabelElement.static.label = null;
5569
5570 /* Methods */
5571
5572 /**
5573 * Set the label element.
5574 *
5575 * If an element is already set, it will be cleaned up before setting up the new element.
5576 *
5577 * @param {jQuery} $label Element to use as label
5578 */
5579 OO.ui.mixin.LabelElement.prototype.setLabelElement = function ( $label ) {
5580 if ( this.$label ) {
5581 this.$label.removeClass( 'oo-ui-labelElement-label' ).empty();
5582 }
5583
5584 this.$label = $label.addClass( 'oo-ui-labelElement-label' );
5585 this.setLabelContent( this.label );
5586 };
5587
5588 /**
5589 * Set the label.
5590 *
5591 * An empty string will result in the label being hidden. A string containing only whitespace will
5592 * be converted to a single `&nbsp;`.
5593 *
5594 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
5595 * text; or null for no label
5596 * @chainable
5597 */
5598 OO.ui.mixin.LabelElement.prototype.setLabel = function ( label ) {
5599 label = typeof label === 'function' ? OO.ui.resolveMsg( label ) : label;
5600 label = ( ( typeof label === 'string' && label.length ) || label instanceof jQuery || label instanceof OO.ui.HtmlSnippet ) ? label : null;
5601
5602 this.$element.toggleClass( 'oo-ui-labelElement', !!label );
5603
5604 if ( this.label !== label ) {
5605 if ( this.$label ) {
5606 this.setLabelContent( label );
5607 }
5608 this.label = label;
5609 this.emit( 'labelChange' );
5610 }
5611
5612 return this;
5613 };
5614
5615 /**
5616 * Get the label.
5617 *
5618 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
5619 * text; or null for no label
5620 */
5621 OO.ui.mixin.LabelElement.prototype.getLabel = function () {
5622 return this.label;
5623 };
5624
5625 /**
5626 * Fit the label.
5627 *
5628 * @chainable
5629 */
5630 OO.ui.mixin.LabelElement.prototype.fitLabel = function () {
5631 if ( this.$label && this.$label.autoEllipsis && this.autoFitLabel ) {
5632 this.$label.autoEllipsis( { hasSpan: false, tooltip: true } );
5633 }
5634
5635 return this;
5636 };
5637
5638 /**
5639 * Set the content of the label.
5640 *
5641 * Do not call this method until after the label element has been set by #setLabelElement.
5642 *
5643 * @private
5644 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
5645 * text; or null for no label
5646 */
5647 OO.ui.mixin.LabelElement.prototype.setLabelContent = function ( label ) {
5648 if ( typeof label === 'string' ) {
5649 if ( label.match( /^\s*$/ ) ) {
5650 // Convert whitespace only string to a single non-breaking space
5651 this.$label.html( '&nbsp;' );
5652 } else {
5653 this.$label.text( label );
5654 }
5655 } else if ( label instanceof OO.ui.HtmlSnippet ) {
5656 this.$label.html( label.toString() );
5657 } else if ( label instanceof jQuery ) {
5658 this.$label.empty().append( label );
5659 } else {
5660 this.$label.empty();
5661 }
5662 };
5663
5664 /**
5665 * LookupElement is a mixin that creates a {@link OO.ui.TextInputMenuSelectWidget menu} of suggested values for
5666 * a {@link OO.ui.TextInputWidget text input widget}. Suggested values are based on the characters the user types
5667 * into the text input field and, in general, the menu is only displayed when the user types. If a suggested value is chosen
5668 * from the lookup menu, that value becomes the value of the input field.
5669 *
5670 * Note that a new menu of suggested items is displayed when a value is chosen from the lookup menu. If this is
5671 * not the desired behavior, disable lookup menus with the #setLookupsDisabled method, then set the value, then
5672 * re-enable lookups.
5673 *
5674 * See the [OOjs UI demos][1] for an example.
5675 *
5676 * [1]: https://tools.wmflabs.org/oojs-ui/oojs-ui/demos/index.html#widgets-apex-vector-ltr
5677 *
5678 * @class
5679 * @abstract
5680 *
5681 * @constructor
5682 * @param {Object} [config] Configuration options
5683 * @cfg {jQuery} [$overlay] Overlay for the lookup menu; defaults to relative positioning
5684 * @cfg {jQuery} [$container=this.$element] The container element. The lookup menu is rendered beneath the specified element.
5685 * @cfg {boolean} [allowSuggestionsWhenEmpty=false] Request and display a lookup menu when the text input is empty.
5686 * By default, the lookup menu is not generated and displayed until the user begins to type.
5687 */
5688 OO.ui.mixin.LookupElement = function OoUiMixinLookupElement( config ) {
5689 // Configuration initialization
5690 config = config || {};
5691
5692 // Properties
5693 this.$overlay = config.$overlay || this.$element;
5694 this.lookupMenu = new OO.ui.TextInputMenuSelectWidget( this, {
5695 widget: this,
5696 input: this,
5697 $container: config.$container
5698 } );
5699
5700 this.allowSuggestionsWhenEmpty = config.allowSuggestionsWhenEmpty || false;
5701
5702 this.lookupCache = {};
5703 this.lookupQuery = null;
5704 this.lookupRequest = null;
5705 this.lookupsDisabled = false;
5706 this.lookupInputFocused = false;
5707
5708 // Events
5709 this.$input.on( {
5710 focus: this.onLookupInputFocus.bind( this ),
5711 blur: this.onLookupInputBlur.bind( this ),
5712 mousedown: this.onLookupInputMouseDown.bind( this )
5713 } );
5714 this.connect( this, { change: 'onLookupInputChange' } );
5715 this.lookupMenu.connect( this, {
5716 toggle: 'onLookupMenuToggle',
5717 choose: 'onLookupMenuItemChoose'
5718 } );
5719
5720 // Initialization
5721 this.$element.addClass( 'oo-ui-lookupElement' );
5722 this.lookupMenu.$element.addClass( 'oo-ui-lookupElement-menu' );
5723 this.$overlay.append( this.lookupMenu.$element );
5724 };
5725
5726 /* Methods */
5727
5728 /**
5729 * Handle input focus event.
5730 *
5731 * @protected
5732 * @param {jQuery.Event} e Input focus event
5733 */
5734 OO.ui.mixin.LookupElement.prototype.onLookupInputFocus = function () {
5735 this.lookupInputFocused = true;
5736 this.populateLookupMenu();
5737 };
5738
5739 /**
5740 * Handle input blur event.
5741 *
5742 * @protected
5743 * @param {jQuery.Event} e Input blur event
5744 */
5745 OO.ui.mixin.LookupElement.prototype.onLookupInputBlur = function () {
5746 this.closeLookupMenu();
5747 this.lookupInputFocused = false;
5748 };
5749
5750 /**
5751 * Handle input mouse down event.
5752 *
5753 * @protected
5754 * @param {jQuery.Event} e Input mouse down event
5755 */
5756 OO.ui.mixin.LookupElement.prototype.onLookupInputMouseDown = function () {
5757 // Only open the menu if the input was already focused.
5758 // This way we allow the user to open the menu again after closing it with Esc
5759 // by clicking in the input. Opening (and populating) the menu when initially
5760 // clicking into the input is handled by the focus handler.
5761 if ( this.lookupInputFocused && !this.lookupMenu.isVisible() ) {
5762 this.populateLookupMenu();
5763 }
5764 };
5765
5766 /**
5767 * Handle input change event.
5768 *
5769 * @protected
5770 * @param {string} value New input value
5771 */
5772 OO.ui.mixin.LookupElement.prototype.onLookupInputChange = function () {
5773 if ( this.lookupInputFocused ) {
5774 this.populateLookupMenu();
5775 }
5776 };
5777
5778 /**
5779 * Handle the lookup menu being shown/hidden.
5780 *
5781 * @protected
5782 * @param {boolean} visible Whether the lookup menu is now visible.
5783 */
5784 OO.ui.mixin.LookupElement.prototype.onLookupMenuToggle = function ( visible ) {
5785 if ( !visible ) {
5786 // When the menu is hidden, abort any active request and clear the menu.
5787 // This has to be done here in addition to closeLookupMenu(), because
5788 // MenuSelectWidget will close itself when the user presses Esc.
5789 this.abortLookupRequest();
5790 this.lookupMenu.clearItems();
5791 }
5792 };
5793
5794 /**
5795 * Handle menu item 'choose' event, updating the text input value to the value of the clicked item.
5796 *
5797 * @protected
5798 * @param {OO.ui.MenuOptionWidget} item Selected item
5799 */
5800 OO.ui.mixin.LookupElement.prototype.onLookupMenuItemChoose = function ( item ) {
5801 this.setValue( item.getData() );
5802 };
5803
5804 /**
5805 * Get lookup menu.
5806 *
5807 * @private
5808 * @return {OO.ui.TextInputMenuSelectWidget}
5809 */
5810 OO.ui.mixin.LookupElement.prototype.getLookupMenu = function () {
5811 return this.lookupMenu;
5812 };
5813
5814 /**
5815 * Disable or re-enable lookups.
5816 *
5817 * When lookups are disabled, calls to #populateLookupMenu will be ignored.
5818 *
5819 * @param {boolean} disabled Disable lookups
5820 */
5821 OO.ui.mixin.LookupElement.prototype.setLookupsDisabled = function ( disabled ) {
5822 this.lookupsDisabled = !!disabled;
5823 };
5824
5825 /**
5826 * Open the menu. If there are no entries in the menu, this does nothing.
5827 *
5828 * @private
5829 * @chainable
5830 */
5831 OO.ui.mixin.LookupElement.prototype.openLookupMenu = function () {
5832 if ( !this.lookupMenu.isEmpty() ) {
5833 this.lookupMenu.toggle( true );
5834 }
5835 return this;
5836 };
5837
5838 /**
5839 * Close the menu, empty it, and abort any pending request.
5840 *
5841 * @private
5842 * @chainable
5843 */
5844 OO.ui.mixin.LookupElement.prototype.closeLookupMenu = function () {
5845 this.lookupMenu.toggle( false );
5846 this.abortLookupRequest();
5847 this.lookupMenu.clearItems();
5848 return this;
5849 };
5850
5851 /**
5852 * Request menu items based on the input's current value, and when they arrive,
5853 * populate the menu with these items and show the menu.
5854 *
5855 * If lookups have been disabled with #setLookupsDisabled, this function does nothing.
5856 *
5857 * @private
5858 * @chainable
5859 */
5860 OO.ui.mixin.LookupElement.prototype.populateLookupMenu = function () {
5861 var widget = this,
5862 value = this.getValue();
5863
5864 if ( this.lookupsDisabled ) {
5865 return;
5866 }
5867
5868 // If the input is empty, clear the menu, unless suggestions when empty are allowed.
5869 if ( !this.allowSuggestionsWhenEmpty && value === '' ) {
5870 this.closeLookupMenu();
5871 // Skip population if there is already a request pending for the current value
5872 } else if ( value !== this.lookupQuery ) {
5873 this.getLookupMenuItems()
5874 .done( function ( items ) {
5875 widget.lookupMenu.clearItems();
5876 if ( items.length ) {
5877 widget.lookupMenu
5878 .addItems( items )
5879 .toggle( true );
5880 widget.initializeLookupMenuSelection();
5881 } else {
5882 widget.lookupMenu.toggle( false );
5883 }
5884 } )
5885 .fail( function () {
5886 widget.lookupMenu.clearItems();
5887 } );
5888 }
5889
5890 return this;
5891 };
5892
5893 /**
5894 * Highlight the first selectable item in the menu.
5895 *
5896 * @private
5897 * @chainable
5898 */
5899 OO.ui.mixin.LookupElement.prototype.initializeLookupMenuSelection = function () {
5900 if ( !this.lookupMenu.getSelectedItem() ) {
5901 this.lookupMenu.highlightItem( this.lookupMenu.getFirstSelectableItem() );
5902 }
5903 };
5904
5905 /**
5906 * Get lookup menu items for the current query.
5907 *
5908 * @private
5909 * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument of
5910 * the done event. If the request was aborted to make way for a subsequent request, this promise
5911 * will not be rejected: it will remain pending forever.
5912 */
5913 OO.ui.mixin.LookupElement.prototype.getLookupMenuItems = function () {
5914 var widget = this,
5915 value = this.getValue(),
5916 deferred = $.Deferred(),
5917 ourRequest;
5918
5919 this.abortLookupRequest();
5920 if ( Object.prototype.hasOwnProperty.call( this.lookupCache, value ) ) {
5921 deferred.resolve( this.getLookupMenuOptionsFromData( this.lookupCache[ value ] ) );
5922 } else {
5923 this.pushPending();
5924 this.lookupQuery = value;
5925 ourRequest = this.lookupRequest = this.getLookupRequest();
5926 ourRequest
5927 .always( function () {
5928 // We need to pop pending even if this is an old request, otherwise
5929 // the widget will remain pending forever.
5930 // TODO: this assumes that an aborted request will fail or succeed soon after
5931 // being aborted, or at least eventually. It would be nice if we could popPending()
5932 // at abort time, but only if we knew that we hadn't already called popPending()
5933 // for that request.
5934 widget.popPending();
5935 } )
5936 .done( function ( response ) {
5937 // If this is an old request (and aborting it somehow caused it to still succeed),
5938 // ignore its success completely
5939 if ( ourRequest === widget.lookupRequest ) {
5940 widget.lookupQuery = null;
5941 widget.lookupRequest = null;
5942 widget.lookupCache[ value ] = widget.getLookupCacheDataFromResponse( response );
5943 deferred.resolve( widget.getLookupMenuOptionsFromData( widget.lookupCache[ value ] ) );
5944 }
5945 } )
5946 .fail( function () {
5947 // If this is an old request (or a request failing because it's being aborted),
5948 // ignore its failure completely
5949 if ( ourRequest === widget.lookupRequest ) {
5950 widget.lookupQuery = null;
5951 widget.lookupRequest = null;
5952 deferred.reject();
5953 }
5954 } );
5955 }
5956 return deferred.promise();
5957 };
5958
5959 /**
5960 * Abort the currently pending lookup request, if any.
5961 *
5962 * @private
5963 */
5964 OO.ui.mixin.LookupElement.prototype.abortLookupRequest = function () {
5965 var oldRequest = this.lookupRequest;
5966 if ( oldRequest ) {
5967 // First unset this.lookupRequest to the fail handler will notice
5968 // that the request is no longer current
5969 this.lookupRequest = null;
5970 this.lookupQuery = null;
5971 oldRequest.abort();
5972 }
5973 };
5974
5975 /**
5976 * Get a new request object of the current lookup query value.
5977 *
5978 * @protected
5979 * @abstract
5980 * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
5981 */
5982 OO.ui.mixin.LookupElement.prototype.getLookupRequest = function () {
5983 // Stub, implemented in subclass
5984 return null;
5985 };
5986
5987 /**
5988 * Pre-process data returned by the request from #getLookupRequest.
5989 *
5990 * The return value of this function will be cached, and any further queries for the given value
5991 * will use the cache rather than doing API requests.
5992 *
5993 * @protected
5994 * @abstract
5995 * @param {Mixed} response Response from server
5996 * @return {Mixed} Cached result data
5997 */
5998 OO.ui.mixin.LookupElement.prototype.getLookupCacheDataFromResponse = function () {
5999 // Stub, implemented in subclass
6000 return [];
6001 };
6002
6003 /**
6004 * Get a list of menu option widgets from the (possibly cached) data returned by
6005 * #getLookupCacheDataFromResponse.
6006 *
6007 * @protected
6008 * @abstract
6009 * @param {Mixed} data Cached result data, usually an array
6010 * @return {OO.ui.MenuOptionWidget[]} Menu items
6011 */
6012 OO.ui.mixin.LookupElement.prototype.getLookupMenuOptionsFromData = function () {
6013 // Stub, implemented in subclass
6014 return [];
6015 };
6016
6017 /**
6018 * Set the read-only state of the widget.
6019 *
6020 * This will also disable/enable the lookups functionality.
6021 *
6022 * @param {boolean} readOnly Make input read-only
6023 * @chainable
6024 */
6025 OO.ui.mixin.LookupElement.prototype.setReadOnly = function ( readOnly ) {
6026 // Parent method
6027 // Note: Calling #setReadOnly this way assumes this is mixed into an OO.ui.TextInputWidget
6028 OO.ui.TextInputWidget.prototype.setReadOnly.call( this, readOnly );
6029
6030 this.setLookupsDisabled( readOnly );
6031 // During construction, #setReadOnly is called before the OO.ui.mixin.LookupElement constructor
6032 if ( readOnly && this.lookupMenu ) {
6033 this.closeLookupMenu();
6034 }
6035
6036 return this;
6037 };
6038
6039 /**
6040 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
6041 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
6042 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
6043 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
6044 *
6045 * @abstract
6046 * @class
6047 *
6048 * @constructor
6049 * @param {Object} [config] Configuration options
6050 * @cfg {Object} [popup] Configuration to pass to popup
6051 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
6052 */
6053 OO.ui.mixin.PopupElement = function OoUiMixinPopupElement( config ) {
6054 // Configuration initialization
6055 config = config || {};
6056
6057 // Properties
6058 this.popup = new OO.ui.PopupWidget( $.extend(
6059 { autoClose: true },
6060 config.popup,
6061 { $autoCloseIgnore: this.$element }
6062 ) );
6063 };
6064
6065 /* Methods */
6066
6067 /**
6068 * Get popup.
6069 *
6070 * @return {OO.ui.PopupWidget} Popup widget
6071 */
6072 OO.ui.mixin.PopupElement.prototype.getPopup = function () {
6073 return this.popup;
6074 };
6075
6076 /**
6077 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
6078 * additional functionality to an element created by another class. The class provides
6079 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
6080 * which are used to customize the look and feel of a widget to better describe its
6081 * importance and functionality.
6082 *
6083 * The library currently contains the following styling flags for general use:
6084 *
6085 * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
6086 * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
6087 * - **constructive**: Constructive styling is applied to convey that the widget will create something.
6088 *
6089 * The flags affect the appearance of the buttons:
6090 *
6091 * @example
6092 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
6093 * var button1 = new OO.ui.ButtonWidget( {
6094 * label: 'Constructive',
6095 * flags: 'constructive'
6096 * } );
6097 * var button2 = new OO.ui.ButtonWidget( {
6098 * label: 'Destructive',
6099 * flags: 'destructive'
6100 * } );
6101 * var button3 = new OO.ui.ButtonWidget( {
6102 * label: 'Progressive',
6103 * flags: 'progressive'
6104 * } );
6105 * $( 'body' ).append( button1.$element, button2.$element, button3.$element );
6106 *
6107 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
6108 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
6109 *
6110 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
6111 *
6112 * @abstract
6113 * @class
6114 *
6115 * @constructor
6116 * @param {Object} [config] Configuration options
6117 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'constructive' or 'primary') to apply.
6118 * Please see the [OOjs UI documentation on MediaWiki] [2] for more information about available flags.
6119 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
6120 * @cfg {jQuery} [$flagged] The flagged element. By default,
6121 * the flagged functionality is applied to the element created by the class ($element).
6122 * If a different element is specified, the flagged functionality will be applied to it instead.
6123 */
6124 OO.ui.mixin.FlaggedElement = function OoUiMixinFlaggedElement( config ) {
6125 // Configuration initialization
6126 config = config || {};
6127
6128 // Properties
6129 this.flags = {};
6130 this.$flagged = null;
6131
6132 // Initialization
6133 this.setFlags( config.flags );
6134 this.setFlaggedElement( config.$flagged || this.$element );
6135 };
6136
6137 /* Events */
6138
6139 /**
6140 * @event flag
6141 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
6142 * parameter contains the name of each modified flag and indicates whether it was
6143 * added or removed.
6144 *
6145 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
6146 * that the flag was added, `false` that the flag was removed.
6147 */
6148
6149 /* Methods */
6150
6151 /**
6152 * Set the flagged element.
6153 *
6154 * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
6155 * If an element is already set, the method will remove the mixin’s effect on that element.
6156 *
6157 * @param {jQuery} $flagged Element that should be flagged
6158 */
6159 OO.ui.mixin.FlaggedElement.prototype.setFlaggedElement = function ( $flagged ) {
6160 var classNames = Object.keys( this.flags ).map( function ( flag ) {
6161 return 'oo-ui-flaggedElement-' + flag;
6162 } ).join( ' ' );
6163
6164 if ( this.$flagged ) {
6165 this.$flagged.removeClass( classNames );
6166 }
6167
6168 this.$flagged = $flagged.addClass( classNames );
6169 };
6170
6171 /**
6172 * Check if the specified flag is set.
6173 *
6174 * @param {string} flag Name of flag
6175 * @return {boolean} The flag is set
6176 */
6177 OO.ui.mixin.FlaggedElement.prototype.hasFlag = function ( flag ) {
6178 return flag in this.flags;
6179 };
6180
6181 /**
6182 * Get the names of all flags set.
6183 *
6184 * @return {string[]} Flag names
6185 */
6186 OO.ui.mixin.FlaggedElement.prototype.getFlags = function () {
6187 return Object.keys( this.flags );
6188 };
6189
6190 /**
6191 * Clear all flags.
6192 *
6193 * @chainable
6194 * @fires flag
6195 */
6196 OO.ui.mixin.FlaggedElement.prototype.clearFlags = function () {
6197 var flag, className,
6198 changes = {},
6199 remove = [],
6200 classPrefix = 'oo-ui-flaggedElement-';
6201
6202 for ( flag in this.flags ) {
6203 className = classPrefix + flag;
6204 changes[ flag ] = false;
6205 delete this.flags[ flag ];
6206 remove.push( className );
6207 }
6208
6209 if ( this.$flagged ) {
6210 this.$flagged.removeClass( remove.join( ' ' ) );
6211 }
6212
6213 this.updateThemeClasses();
6214 this.emit( 'flag', changes );
6215
6216 return this;
6217 };
6218
6219 /**
6220 * Add one or more flags.
6221 *
6222 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
6223 * or an object keyed by flag name with a boolean value that indicates whether the flag should
6224 * be added (`true`) or removed (`false`).
6225 * @chainable
6226 * @fires flag
6227 */
6228 OO.ui.mixin.FlaggedElement.prototype.setFlags = function ( flags ) {
6229 var i, len, flag, className,
6230 changes = {},
6231 add = [],
6232 remove = [],
6233 classPrefix = 'oo-ui-flaggedElement-';
6234
6235 if ( typeof flags === 'string' ) {
6236 className = classPrefix + flags;
6237 // Set
6238 if ( !this.flags[ flags ] ) {
6239 this.flags[ flags ] = true;
6240 add.push( className );
6241 }
6242 } else if ( Array.isArray( flags ) ) {
6243 for ( i = 0, len = flags.length; i < len; i++ ) {
6244 flag = flags[ i ];
6245 className = classPrefix + flag;
6246 // Set
6247 if ( !this.flags[ flag ] ) {
6248 changes[ flag ] = true;
6249 this.flags[ flag ] = true;
6250 add.push( className );
6251 }
6252 }
6253 } else if ( OO.isPlainObject( flags ) ) {
6254 for ( flag in flags ) {
6255 className = classPrefix + flag;
6256 if ( flags[ flag ] ) {
6257 // Set
6258 if ( !this.flags[ flag ] ) {
6259 changes[ flag ] = true;
6260 this.flags[ flag ] = true;
6261 add.push( className );
6262 }
6263 } else {
6264 // Remove
6265 if ( this.flags[ flag ] ) {
6266 changes[ flag ] = false;
6267 delete this.flags[ flag ];
6268 remove.push( className );
6269 }
6270 }
6271 }
6272 }
6273
6274 if ( this.$flagged ) {
6275 this.$flagged
6276 .addClass( add.join( ' ' ) )
6277 .removeClass( remove.join( ' ' ) );
6278 }
6279
6280 this.updateThemeClasses();
6281 this.emit( 'flag', changes );
6282
6283 return this;
6284 };
6285
6286 /**
6287 * TitledElement is mixed into other classes to provide a `title` attribute.
6288 * Titles are rendered by the browser and are made visible when the user moves
6289 * the mouse over the element. Titles are not visible on touch devices.
6290 *
6291 * @example
6292 * // TitledElement provides a 'title' attribute to the
6293 * // ButtonWidget class
6294 * var button = new OO.ui.ButtonWidget( {
6295 * label: 'Button with Title',
6296 * title: 'I am a button'
6297 * } );
6298 * $( 'body' ).append( button.$element );
6299 *
6300 * @abstract
6301 * @class
6302 *
6303 * @constructor
6304 * @param {Object} [config] Configuration options
6305 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
6306 * If this config is omitted, the title functionality is applied to $element, the
6307 * element created by the class.
6308 * @cfg {string|Function} [title] The title text or a function that returns text. If
6309 * this config is omitted, the value of the {@link #static-title static title} property is used.
6310 */
6311 OO.ui.mixin.TitledElement = function OoUiMixinTitledElement( config ) {
6312 // Configuration initialization
6313 config = config || {};
6314
6315 // Properties
6316 this.$titled = null;
6317 this.title = null;
6318
6319 // Initialization
6320 this.setTitle( config.title || this.constructor.static.title );
6321 this.setTitledElement( config.$titled || this.$element );
6322 };
6323
6324 /* Setup */
6325
6326 OO.initClass( OO.ui.mixin.TitledElement );
6327
6328 /* Static Properties */
6329
6330 /**
6331 * The title text, a function that returns text, or `null` for no title. The value of the static property
6332 * is overridden if the #title config option is used.
6333 *
6334 * @static
6335 * @inheritable
6336 * @property {string|Function|null}
6337 */
6338 OO.ui.mixin.TitledElement.static.title = null;
6339
6340 /* Methods */
6341
6342 /**
6343 * Set the titled element.
6344 *
6345 * This method is used to retarget a titledElement mixin so that its functionality applies to the specified element.
6346 * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
6347 *
6348 * @param {jQuery} $titled Element that should use the 'titled' functionality
6349 */
6350 OO.ui.mixin.TitledElement.prototype.setTitledElement = function ( $titled ) {
6351 if ( this.$titled ) {
6352 this.$titled.removeAttr( 'title' );
6353 }
6354
6355 this.$titled = $titled;
6356 if ( this.title ) {
6357 this.$titled.attr( 'title', this.title );
6358 }
6359 };
6360
6361 /**
6362 * Set title.
6363 *
6364 * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
6365 * @chainable
6366 */
6367 OO.ui.mixin.TitledElement.prototype.setTitle = function ( title ) {
6368 title = typeof title === 'string' ? OO.ui.resolveMsg( title ) : null;
6369
6370 if ( this.title !== title ) {
6371 if ( this.$titled ) {
6372 if ( title !== null ) {
6373 this.$titled.attr( 'title', title );
6374 } else {
6375 this.$titled.removeAttr( 'title' );
6376 }
6377 }
6378 this.title = title;
6379 }
6380
6381 return this;
6382 };
6383
6384 /**
6385 * Get title.
6386 *
6387 * @return {string} Title string
6388 */
6389 OO.ui.mixin.TitledElement.prototype.getTitle = function () {
6390 return this.title;
6391 };
6392
6393 /**
6394 * Element that can be automatically clipped to visible boundaries.
6395 *
6396 * Whenever the element's natural height changes, you have to call
6397 * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
6398 * clipping correctly.
6399 *
6400 * @abstract
6401 * @class
6402 *
6403 * @constructor
6404 * @param {Object} [config] Configuration options
6405 * @cfg {jQuery} [$clippable] Nodes to clip, assigned to #$clippable, omit to use #$element
6406 */
6407 OO.ui.mixin.ClippableElement = function OoUiMixinClippableElement( config ) {
6408 // Configuration initialization
6409 config = config || {};
6410
6411 // Properties
6412 this.$clippable = null;
6413 this.clipping = false;
6414 this.clippedHorizontally = false;
6415 this.clippedVertically = false;
6416 this.$clippableContainer = null;
6417 this.$clippableScroller = null;
6418 this.$clippableWindow = null;
6419 this.idealWidth = null;
6420 this.idealHeight = null;
6421 this.onClippableContainerScrollHandler = this.clip.bind( this );
6422 this.onClippableWindowResizeHandler = this.clip.bind( this );
6423
6424 // Initialization
6425 this.setClippableElement( config.$clippable || this.$element );
6426 };
6427
6428 /* Methods */
6429
6430 /**
6431 * Set clippable element.
6432 *
6433 * If an element is already set, it will be cleaned up before setting up the new element.
6434 *
6435 * @param {jQuery} $clippable Element to make clippable
6436 */
6437 OO.ui.mixin.ClippableElement.prototype.setClippableElement = function ( $clippable ) {
6438 if ( this.$clippable ) {
6439 this.$clippable.removeClass( 'oo-ui-clippableElement-clippable' );
6440 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
6441 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
6442 }
6443
6444 this.$clippable = $clippable.addClass( 'oo-ui-clippableElement-clippable' );
6445 this.clip();
6446 };
6447
6448 /**
6449 * Toggle clipping.
6450 *
6451 * Do not turn clipping on until after the element is attached to the DOM and visible.
6452 *
6453 * @param {boolean} [clipping] Enable clipping, omit to toggle
6454 * @chainable
6455 */
6456 OO.ui.mixin.ClippableElement.prototype.toggleClipping = function ( clipping ) {
6457 clipping = clipping === undefined ? !this.clipping : !!clipping;
6458
6459 if ( this.clipping !== clipping ) {
6460 this.clipping = clipping;
6461 if ( clipping ) {
6462 this.$clippableContainer = $( this.getClosestScrollableElementContainer() );
6463 // If the clippable container is the root, we have to listen to scroll events and check
6464 // jQuery.scrollTop on the window because of browser inconsistencies
6465 this.$clippableScroller = this.$clippableContainer.is( 'html, body' ) ?
6466 $( OO.ui.Element.static.getWindow( this.$clippableContainer ) ) :
6467 this.$clippableContainer;
6468 this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler );
6469 this.$clippableWindow = $( this.getElementWindow() )
6470 .on( 'resize', this.onClippableWindowResizeHandler );
6471 // Initial clip after visible
6472 this.clip();
6473 } else {
6474 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
6475 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
6476
6477 this.$clippableContainer = null;
6478 this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler );
6479 this.$clippableScroller = null;
6480 this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
6481 this.$clippableWindow = null;
6482 }
6483 }
6484
6485 return this;
6486 };
6487
6488 /**
6489 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
6490 *
6491 * @return {boolean} Element will be clipped to the visible area
6492 */
6493 OO.ui.mixin.ClippableElement.prototype.isClipping = function () {
6494 return this.clipping;
6495 };
6496
6497 /**
6498 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
6499 *
6500 * @return {boolean} Part of the element is being clipped
6501 */
6502 OO.ui.mixin.ClippableElement.prototype.isClipped = function () {
6503 return this.clippedHorizontally || this.clippedVertically;
6504 };
6505
6506 /**
6507 * Check if the right of the element is being clipped by the nearest scrollable container.
6508 *
6509 * @return {boolean} Part of the element is being clipped
6510 */
6511 OO.ui.mixin.ClippableElement.prototype.isClippedHorizontally = function () {
6512 return this.clippedHorizontally;
6513 };
6514
6515 /**
6516 * Check if the bottom of the element is being clipped by the nearest scrollable container.
6517 *
6518 * @return {boolean} Part of the element is being clipped
6519 */
6520 OO.ui.mixin.ClippableElement.prototype.isClippedVertically = function () {
6521 return this.clippedVertically;
6522 };
6523
6524 /**
6525 * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
6526 *
6527 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
6528 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
6529 */
6530 OO.ui.mixin.ClippableElement.prototype.setIdealSize = function ( width, height ) {
6531 this.idealWidth = width;
6532 this.idealHeight = height;
6533
6534 if ( !this.clipping ) {
6535 // Update dimensions
6536 this.$clippable.css( { width: width, height: height } );
6537 }
6538 // While clipping, idealWidth and idealHeight are not considered
6539 };
6540
6541 /**
6542 * Clip element to visible boundaries and allow scrolling when needed. Call this method when
6543 * the element's natural height changes.
6544 *
6545 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
6546 * overlapped by, the visible area of the nearest scrollable container.
6547 *
6548 * @chainable
6549 */
6550 OO.ui.mixin.ClippableElement.prototype.clip = function () {
6551 if ( !this.clipping ) {
6552 // this.$clippableContainer and this.$clippableWindow are null, so the below will fail
6553 return this;
6554 }
6555
6556 var buffer = 7, // Chosen by fair dice roll
6557 cOffset = this.$clippable.offset(),
6558 $container = this.$clippableContainer.is( 'html, body' ) ?
6559 this.$clippableWindow : this.$clippableContainer,
6560 ccOffset = $container.offset() || { top: 0, left: 0 },
6561 ccHeight = $container.innerHeight() - buffer,
6562 ccWidth = $container.innerWidth() - buffer,
6563 cWidth = this.$clippable.outerWidth() + buffer,
6564 scrollerIsWindow = this.$clippableScroller[0] === this.$clippableWindow[0],
6565 scrollTop = scrollerIsWindow ? this.$clippableScroller.scrollTop() : 0,
6566 scrollLeft = scrollerIsWindow ? this.$clippableScroller.scrollLeft() : 0,
6567 desiredWidth = cOffset.left < 0 ?
6568 cWidth + cOffset.left :
6569 ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left,
6570 desiredHeight = ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top,
6571 naturalWidth = this.$clippable.prop( 'scrollWidth' ),
6572 naturalHeight = this.$clippable.prop( 'scrollHeight' ),
6573 clipWidth = desiredWidth < naturalWidth,
6574 clipHeight = desiredHeight < naturalHeight;
6575
6576 if ( clipWidth ) {
6577 this.$clippable.css( { overflowX: 'scroll', width: desiredWidth } );
6578 } else {
6579 this.$clippable.css( { width: this.idealWidth || '', overflowX: '' } );
6580 }
6581 if ( clipHeight ) {
6582 this.$clippable.css( { overflowY: 'scroll', height: desiredHeight } );
6583 } else {
6584 this.$clippable.css( { height: this.idealHeight || '', overflowY: '' } );
6585 }
6586
6587 // If we stopped clipping in at least one of the dimensions
6588 if ( ( this.clippedHorizontally && !clipWidth ) || ( this.clippedVertically && !clipHeight ) ) {
6589 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
6590 }
6591
6592 this.clippedHorizontally = clipWidth;
6593 this.clippedVertically = clipHeight;
6594
6595 return this;
6596 };
6597
6598 /**
6599 * Tools, together with {@link OO.ui.ToolGroup toolgroups}, constitute {@link OO.ui.Toolbar toolbars}.
6600 * Each tool is configured with a static name, title, and icon and is customized with the command to carry
6601 * out when the tool is selected. Tools must also be registered with a {@link OO.ui.ToolFactory tool factory},
6602 * which creates the tools on demand.
6603 *
6604 * Tools are added to toolgroups ({@link OO.ui.ListToolGroup ListToolGroup},
6605 * {@link OO.ui.BarToolGroup BarToolGroup}, or {@link OO.ui.MenuToolGroup MenuToolGroup}), which determine how
6606 * the tool is displayed in the toolbar. See {@link OO.ui.Toolbar toolbars} for an example.
6607 *
6608 * For more information, please see the [OOjs UI documentation on MediaWiki][1].
6609 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
6610 *
6611 * @abstract
6612 * @class
6613 * @extends OO.ui.Widget
6614 * @mixins OO.ui.mixin.IconElement
6615 * @mixins OO.ui.mixin.FlaggedElement
6616 * @mixins OO.ui.mixin.TabIndexedElement
6617 *
6618 * @constructor
6619 * @param {OO.ui.ToolGroup} toolGroup
6620 * @param {Object} [config] Configuration options
6621 * @cfg {string|Function} [title] Title text or a function that returns text. If this config is omitted, the value of
6622 * the {@link #static-title static title} property is used.
6623 *
6624 * The title is used in different ways depending on the type of toolgroup that contains the tool. The
6625 * 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
6626 * part of a {@link OO.ui.ListToolGroup list} or {@link OO.ui.MenuToolGroup menu} toolgroup.
6627 *
6628 * For bar toolgroups, a description of the accelerator key is appended to the title if an accelerator key
6629 * is associated with an action by the same name as the tool and accelerator functionality has been added to the application.
6630 * To add accelerator key functionality, you must subclass OO.ui.Toolbar and override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method.
6631 */
6632 OO.ui.Tool = function OoUiTool( toolGroup, config ) {
6633 // Allow passing positional parameters inside the config object
6634 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
6635 config = toolGroup;
6636 toolGroup = config.toolGroup;
6637 }
6638
6639 // Configuration initialization
6640 config = config || {};
6641
6642 // Parent constructor
6643 OO.ui.Tool.parent.call( this, config );
6644
6645 // Properties
6646 this.toolGroup = toolGroup;
6647 this.toolbar = this.toolGroup.getToolbar();
6648 this.active = false;
6649 this.$title = $( '<span>' );
6650 this.$accel = $( '<span>' );
6651 this.$link = $( '<a>' );
6652 this.title = null;
6653
6654 // Mixin constructors
6655 OO.ui.mixin.IconElement.call( this, config );
6656 OO.ui.mixin.FlaggedElement.call( this, config );
6657 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$link } ) );
6658
6659 // Events
6660 this.toolbar.connect( this, { updateState: 'onUpdateState' } );
6661
6662 // Initialization
6663 this.$title.addClass( 'oo-ui-tool-title' );
6664 this.$accel
6665 .addClass( 'oo-ui-tool-accel' )
6666 .prop( {
6667 // This may need to be changed if the key names are ever localized,
6668 // but for now they are essentially written in English
6669 dir: 'ltr',
6670 lang: 'en'
6671 } );
6672 this.$link
6673 .addClass( 'oo-ui-tool-link' )
6674 .append( this.$icon, this.$title, this.$accel )
6675 .attr( 'role', 'button' );
6676 this.$element
6677 .data( 'oo-ui-tool', this )
6678 .addClass(
6679 'oo-ui-tool ' + 'oo-ui-tool-name-' +
6680 this.constructor.static.name.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' )
6681 )
6682 .toggleClass( 'oo-ui-tool-with-label', this.constructor.static.displayBothIconAndLabel )
6683 .append( this.$link );
6684 this.setTitle( config.title || this.constructor.static.title );
6685 };
6686
6687 /* Setup */
6688
6689 OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
6690 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.IconElement );
6691 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.FlaggedElement );
6692 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.TabIndexedElement );
6693
6694 /* Static Properties */
6695
6696 /**
6697 * @static
6698 * @inheritdoc
6699 */
6700 OO.ui.Tool.static.tagName = 'span';
6701
6702 /**
6703 * Symbolic name of tool.
6704 *
6705 * The symbolic name is used internally to register the tool with a {@link OO.ui.ToolFactory ToolFactory}. It can
6706 * also be used when adding tools to toolgroups.
6707 *
6708 * @abstract
6709 * @static
6710 * @inheritable
6711 * @property {string}
6712 */
6713 OO.ui.Tool.static.name = '';
6714
6715 /**
6716 * Symbolic name of the group.
6717 *
6718 * The group name is used to associate tools with each other so that they can be selected later by
6719 * a {@link OO.ui.ToolGroup toolgroup}.
6720 *
6721 * @abstract
6722 * @static
6723 * @inheritable
6724 * @property {string}
6725 */
6726 OO.ui.Tool.static.group = '';
6727
6728 /**
6729 * 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.
6730 *
6731 * @abstract
6732 * @static
6733 * @inheritable
6734 * @property {string|Function}
6735 */
6736 OO.ui.Tool.static.title = '';
6737
6738 /**
6739 * Display both icon and label when the tool is used in a {@link OO.ui.BarToolGroup bar} toolgroup.
6740 * Normally only the icon is displayed, or only the label if no icon is given.
6741 *
6742 * @static
6743 * @inheritable
6744 * @property {boolean}
6745 */
6746 OO.ui.Tool.static.displayBothIconAndLabel = false;
6747
6748 /**
6749 * Add tool to catch-all groups automatically.
6750 *
6751 * A catch-all group, which contains all tools that do not currently belong to a toolgroup,
6752 * can be included in a toolgroup using the wildcard selector, an asterisk (*).
6753 *
6754 * @static
6755 * @inheritable
6756 * @property {boolean}
6757 */
6758 OO.ui.Tool.static.autoAddToCatchall = true;
6759
6760 /**
6761 * Add tool to named groups automatically.
6762 *
6763 * By default, tools that are configured with a static ‘group’ property are added
6764 * to that group and will be selected when the symbolic name of the group is specified (e.g., when
6765 * toolgroups include tools by group name).
6766 *
6767 * @static
6768 * @property {boolean}
6769 * @inheritable
6770 */
6771 OO.ui.Tool.static.autoAddToGroup = true;
6772
6773 /**
6774 * Check if this tool is compatible with given data.
6775 *
6776 * This is a stub that can be overriden to provide support for filtering tools based on an
6777 * arbitrary piece of information (e.g., where the cursor is in a document). The implementation
6778 * must also call this method so that the compatibility check can be performed.
6779 *
6780 * @static
6781 * @inheritable
6782 * @param {Mixed} data Data to check
6783 * @return {boolean} Tool can be used with data
6784 */
6785 OO.ui.Tool.static.isCompatibleWith = function () {
6786 return false;
6787 };
6788
6789 /* Methods */
6790
6791 /**
6792 * Handle the toolbar state being updated.
6793 *
6794 * This is an abstract method that must be overridden in a concrete subclass.
6795 *
6796 * @protected
6797 * @abstract
6798 */
6799 OO.ui.Tool.prototype.onUpdateState = function () {
6800 throw new Error(
6801 'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor
6802 );
6803 };
6804
6805 /**
6806 * Handle the tool being selected.
6807 *
6808 * This is an abstract method that must be overridden in a concrete subclass.
6809 *
6810 * @protected
6811 * @abstract
6812 */
6813 OO.ui.Tool.prototype.onSelect = function () {
6814 throw new Error(
6815 'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor
6816 );
6817 };
6818
6819 /**
6820 * Check if the tool is active.
6821 *
6822 * Tools become active when their #onSelect or #onUpdateState handlers change them to appear pressed
6823 * with the #setActive method. Additional CSS is applied to the tool to reflect the active state.
6824 *
6825 * @return {boolean} Tool is active
6826 */
6827 OO.ui.Tool.prototype.isActive = function () {
6828 return this.active;
6829 };
6830
6831 /**
6832 * Make the tool appear active or inactive.
6833 *
6834 * This method should be called within #onSelect or #onUpdateState event handlers to make the tool
6835 * appear pressed or not.
6836 *
6837 * @param {boolean} state Make tool appear active
6838 */
6839 OO.ui.Tool.prototype.setActive = function ( state ) {
6840 this.active = !!state;
6841 if ( this.active ) {
6842 this.$element.addClass( 'oo-ui-tool-active' );
6843 } else {
6844 this.$element.removeClass( 'oo-ui-tool-active' );
6845 }
6846 };
6847
6848 /**
6849 * Set the tool #title.
6850 *
6851 * @param {string|Function} title Title text or a function that returns text
6852 * @chainable
6853 */
6854 OO.ui.Tool.prototype.setTitle = function ( title ) {
6855 this.title = OO.ui.resolveMsg( title );
6856 this.updateTitle();
6857 return this;
6858 };
6859
6860 /**
6861 * Get the tool #title.
6862 *
6863 * @return {string} Title text
6864 */
6865 OO.ui.Tool.prototype.getTitle = function () {
6866 return this.title;
6867 };
6868
6869 /**
6870 * Get the tool's symbolic name.
6871 *
6872 * @return {string} Symbolic name of tool
6873 */
6874 OO.ui.Tool.prototype.getName = function () {
6875 return this.constructor.static.name;
6876 };
6877
6878 /**
6879 * Update the title.
6880 */
6881 OO.ui.Tool.prototype.updateTitle = function () {
6882 var titleTooltips = this.toolGroup.constructor.static.titleTooltips,
6883 accelTooltips = this.toolGroup.constructor.static.accelTooltips,
6884 accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
6885 tooltipParts = [];
6886
6887 this.$title.text( this.title );
6888 this.$accel.text( accel );
6889
6890 if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
6891 tooltipParts.push( this.title );
6892 }
6893 if ( accelTooltips && typeof accel === 'string' && accel.length ) {
6894 tooltipParts.push( accel );
6895 }
6896 if ( tooltipParts.length ) {
6897 this.$link.attr( 'title', tooltipParts.join( ' ' ) );
6898 } else {
6899 this.$link.removeAttr( 'title' );
6900 }
6901 };
6902
6903 /**
6904 * Destroy tool.
6905 *
6906 * Destroying the tool removes all event handlers and the tool’s DOM elements.
6907 * Call this method whenever you are done using a tool.
6908 */
6909 OO.ui.Tool.prototype.destroy = function () {
6910 this.toolbar.disconnect( this );
6911 this.$element.remove();
6912 };
6913
6914 /**
6915 * Toolbars are complex interface components that permit users to easily access a variety
6916 * of {@link OO.ui.Tool tools} (e.g., formatting commands) and actions, which are additional commands that are
6917 * part of the toolbar, but not configured as tools.
6918 *
6919 * Individual tools are customized and then registered with a {@link OO.ui.ToolFactory tool factory}, which creates
6920 * the tools on demand. Each tool has a symbolic name (used when registering the tool), a title (e.g., ‘Insert
6921 * picture’), and an icon.
6922 *
6923 * Individual tools are organized in {@link OO.ui.ToolGroup toolgroups}, which can be {@link OO.ui.MenuToolGroup menus}
6924 * of tools, {@link OO.ui.ListToolGroup lists} of tools, or a single {@link OO.ui.BarToolGroup bar} of tools.
6925 * The arrangement and order of the toolgroups is customized when the toolbar is set up. Tools can be presented in
6926 * any order, but each can only appear once in the toolbar.
6927 *
6928 * The following is an example of a basic toolbar.
6929 *
6930 * @example
6931 * // Example of a toolbar
6932 * // Create the toolbar
6933 * var toolFactory = new OO.ui.ToolFactory();
6934 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
6935 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
6936 *
6937 * // We will be placing status text in this element when tools are used
6938 * var $area = $( '<p>' ).text( 'Toolbar example' );
6939 *
6940 * // Define the tools that we're going to place in our toolbar
6941 *
6942 * // Create a class inheriting from OO.ui.Tool
6943 * function PictureTool() {
6944 * PictureTool.parent.apply( this, arguments );
6945 * }
6946 * OO.inheritClass( PictureTool, OO.ui.Tool );
6947 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
6948 * // of 'icon' and 'title' (displayed icon and text).
6949 * PictureTool.static.name = 'picture';
6950 * PictureTool.static.icon = 'picture';
6951 * PictureTool.static.title = 'Insert picture';
6952 * // Defines the action that will happen when this tool is selected (clicked).
6953 * PictureTool.prototype.onSelect = function () {
6954 * $area.text( 'Picture tool clicked!' );
6955 * // Never display this tool as "active" (selected).
6956 * this.setActive( false );
6957 * };
6958 * // Make this tool available in our toolFactory and thus our toolbar
6959 * toolFactory.register( PictureTool );
6960 *
6961 * // Register two more tools, nothing interesting here
6962 * function SettingsTool() {
6963 * SettingsTool.parent.apply( this, arguments );
6964 * }
6965 * OO.inheritClass( SettingsTool, OO.ui.Tool );
6966 * SettingsTool.static.name = 'settings';
6967 * SettingsTool.static.icon = 'settings';
6968 * SettingsTool.static.title = 'Change settings';
6969 * SettingsTool.prototype.onSelect = function () {
6970 * $area.text( 'Settings tool clicked!' );
6971 * this.setActive( false );
6972 * };
6973 * toolFactory.register( SettingsTool );
6974 *
6975 * // Register two more tools, nothing interesting here
6976 * function StuffTool() {
6977 * StuffTool.parent.apply( this, arguments );
6978 * }
6979 * OO.inheritClass( StuffTool, OO.ui.Tool );
6980 * StuffTool.static.name = 'stuff';
6981 * StuffTool.static.icon = 'ellipsis';
6982 * StuffTool.static.title = 'More stuff';
6983 * StuffTool.prototype.onSelect = function () {
6984 * $area.text( 'More stuff tool clicked!' );
6985 * this.setActive( false );
6986 * };
6987 * toolFactory.register( StuffTool );
6988 *
6989 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
6990 * // little popup window (a PopupWidget).
6991 * function HelpTool( toolGroup, config ) {
6992 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
6993 * padded: true,
6994 * label: 'Help',
6995 * head: true
6996 * } }, config ) );
6997 * this.popup.$body.append( '<p>I am helpful!</p>' );
6998 * }
6999 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
7000 * HelpTool.static.name = 'help';
7001 * HelpTool.static.icon = 'help';
7002 * HelpTool.static.title = 'Help';
7003 * toolFactory.register( HelpTool );
7004 *
7005 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
7006 * // used once (but not all defined tools must be used).
7007 * toolbar.setup( [
7008 * {
7009 * // 'bar' tool groups display tools' icons only, side-by-side.
7010 * type: 'bar',
7011 * include: [ 'picture', 'help' ]
7012 * },
7013 * {
7014 * // 'list' tool groups display both the titles and icons, in a dropdown list.
7015 * type: 'list',
7016 * indicator: 'down',
7017 * label: 'More',
7018 * include: [ 'settings', 'stuff' ]
7019 * }
7020 * // Note how the tools themselves are toolgroup-agnostic - the same tool can be displayed
7021 * // either in a 'list' or a 'bar'. There is a 'menu' tool group too, not showcased here,
7022 * // since it's more complicated to use. (See the next example snippet on this page.)
7023 * ] );
7024 *
7025 * // Create some UI around the toolbar and place it in the document
7026 * var frame = new OO.ui.PanelLayout( {
7027 * expanded: false,
7028 * framed: true
7029 * } );
7030 * var contentFrame = new OO.ui.PanelLayout( {
7031 * expanded: false,
7032 * padded: true
7033 * } );
7034 * frame.$element.append(
7035 * toolbar.$element,
7036 * contentFrame.$element.append( $area )
7037 * );
7038 * $( 'body' ).append( frame.$element );
7039 *
7040 * // Here is where the toolbar is actually built. This must be done after inserting it into the
7041 * // document.
7042 * toolbar.initialize();
7043 *
7044 * The following example extends the previous one to illustrate 'menu' toolgroups and the usage of
7045 * 'updateState' event.
7046 *
7047 * @example
7048 * // Create the toolbar
7049 * var toolFactory = new OO.ui.ToolFactory();
7050 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
7051 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
7052 *
7053 * // We will be placing status text in this element when tools are used
7054 * var $area = $( '<p>' ).text( 'Toolbar example' );
7055 *
7056 * // Define the tools that we're going to place in our toolbar
7057 *
7058 * // Create a class inheriting from OO.ui.Tool
7059 * function PictureTool() {
7060 * PictureTool.parent.apply( this, arguments );
7061 * }
7062 * OO.inheritClass( PictureTool, OO.ui.Tool );
7063 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
7064 * // of 'icon' and 'title' (displayed icon and text).
7065 * PictureTool.static.name = 'picture';
7066 * PictureTool.static.icon = 'picture';
7067 * PictureTool.static.title = 'Insert picture';
7068 * // Defines the action that will happen when this tool is selected (clicked).
7069 * PictureTool.prototype.onSelect = function () {
7070 * $area.text( 'Picture tool clicked!' );
7071 * // Never display this tool as "active" (selected).
7072 * this.setActive( false );
7073 * };
7074 * // The toolbar can be synchronized with the state of some external stuff, like a text
7075 * // editor's editing area, highlighting the tools (e.g. a 'bold' tool would be shown as active
7076 * // when the text cursor was inside bolded text). Here we simply disable this feature.
7077 * PictureTool.prototype.onUpdateState = function () {
7078 * };
7079 * // Make this tool available in our toolFactory and thus our toolbar
7080 * toolFactory.register( PictureTool );
7081 *
7082 * // Register two more tools, nothing interesting here
7083 * function SettingsTool() {
7084 * SettingsTool.parent.apply( this, arguments );
7085 * this.reallyActive = false;
7086 * }
7087 * OO.inheritClass( SettingsTool, OO.ui.Tool );
7088 * SettingsTool.static.name = 'settings';
7089 * SettingsTool.static.icon = 'settings';
7090 * SettingsTool.static.title = 'Change settings';
7091 * SettingsTool.prototype.onSelect = function () {
7092 * $area.text( 'Settings tool clicked!' );
7093 * // Toggle the active state on each click
7094 * this.reallyActive = !this.reallyActive;
7095 * this.setActive( this.reallyActive );
7096 * // To update the menu label
7097 * this.toolbar.emit( 'updateState' );
7098 * };
7099 * SettingsTool.prototype.onUpdateState = function () {
7100 * };
7101 * toolFactory.register( SettingsTool );
7102 *
7103 * // Register two more tools, nothing interesting here
7104 * function StuffTool() {
7105 * StuffTool.parent.apply( this, arguments );
7106 * this.reallyActive = false;
7107 * }
7108 * OO.inheritClass( StuffTool, OO.ui.Tool );
7109 * StuffTool.static.name = 'stuff';
7110 * StuffTool.static.icon = 'ellipsis';
7111 * StuffTool.static.title = 'More stuff';
7112 * StuffTool.prototype.onSelect = function () {
7113 * $area.text( 'More stuff tool clicked!' );
7114 * // Toggle the active state on each click
7115 * this.reallyActive = !this.reallyActive;
7116 * this.setActive( this.reallyActive );
7117 * // To update the menu label
7118 * this.toolbar.emit( 'updateState' );
7119 * };
7120 * StuffTool.prototype.onUpdateState = function () {
7121 * };
7122 * toolFactory.register( StuffTool );
7123 *
7124 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
7125 * // little popup window (a PopupWidget). 'onUpdateState' is also already implemented.
7126 * function HelpTool( toolGroup, config ) {
7127 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
7128 * padded: true,
7129 * label: 'Help',
7130 * head: true
7131 * } }, config ) );
7132 * this.popup.$body.append( '<p>I am helpful!</p>' );
7133 * }
7134 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
7135 * HelpTool.static.name = 'help';
7136 * HelpTool.static.icon = 'help';
7137 * HelpTool.static.title = 'Help';
7138 * toolFactory.register( HelpTool );
7139 *
7140 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
7141 * // used once (but not all defined tools must be used).
7142 * toolbar.setup( [
7143 * {
7144 * // 'bar' tool groups display tools' icons only, side-by-side.
7145 * type: 'bar',
7146 * include: [ 'picture', 'help' ]
7147 * },
7148 * {
7149 * // 'menu' tool groups display both the titles and icons, in a dropdown menu.
7150 * // Menu label indicates which items are selected.
7151 * type: 'menu',
7152 * indicator: 'down',
7153 * include: [ 'settings', 'stuff' ]
7154 * }
7155 * ] );
7156 *
7157 * // Create some UI around the toolbar and place it in the document
7158 * var frame = new OO.ui.PanelLayout( {
7159 * expanded: false,
7160 * framed: true
7161 * } );
7162 * var contentFrame = new OO.ui.PanelLayout( {
7163 * expanded: false,
7164 * padded: true
7165 * } );
7166 * frame.$element.append(
7167 * toolbar.$element,
7168 * contentFrame.$element.append( $area )
7169 * );
7170 * $( 'body' ).append( frame.$element );
7171 *
7172 * // Here is where the toolbar is actually built. This must be done after inserting it into the
7173 * // document.
7174 * toolbar.initialize();
7175 * toolbar.emit( 'updateState' );
7176 *
7177 * @class
7178 * @extends OO.ui.Element
7179 * @mixins OO.EventEmitter
7180 * @mixins OO.ui.mixin.GroupElement
7181 *
7182 * @constructor
7183 * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
7184 * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating toolgroups
7185 * @param {Object} [config] Configuration options
7186 * @cfg {boolean} [actions] Add an actions section to the toolbar. Actions are commands that are included
7187 * in the toolbar, but are not configured as tools. By default, actions are displayed on the right side of
7188 * the toolbar.
7189 * @cfg {boolean} [shadow] Add a shadow below the toolbar.
7190 */
7191 OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
7192 // Allow passing positional parameters inside the config object
7193 if ( OO.isPlainObject( toolFactory ) && config === undefined ) {
7194 config = toolFactory;
7195 toolFactory = config.toolFactory;
7196 toolGroupFactory = config.toolGroupFactory;
7197 }
7198
7199 // Configuration initialization
7200 config = config || {};
7201
7202 // Parent constructor
7203 OO.ui.Toolbar.parent.call( this, config );
7204
7205 // Mixin constructors
7206 OO.EventEmitter.call( this );
7207 OO.ui.mixin.GroupElement.call( this, config );
7208
7209 // Properties
7210 this.toolFactory = toolFactory;
7211 this.toolGroupFactory = toolGroupFactory;
7212 this.groups = [];
7213 this.tools = {};
7214 this.$bar = $( '<div>' );
7215 this.$actions = $( '<div>' );
7216 this.initialized = false;
7217 this.onWindowResizeHandler = this.onWindowResize.bind( this );
7218
7219 // Events
7220 this.$element
7221 .add( this.$bar ).add( this.$group ).add( this.$actions )
7222 .on( 'mousedown keydown', this.onPointerDown.bind( this ) );
7223
7224 // Initialization
7225 this.$group.addClass( 'oo-ui-toolbar-tools' );
7226 if ( config.actions ) {
7227 this.$bar.append( this.$actions.addClass( 'oo-ui-toolbar-actions' ) );
7228 }
7229 this.$bar
7230 .addClass( 'oo-ui-toolbar-bar' )
7231 .append( this.$group, '<div style="clear:both"></div>' );
7232 if ( config.shadow ) {
7233 this.$bar.append( '<div class="oo-ui-toolbar-shadow"></div>' );
7234 }
7235 this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar );
7236 };
7237
7238 /* Setup */
7239
7240 OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
7241 OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
7242 OO.mixinClass( OO.ui.Toolbar, OO.ui.mixin.GroupElement );
7243
7244 /* Methods */
7245
7246 /**
7247 * Get the tool factory.
7248 *
7249 * @return {OO.ui.ToolFactory} Tool factory
7250 */
7251 OO.ui.Toolbar.prototype.getToolFactory = function () {
7252 return this.toolFactory;
7253 };
7254
7255 /**
7256 * Get the toolgroup factory.
7257 *
7258 * @return {OO.Factory} Toolgroup factory
7259 */
7260 OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
7261 return this.toolGroupFactory;
7262 };
7263
7264 /**
7265 * Handles mouse down events.
7266 *
7267 * @private
7268 * @param {jQuery.Event} e Mouse down event
7269 */
7270 OO.ui.Toolbar.prototype.onPointerDown = function ( e ) {
7271 var $closestWidgetToEvent = $( e.target ).closest( '.oo-ui-widget' ),
7272 $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
7273 if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[ 0 ] === $closestWidgetToToolbar[ 0 ] ) {
7274 return false;
7275 }
7276 };
7277
7278 /**
7279 * Handle window resize event.
7280 *
7281 * @private
7282 * @param {jQuery.Event} e Window resize event
7283 */
7284 OO.ui.Toolbar.prototype.onWindowResize = function () {
7285 this.$element.toggleClass(
7286 'oo-ui-toolbar-narrow',
7287 this.$bar.width() <= this.narrowThreshold
7288 );
7289 };
7290
7291 /**
7292 * Sets up handles and preloads required information for the toolbar to work.
7293 * This must be called after it is attached to a visible document and before doing anything else.
7294 */
7295 OO.ui.Toolbar.prototype.initialize = function () {
7296 this.initialized = true;
7297 this.narrowThreshold = this.$group.width() + this.$actions.width();
7298 $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
7299 this.onWindowResize();
7300 };
7301
7302 /**
7303 * Set up the toolbar.
7304 *
7305 * The toolbar is set up with a list of toolgroup configurations that specify the type of
7306 * toolgroup ({@link OO.ui.BarToolGroup bar}, {@link OO.ui.MenuToolGroup menu}, or {@link OO.ui.ListToolGroup list})
7307 * to add and which tools to include, exclude, promote, or demote within that toolgroup. Please
7308 * see {@link OO.ui.ToolGroup toolgroups} for more information about including tools in toolgroups.
7309 *
7310 * @param {Object.<string,Array>} groups List of toolgroup configurations
7311 * @param {Array|string} [groups.include] Tools to include in the toolgroup
7312 * @param {Array|string} [groups.exclude] Tools to exclude from the toolgroup
7313 * @param {Array|string} [groups.promote] Tools to promote to the beginning of the toolgroup
7314 * @param {Array|string} [groups.demote] Tools to demote to the end of the toolgroup
7315 */
7316 OO.ui.Toolbar.prototype.setup = function ( groups ) {
7317 var i, len, type, group,
7318 items = [],
7319 defaultType = 'bar';
7320
7321 // Cleanup previous groups
7322 this.reset();
7323
7324 // Build out new groups
7325 for ( i = 0, len = groups.length; i < len; i++ ) {
7326 group = groups[ i ];
7327 if ( group.include === '*' ) {
7328 // Apply defaults to catch-all groups
7329 if ( group.type === undefined ) {
7330 group.type = 'list';
7331 }
7332 if ( group.label === undefined ) {
7333 group.label = OO.ui.msg( 'ooui-toolbar-more' );
7334 }
7335 }
7336 // Check type has been registered
7337 type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType;
7338 items.push(
7339 this.getToolGroupFactory().create( type, this, group )
7340 );
7341 }
7342 this.addItems( items );
7343 };
7344
7345 /**
7346 * Remove all tools and toolgroups from the toolbar.
7347 */
7348 OO.ui.Toolbar.prototype.reset = function () {
7349 var i, len;
7350
7351 this.groups = [];
7352 this.tools = {};
7353 for ( i = 0, len = this.items.length; i < len; i++ ) {
7354 this.items[ i ].destroy();
7355 }
7356 this.clearItems();
7357 };
7358
7359 /**
7360 * Destroy the toolbar.
7361 *
7362 * Destroying the toolbar removes all event handlers and DOM elements that constitute the toolbar. Call
7363 * this method whenever you are done using a toolbar.
7364 */
7365 OO.ui.Toolbar.prototype.destroy = function () {
7366 $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
7367 this.reset();
7368 this.$element.remove();
7369 };
7370
7371 /**
7372 * Check if the tool is available.
7373 *
7374 * Available tools are ones that have not yet been added to the toolbar.
7375 *
7376 * @param {string} name Symbolic name of tool
7377 * @return {boolean} Tool is available
7378 */
7379 OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
7380 return !this.tools[ name ];
7381 };
7382
7383 /**
7384 * Prevent tool from being used again.
7385 *
7386 * @param {OO.ui.Tool} tool Tool to reserve
7387 */
7388 OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
7389 this.tools[ tool.getName() ] = tool;
7390 };
7391
7392 /**
7393 * Allow tool to be used again.
7394 *
7395 * @param {OO.ui.Tool} tool Tool to release
7396 */
7397 OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
7398 delete this.tools[ tool.getName() ];
7399 };
7400
7401 /**
7402 * Get accelerator label for tool.
7403 *
7404 * The OOjs UI library does not contain an accelerator system, but this is the hook for one. To
7405 * use an accelerator system, subclass the toolbar and override this method, which is meant to return a label
7406 * that describes the accelerator keys for the tool passed (by symbolic name) to the method.
7407 *
7408 * @param {string} name Symbolic name of tool
7409 * @return {string|undefined} Tool accelerator label if available
7410 */
7411 OO.ui.Toolbar.prototype.getToolAccelerator = function () {
7412 return undefined;
7413 };
7414
7415 /**
7416 * ToolGroups are collections of {@link OO.ui.Tool tools} that are used in a {@link OO.ui.Toolbar toolbar}.
7417 * The type of toolgroup ({@link OO.ui.ListToolGroup list}, {@link OO.ui.BarToolGroup bar}, or {@link OO.ui.MenuToolGroup menu})
7418 * to which a tool belongs determines how the tool is arranged and displayed in the toolbar. Toolgroups
7419 * themselves are created on demand with a {@link OO.ui.ToolGroupFactory toolgroup factory}.
7420 *
7421 * Toolgroups can contain individual tools, groups of tools, or all available tools:
7422 *
7423 * To include an individual tool (or array of individual tools), specify tools by symbolic name:
7424 *
7425 * include: [ 'tool-name' ] or [ { name: 'tool-name' }]
7426 *
7427 * 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.)
7428 *
7429 * include: [ { group: 'group-name' } ]
7430 *
7431 * To include all tools that are not yet assigned to a toolgroup, use the catch-all selector, an asterisk (*):
7432 *
7433 * include: '*'
7434 *
7435 * See {@link OO.ui.Toolbar toolbars} for a full example. For more information about toolbars in general,
7436 * please see the [OOjs UI documentation on MediaWiki][1].
7437 *
7438 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
7439 *
7440 * @abstract
7441 * @class
7442 * @extends OO.ui.Widget
7443 * @mixins OO.ui.mixin.GroupElement
7444 *
7445 * @constructor
7446 * @param {OO.ui.Toolbar} toolbar
7447 * @param {Object} [config] Configuration options
7448 * @cfg {Array|string} [include=[]] List of tools to include in the toolgroup.
7449 * @cfg {Array|string} [exclude=[]] List of tools to exclude from the toolgroup.
7450 * @cfg {Array|string} [promote=[]] List of tools to promote to the beginning of the toolgroup.
7451 * @cfg {Array|string} [demote=[]] List of tools to demote to the end of the toolgroup.
7452 * This setting is particularly useful when tools have been added to the toolgroup
7453 * en masse (e.g., via the catch-all selector).
7454 */
7455 OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
7456 // Allow passing positional parameters inside the config object
7457 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
7458 config = toolbar;
7459 toolbar = config.toolbar;
7460 }
7461
7462 // Configuration initialization
7463 config = config || {};
7464
7465 // Parent constructor
7466 OO.ui.ToolGroup.parent.call( this, config );
7467
7468 // Mixin constructors
7469 OO.ui.mixin.GroupElement.call( this, config );
7470
7471 // Properties
7472 this.toolbar = toolbar;
7473 this.tools = {};
7474 this.pressed = null;
7475 this.autoDisabled = false;
7476 this.include = config.include || [];
7477 this.exclude = config.exclude || [];
7478 this.promote = config.promote || [];
7479 this.demote = config.demote || [];
7480 this.onCapturedMouseKeyUpHandler = this.onCapturedMouseKeyUp.bind( this );
7481
7482 // Events
7483 this.$element.on( {
7484 mousedown: this.onMouseKeyDown.bind( this ),
7485 mouseup: this.onMouseKeyUp.bind( this ),
7486 keydown: this.onMouseKeyDown.bind( this ),
7487 keyup: this.onMouseKeyUp.bind( this ),
7488 focus: this.onMouseOverFocus.bind( this ),
7489 blur: this.onMouseOutBlur.bind( this ),
7490 mouseover: this.onMouseOverFocus.bind( this ),
7491 mouseout: this.onMouseOutBlur.bind( this )
7492 } );
7493 this.toolbar.getToolFactory().connect( this, { register: 'onToolFactoryRegister' } );
7494 this.aggregate( { disable: 'itemDisable' } );
7495 this.connect( this, { itemDisable: 'updateDisabled' } );
7496
7497 // Initialization
7498 this.$group.addClass( 'oo-ui-toolGroup-tools' );
7499 this.$element
7500 .addClass( 'oo-ui-toolGroup' )
7501 .append( this.$group );
7502 this.populate();
7503 };
7504
7505 /* Setup */
7506
7507 OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget );
7508 OO.mixinClass( OO.ui.ToolGroup, OO.ui.mixin.GroupElement );
7509
7510 /* Events */
7511
7512 /**
7513 * @event update
7514 */
7515
7516 /* Static Properties */
7517
7518 /**
7519 * Show labels in tooltips.
7520 *
7521 * @static
7522 * @inheritable
7523 * @property {boolean}
7524 */
7525 OO.ui.ToolGroup.static.titleTooltips = false;
7526
7527 /**
7528 * Show acceleration labels in tooltips.
7529 *
7530 * Note: The OOjs UI library does not include an accelerator system, but does contain
7531 * a hook for one. To use an accelerator system, subclass the {@link OO.ui.Toolbar toolbar} and
7532 * override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method, which is
7533 * meant to return a label that describes the accelerator keys for a given tool (e.g., 'Ctrl + M').
7534 *
7535 * @static
7536 * @inheritable
7537 * @property {boolean}
7538 */
7539 OO.ui.ToolGroup.static.accelTooltips = false;
7540
7541 /**
7542 * Automatically disable the toolgroup when all tools are disabled
7543 *
7544 * @static
7545 * @inheritable
7546 * @property {boolean}
7547 */
7548 OO.ui.ToolGroup.static.autoDisable = true;
7549
7550 /* Methods */
7551
7552 /**
7553 * @inheritdoc
7554 */
7555 OO.ui.ToolGroup.prototype.isDisabled = function () {
7556 return this.autoDisabled || OO.ui.ToolGroup.parent.prototype.isDisabled.apply( this, arguments );
7557 };
7558
7559 /**
7560 * @inheritdoc
7561 */
7562 OO.ui.ToolGroup.prototype.updateDisabled = function () {
7563 var i, item, allDisabled = true;
7564
7565 if ( this.constructor.static.autoDisable ) {
7566 for ( i = this.items.length - 1; i >= 0; i-- ) {
7567 item = this.items[ i ];
7568 if ( !item.isDisabled() ) {
7569 allDisabled = false;
7570 break;
7571 }
7572 }
7573 this.autoDisabled = allDisabled;
7574 }
7575 OO.ui.ToolGroup.parent.prototype.updateDisabled.apply( this, arguments );
7576 };
7577
7578 /**
7579 * Handle mouse down and key down events.
7580 *
7581 * @protected
7582 * @param {jQuery.Event} e Mouse down or key down event
7583 */
7584 OO.ui.ToolGroup.prototype.onMouseKeyDown = function ( e ) {
7585 if (
7586 !this.isDisabled() &&
7587 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
7588 ) {
7589 this.pressed = this.getTargetTool( e );
7590 if ( this.pressed ) {
7591 this.pressed.setActive( true );
7592 this.getElementDocument().addEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler, true );
7593 this.getElementDocument().addEventListener( 'keyup', this.onCapturedMouseKeyUpHandler, true );
7594 }
7595 return false;
7596 }
7597 };
7598
7599 /**
7600 * Handle captured mouse up and key up events.
7601 *
7602 * @protected
7603 * @param {Event} e Mouse up or key up event
7604 */
7605 OO.ui.ToolGroup.prototype.onCapturedMouseKeyUp = function ( e ) {
7606 this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler, true );
7607 this.getElementDocument().removeEventListener( 'keyup', this.onCapturedMouseKeyUpHandler, true );
7608 // onMouseKeyUp may be called a second time, depending on where the mouse is when the button is
7609 // released, but since `this.pressed` will no longer be true, the second call will be ignored.
7610 this.onMouseKeyUp( e );
7611 };
7612
7613 /**
7614 * Handle mouse up and key up events.
7615 *
7616 * @protected
7617 * @param {jQuery.Event} e Mouse up or key up event
7618 */
7619 OO.ui.ToolGroup.prototype.onMouseKeyUp = function ( e ) {
7620 var tool = this.getTargetTool( e );
7621
7622 if (
7623 !this.isDisabled() && this.pressed && this.pressed === tool &&
7624 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
7625 ) {
7626 this.pressed.onSelect();
7627 this.pressed = null;
7628 return false;
7629 }
7630
7631 this.pressed = null;
7632 };
7633
7634 /**
7635 * Handle mouse over and focus events.
7636 *
7637 * @protected
7638 * @param {jQuery.Event} e Mouse over or focus event
7639 */
7640 OO.ui.ToolGroup.prototype.onMouseOverFocus = function ( e ) {
7641 var tool = this.getTargetTool( e );
7642
7643 if ( this.pressed && this.pressed === tool ) {
7644 this.pressed.setActive( true );
7645 }
7646 };
7647
7648 /**
7649 * Handle mouse out and blur events.
7650 *
7651 * @protected
7652 * @param {jQuery.Event} e Mouse out or blur event
7653 */
7654 OO.ui.ToolGroup.prototype.onMouseOutBlur = function ( e ) {
7655 var tool = this.getTargetTool( e );
7656
7657 if ( this.pressed && this.pressed === tool ) {
7658 this.pressed.setActive( false );
7659 }
7660 };
7661
7662 /**
7663 * Get the closest tool to a jQuery.Event.
7664 *
7665 * Only tool links are considered, which prevents other elements in the tool such as popups from
7666 * triggering tool group interactions.
7667 *
7668 * @private
7669 * @param {jQuery.Event} e
7670 * @return {OO.ui.Tool|null} Tool, `null` if none was found
7671 */
7672 OO.ui.ToolGroup.prototype.getTargetTool = function ( e ) {
7673 var tool,
7674 $item = $( e.target ).closest( '.oo-ui-tool-link' );
7675
7676 if ( $item.length ) {
7677 tool = $item.parent().data( 'oo-ui-tool' );
7678 }
7679
7680 return tool && !tool.isDisabled() ? tool : null;
7681 };
7682
7683 /**
7684 * Handle tool registry register events.
7685 *
7686 * If a tool is registered after the group is created, we must repopulate the list to account for:
7687 *
7688 * - a tool being added that may be included
7689 * - a tool already included being overridden
7690 *
7691 * @protected
7692 * @param {string} name Symbolic name of tool
7693 */
7694 OO.ui.ToolGroup.prototype.onToolFactoryRegister = function () {
7695 this.populate();
7696 };
7697
7698 /**
7699 * Get the toolbar that contains the toolgroup.
7700 *
7701 * @return {OO.ui.Toolbar} Toolbar that contains the toolgroup
7702 */
7703 OO.ui.ToolGroup.prototype.getToolbar = function () {
7704 return this.toolbar;
7705 };
7706
7707 /**
7708 * Add and remove tools based on configuration.
7709 */
7710 OO.ui.ToolGroup.prototype.populate = function () {
7711 var i, len, name, tool,
7712 toolFactory = this.toolbar.getToolFactory(),
7713 names = {},
7714 add = [],
7715 remove = [],
7716 list = this.toolbar.getToolFactory().getTools(
7717 this.include, this.exclude, this.promote, this.demote
7718 );
7719
7720 // Build a list of needed tools
7721 for ( i = 0, len = list.length; i < len; i++ ) {
7722 name = list[ i ];
7723 if (
7724 // Tool exists
7725 toolFactory.lookup( name ) &&
7726 // Tool is available or is already in this group
7727 ( this.toolbar.isToolAvailable( name ) || this.tools[ name ] )
7728 ) {
7729 // Hack to prevent infinite recursion via ToolGroupTool. We need to reserve the tool before
7730 // creating it, but we can't call reserveTool() yet because we haven't created the tool.
7731 this.toolbar.tools[ name ] = true;
7732 tool = this.tools[ name ];
7733 if ( !tool ) {
7734 // Auto-initialize tools on first use
7735 this.tools[ name ] = tool = toolFactory.create( name, this );
7736 tool.updateTitle();
7737 }
7738 this.toolbar.reserveTool( tool );
7739 add.push( tool );
7740 names[ name ] = true;
7741 }
7742 }
7743 // Remove tools that are no longer needed
7744 for ( name in this.tools ) {
7745 if ( !names[ name ] ) {
7746 this.tools[ name ].destroy();
7747 this.toolbar.releaseTool( this.tools[ name ] );
7748 remove.push( this.tools[ name ] );
7749 delete this.tools[ name ];
7750 }
7751 }
7752 if ( remove.length ) {
7753 this.removeItems( remove );
7754 }
7755 // Update emptiness state
7756 if ( add.length ) {
7757 this.$element.removeClass( 'oo-ui-toolGroup-empty' );
7758 } else {
7759 this.$element.addClass( 'oo-ui-toolGroup-empty' );
7760 }
7761 // Re-add tools (moving existing ones to new locations)
7762 this.addItems( add );
7763 // Disabled state may depend on items
7764 this.updateDisabled();
7765 };
7766
7767 /**
7768 * Destroy toolgroup.
7769 */
7770 OO.ui.ToolGroup.prototype.destroy = function () {
7771 var name;
7772
7773 this.clearItems();
7774 this.toolbar.getToolFactory().disconnect( this );
7775 for ( name in this.tools ) {
7776 this.toolbar.releaseTool( this.tools[ name ] );
7777 this.tools[ name ].disconnect( this ).destroy();
7778 delete this.tools[ name ];
7779 }
7780 this.$element.remove();
7781 };
7782
7783 /**
7784 * MessageDialogs display a confirmation or alert message. By default, the rendered dialog box
7785 * consists of a header that contains the dialog title, a body with the message, and a footer that
7786 * contains any {@link OO.ui.ActionWidget action widgets}. The MessageDialog class is the only type
7787 * of {@link OO.ui.Dialog dialog} that is usually instantiated directly.
7788 *
7789 * There are two basic types of message dialogs, confirmation and alert:
7790 *
7791 * - **confirmation**: the dialog title describes what a progressive action will do and the message provides
7792 * more details about the consequences.
7793 * - **alert**: the dialog title describes which event occurred and the message provides more information
7794 * about why the event occurred.
7795 *
7796 * The MessageDialog class specifies two actions: ‘accept’, the primary
7797 * action (e.g., ‘ok’) and ‘reject,’ the safe action (e.g., ‘cancel’). Both will close the window,
7798 * passing along the selected action.
7799 *
7800 * For more information and examples, please see the [OOjs UI documentation on MediaWiki][1].
7801 *
7802 * @example
7803 * // Example: Creating and opening a message dialog window.
7804 * var messageDialog = new OO.ui.MessageDialog();
7805 *
7806 * // Create and append a window manager.
7807 * var windowManager = new OO.ui.WindowManager();
7808 * $( 'body' ).append( windowManager.$element );
7809 * windowManager.addWindows( [ messageDialog ] );
7810 * // Open the window.
7811 * windowManager.openWindow( messageDialog, {
7812 * title: 'Basic message dialog',
7813 * message: 'This is the message'
7814 * } );
7815 *
7816 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Message_Dialogs
7817 *
7818 * @class
7819 * @extends OO.ui.Dialog
7820 *
7821 * @constructor
7822 * @param {Object} [config] Configuration options
7823 */
7824 OO.ui.MessageDialog = function OoUiMessageDialog( config ) {
7825 // Parent constructor
7826 OO.ui.MessageDialog.parent.call( this, config );
7827
7828 // Properties
7829 this.verticalActionLayout = null;
7830
7831 // Initialization
7832 this.$element.addClass( 'oo-ui-messageDialog' );
7833 };
7834
7835 /* Inheritance */
7836
7837 OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog );
7838
7839 /* Static Properties */
7840
7841 OO.ui.MessageDialog.static.name = 'message';
7842
7843 OO.ui.MessageDialog.static.size = 'small';
7844
7845 OO.ui.MessageDialog.static.verbose = false;
7846
7847 /**
7848 * Dialog title.
7849 *
7850 * The title of a confirmation dialog describes what a progressive action will do. The
7851 * title of an alert dialog describes which event occurred.
7852 *
7853 * @static
7854 * @inheritable
7855 * @property {jQuery|string|Function|null}
7856 */
7857 OO.ui.MessageDialog.static.title = null;
7858
7859 /**
7860 * The message displayed in the dialog body.
7861 *
7862 * A confirmation message describes the consequences of a progressive action. An alert
7863 * message describes why an event occurred.
7864 *
7865 * @static
7866 * @inheritable
7867 * @property {jQuery|string|Function|null}
7868 */
7869 OO.ui.MessageDialog.static.message = null;
7870
7871 OO.ui.MessageDialog.static.actions = [
7872 { action: 'accept', label: OO.ui.deferMsg( 'ooui-dialog-message-accept' ), flags: 'primary' },
7873 { action: 'reject', label: OO.ui.deferMsg( 'ooui-dialog-message-reject' ), flags: 'safe' }
7874 ];
7875
7876 /* Methods */
7877
7878 /**
7879 * @inheritdoc
7880 */
7881 OO.ui.MessageDialog.prototype.setManager = function ( manager ) {
7882 OO.ui.MessageDialog.parent.prototype.setManager.call( this, manager );
7883
7884 // Events
7885 this.manager.connect( this, {
7886 resize: 'onResize'
7887 } );
7888
7889 return this;
7890 };
7891
7892 /**
7893 * @inheritdoc
7894 */
7895 OO.ui.MessageDialog.prototype.onActionResize = function ( action ) {
7896 this.fitActions();
7897 return OO.ui.MessageDialog.parent.prototype.onActionResize.call( this, action );
7898 };
7899
7900 /**
7901 * Handle window resized events.
7902 *
7903 * @private
7904 */
7905 OO.ui.MessageDialog.prototype.onResize = function () {
7906 var dialog = this;
7907 dialog.fitActions();
7908 // Wait for CSS transition to finish and do it again :(
7909 setTimeout( function () {
7910 dialog.fitActions();
7911 }, 300 );
7912 };
7913
7914 /**
7915 * Toggle action layout between vertical and horizontal.
7916 *
7917 *
7918 * @private
7919 * @param {boolean} [value] Layout actions vertically, omit to toggle
7920 * @chainable
7921 */
7922 OO.ui.MessageDialog.prototype.toggleVerticalActionLayout = function ( value ) {
7923 value = value === undefined ? !this.verticalActionLayout : !!value;
7924
7925 if ( value !== this.verticalActionLayout ) {
7926 this.verticalActionLayout = value;
7927 this.$actions
7928 .toggleClass( 'oo-ui-messageDialog-actions-vertical', value )
7929 .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value );
7930 }
7931
7932 return this;
7933 };
7934
7935 /**
7936 * @inheritdoc
7937 */
7938 OO.ui.MessageDialog.prototype.getActionProcess = function ( action ) {
7939 if ( action ) {
7940 return new OO.ui.Process( function () {
7941 this.close( { action: action } );
7942 }, this );
7943 }
7944 return OO.ui.MessageDialog.parent.prototype.getActionProcess.call( this, action );
7945 };
7946
7947 /**
7948 * @inheritdoc
7949 *
7950 * @param {Object} [data] Dialog opening data
7951 * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
7952 * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
7953 * @param {boolean} [data.verbose] Message is verbose and should be styled as a long message
7954 * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each
7955 * action item
7956 */
7957 OO.ui.MessageDialog.prototype.getSetupProcess = function ( data ) {
7958 data = data || {};
7959
7960 // Parent method
7961 return OO.ui.MessageDialog.parent.prototype.getSetupProcess.call( this, data )
7962 .next( function () {
7963 this.title.setLabel(
7964 data.title !== undefined ? data.title : this.constructor.static.title
7965 );
7966 this.message.setLabel(
7967 data.message !== undefined ? data.message : this.constructor.static.message
7968 );
7969 this.message.$element.toggleClass(
7970 'oo-ui-messageDialog-message-verbose',
7971 data.verbose !== undefined ? data.verbose : this.constructor.static.verbose
7972 );
7973 }, this );
7974 };
7975
7976 /**
7977 * @inheritdoc
7978 */
7979 OO.ui.MessageDialog.prototype.getBodyHeight = function () {
7980 var bodyHeight, oldOverflow,
7981 $scrollable = this.container.$element;
7982
7983 oldOverflow = $scrollable[ 0 ].style.overflow;
7984 $scrollable[ 0 ].style.overflow = 'hidden';
7985
7986 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
7987
7988 bodyHeight = this.text.$element.outerHeight( true );
7989 $scrollable[ 0 ].style.overflow = oldOverflow;
7990
7991 return bodyHeight;
7992 };
7993
7994 /**
7995 * @inheritdoc
7996 */
7997 OO.ui.MessageDialog.prototype.setDimensions = function ( dim ) {
7998 var $scrollable = this.container.$element;
7999 OO.ui.MessageDialog.parent.prototype.setDimensions.call( this, dim );
8000
8001 // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
8002 // Need to do it after transition completes (250ms), add 50ms just in case.
8003 setTimeout( function () {
8004 var oldOverflow = $scrollable[ 0 ].style.overflow;
8005 $scrollable[ 0 ].style.overflow = 'hidden';
8006
8007 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
8008
8009 $scrollable[ 0 ].style.overflow = oldOverflow;
8010 }, 300 );
8011
8012 return this;
8013 };
8014
8015 /**
8016 * @inheritdoc
8017 */
8018 OO.ui.MessageDialog.prototype.initialize = function () {
8019 // Parent method
8020 OO.ui.MessageDialog.parent.prototype.initialize.call( this );
8021
8022 // Properties
8023 this.$actions = $( '<div>' );
8024 this.container = new OO.ui.PanelLayout( {
8025 scrollable: true, classes: [ 'oo-ui-messageDialog-container' ]
8026 } );
8027 this.text = new OO.ui.PanelLayout( {
8028 padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ]
8029 } );
8030 this.message = new OO.ui.LabelWidget( {
8031 classes: [ 'oo-ui-messageDialog-message' ]
8032 } );
8033
8034 // Initialization
8035 this.title.$element.addClass( 'oo-ui-messageDialog-title' );
8036 this.$content.addClass( 'oo-ui-messageDialog-content' );
8037 this.container.$element.append( this.text.$element );
8038 this.text.$element.append( this.title.$element, this.message.$element );
8039 this.$body.append( this.container.$element );
8040 this.$actions.addClass( 'oo-ui-messageDialog-actions' );
8041 this.$foot.append( this.$actions );
8042 };
8043
8044 /**
8045 * @inheritdoc
8046 */
8047 OO.ui.MessageDialog.prototype.attachActions = function () {
8048 var i, len, other, special, others;
8049
8050 // Parent method
8051 OO.ui.MessageDialog.parent.prototype.attachActions.call( this );
8052
8053 special = this.actions.getSpecial();
8054 others = this.actions.getOthers();
8055 if ( special.safe ) {
8056 this.$actions.append( special.safe.$element );
8057 special.safe.toggleFramed( false );
8058 }
8059 if ( others.length ) {
8060 for ( i = 0, len = others.length; i < len; i++ ) {
8061 other = others[ i ];
8062 this.$actions.append( other.$element );
8063 other.toggleFramed( false );
8064 }
8065 }
8066 if ( special.primary ) {
8067 this.$actions.append( special.primary.$element );
8068 special.primary.toggleFramed( false );
8069 }
8070
8071 if ( !this.isOpening() ) {
8072 // If the dialog is currently opening, this will be called automatically soon.
8073 // This also calls #fitActions.
8074 this.updateSize();
8075 }
8076 };
8077
8078 /**
8079 * Fit action actions into columns or rows.
8080 *
8081 * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
8082 *
8083 * @private
8084 */
8085 OO.ui.MessageDialog.prototype.fitActions = function () {
8086 var i, len, action,
8087 previous = this.verticalActionLayout,
8088 actions = this.actions.get();
8089
8090 // Detect clipping
8091 this.toggleVerticalActionLayout( false );
8092 for ( i = 0, len = actions.length; i < len; i++ ) {
8093 action = actions[ i ];
8094 if ( action.$element.innerWidth() < action.$label.outerWidth( true ) ) {
8095 this.toggleVerticalActionLayout( true );
8096 break;
8097 }
8098 }
8099
8100 // Move the body out of the way of the foot
8101 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
8102
8103 if ( this.verticalActionLayout !== previous ) {
8104 // We changed the layout, window height might need to be updated.
8105 this.updateSize();
8106 }
8107 };
8108
8109 /**
8110 * ProcessDialog windows encapsulate a {@link OO.ui.Process process} and all of the code necessary
8111 * to complete it. If the process terminates with an error, a customizable {@link OO.ui.Error error
8112 * interface} alerts users to the trouble, permitting the user to dismiss the error and try again when
8113 * relevant. The ProcessDialog class is always extended and customized with the actions and content
8114 * required for each process.
8115 *
8116 * The process dialog box consists of a header that visually represents the ‘working’ state of long
8117 * processes with an animation. The header contains the dialog title as well as
8118 * two {@link OO.ui.ActionWidget action widgets}: a ‘safe’ action on the left (e.g., ‘Cancel’) and
8119 * a ‘primary’ action on the right (e.g., ‘Done’).
8120 *
8121 * Like other windows, the process dialog is managed by a {@link OO.ui.WindowManager window manager}.
8122 * Please see the [OOjs UI documentation on MediaWiki][1] for more information and examples.
8123 *
8124 * @example
8125 * // Example: Creating and opening a process dialog window.
8126 * function MyProcessDialog( config ) {
8127 * MyProcessDialog.parent.call( this, config );
8128 * }
8129 * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
8130 *
8131 * MyProcessDialog.static.title = 'Process dialog';
8132 * MyProcessDialog.static.actions = [
8133 * { action: 'save', label: 'Done', flags: 'primary' },
8134 * { label: 'Cancel', flags: 'safe' }
8135 * ];
8136 *
8137 * MyProcessDialog.prototype.initialize = function () {
8138 * MyProcessDialog.parent.prototype.initialize.apply( this, arguments );
8139 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
8140 * 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>' );
8141 * this.$body.append( this.content.$element );
8142 * };
8143 * MyProcessDialog.prototype.getActionProcess = function ( action ) {
8144 * var dialog = this;
8145 * if ( action ) {
8146 * return new OO.ui.Process( function () {
8147 * dialog.close( { action: action } );
8148 * } );
8149 * }
8150 * return MyProcessDialog.parent.prototype.getActionProcess.call( this, action );
8151 * };
8152 *
8153 * var windowManager = new OO.ui.WindowManager();
8154 * $( 'body' ).append( windowManager.$element );
8155 *
8156 * var dialog = new MyProcessDialog();
8157 * windowManager.addWindows( [ dialog ] );
8158 * windowManager.openWindow( dialog );
8159 *
8160 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
8161 *
8162 * @abstract
8163 * @class
8164 * @extends OO.ui.Dialog
8165 *
8166 * @constructor
8167 * @param {Object} [config] Configuration options
8168 */
8169 OO.ui.ProcessDialog = function OoUiProcessDialog( config ) {
8170 // Parent constructor
8171 OO.ui.ProcessDialog.parent.call( this, config );
8172
8173 // Initialization
8174 this.$element.addClass( 'oo-ui-processDialog' );
8175 };
8176
8177 /* Setup */
8178
8179 OO.inheritClass( OO.ui.ProcessDialog, OO.ui.Dialog );
8180
8181 /* Methods */
8182
8183 /**
8184 * Handle dismiss button click events.
8185 *
8186 * Hides errors.
8187 *
8188 * @private
8189 */
8190 OO.ui.ProcessDialog.prototype.onDismissErrorButtonClick = function () {
8191 this.hideErrors();
8192 };
8193
8194 /**
8195 * Handle retry button click events.
8196 *
8197 * Hides errors and then tries again.
8198 *
8199 * @private
8200 */
8201 OO.ui.ProcessDialog.prototype.onRetryButtonClick = function () {
8202 this.hideErrors();
8203 this.executeAction( this.currentAction );
8204 };
8205
8206 /**
8207 * @inheritdoc
8208 */
8209 OO.ui.ProcessDialog.prototype.onActionResize = function ( action ) {
8210 if ( this.actions.isSpecial( action ) ) {
8211 this.fitLabel();
8212 }
8213 return OO.ui.ProcessDialog.parent.prototype.onActionResize.call( this, action );
8214 };
8215
8216 /**
8217 * @inheritdoc
8218 */
8219 OO.ui.ProcessDialog.prototype.initialize = function () {
8220 // Parent method
8221 OO.ui.ProcessDialog.parent.prototype.initialize.call( this );
8222
8223 // Properties
8224 this.$navigation = $( '<div>' );
8225 this.$location = $( '<div>' );
8226 this.$safeActions = $( '<div>' );
8227 this.$primaryActions = $( '<div>' );
8228 this.$otherActions = $( '<div>' );
8229 this.dismissButton = new OO.ui.ButtonWidget( {
8230 label: OO.ui.msg( 'ooui-dialog-process-dismiss' )
8231 } );
8232 this.retryButton = new OO.ui.ButtonWidget();
8233 this.$errors = $( '<div>' );
8234 this.$errorsTitle = $( '<div>' );
8235
8236 // Events
8237 this.dismissButton.connect( this, { click: 'onDismissErrorButtonClick' } );
8238 this.retryButton.connect( this, { click: 'onRetryButtonClick' } );
8239
8240 // Initialization
8241 this.title.$element.addClass( 'oo-ui-processDialog-title' );
8242 this.$location
8243 .append( this.title.$element )
8244 .addClass( 'oo-ui-processDialog-location' );
8245 this.$safeActions.addClass( 'oo-ui-processDialog-actions-safe' );
8246 this.$primaryActions.addClass( 'oo-ui-processDialog-actions-primary' );
8247 this.$otherActions.addClass( 'oo-ui-processDialog-actions-other' );
8248 this.$errorsTitle
8249 .addClass( 'oo-ui-processDialog-errors-title' )
8250 .text( OO.ui.msg( 'ooui-dialog-process-error' ) );
8251 this.$errors
8252 .addClass( 'oo-ui-processDialog-errors oo-ui-element-hidden' )
8253 .append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element );
8254 this.$content
8255 .addClass( 'oo-ui-processDialog-content' )
8256 .append( this.$errors );
8257 this.$navigation
8258 .addClass( 'oo-ui-processDialog-navigation' )
8259 .append( this.$safeActions, this.$location, this.$primaryActions );
8260 this.$head.append( this.$navigation );
8261 this.$foot.append( this.$otherActions );
8262 };
8263
8264 /**
8265 * @inheritdoc
8266 */
8267 OO.ui.ProcessDialog.prototype.getActionWidgets = function ( actions ) {
8268 var i, len, widgets = [];
8269 for ( i = 0, len = actions.length; i < len; i++ ) {
8270 widgets.push(
8271 new OO.ui.ActionWidget( $.extend( { framed: true }, actions[ i ] ) )
8272 );
8273 }
8274 return widgets;
8275 };
8276
8277 /**
8278 * @inheritdoc
8279 */
8280 OO.ui.ProcessDialog.prototype.attachActions = function () {
8281 var i, len, other, special, others;
8282
8283 // Parent method
8284 OO.ui.ProcessDialog.parent.prototype.attachActions.call( this );
8285
8286 special = this.actions.getSpecial();
8287 others = this.actions.getOthers();
8288 if ( special.primary ) {
8289 this.$primaryActions.append( special.primary.$element );
8290 }
8291 for ( i = 0, len = others.length; i < len; i++ ) {
8292 other = others[ i ];
8293 this.$otherActions.append( other.$element );
8294 }
8295 if ( special.safe ) {
8296 this.$safeActions.append( special.safe.$element );
8297 }
8298
8299 this.fitLabel();
8300 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
8301 };
8302
8303 /**
8304 * @inheritdoc
8305 */
8306 OO.ui.ProcessDialog.prototype.executeAction = function ( action ) {
8307 var process = this;
8308 return OO.ui.ProcessDialog.parent.prototype.executeAction.call( this, action )
8309 .fail( function ( errors ) {
8310 process.showErrors( errors || [] );
8311 } );
8312 };
8313
8314 /**
8315 * Fit label between actions.
8316 *
8317 * @private
8318 * @chainable
8319 */
8320 OO.ui.ProcessDialog.prototype.fitLabel = function () {
8321 var safeWidth, primaryWidth, biggerWidth, labelWidth, navigationWidth, leftWidth, rightWidth;
8322
8323 safeWidth = this.$safeActions.is( ':visible' ) ? this.$safeActions.width() : 0;
8324 primaryWidth = this.$primaryActions.is( ':visible' ) ? this.$primaryActions.width() : 0;
8325 biggerWidth = Math.max( safeWidth, primaryWidth );
8326
8327 labelWidth = this.title.$element.width();
8328 // Is there a better way to calculate this?
8329 navigationWidth = OO.ui.WindowManager.static.sizes[ this.getSize() ].width - 20;
8330
8331 if ( 2 * biggerWidth + labelWidth < navigationWidth ) {
8332 // We have enough space to center the label
8333 leftWidth = rightWidth = biggerWidth;
8334 } else {
8335 // Let's hope we at least have enough space not to overlap, because we can't wrap the label…
8336 if ( this.getDir() === 'ltr' ) {
8337 leftWidth = safeWidth;
8338 rightWidth = primaryWidth;
8339 } else {
8340 leftWidth = primaryWidth;
8341 rightWidth = safeWidth;
8342 }
8343 }
8344
8345 this.$location.css( { paddingLeft: leftWidth, paddingRight: rightWidth } );
8346
8347 return this;
8348 };
8349
8350 /**
8351 * Handle errors that occurred during accept or reject processes.
8352 *
8353 * @private
8354 * @param {OO.ui.Error[]|OO.ui.Error} errors Errors to be handled
8355 */
8356 OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
8357 var i, len, $item, actions,
8358 items = [],
8359 abilities = {},
8360 recoverable = true,
8361 warning = false;
8362
8363 if ( errors instanceof OO.ui.Error ) {
8364 errors = [ errors ];
8365 }
8366
8367 for ( i = 0, len = errors.length; i < len; i++ ) {
8368 if ( !errors[ i ].isRecoverable() ) {
8369 recoverable = false;
8370 }
8371 if ( errors[ i ].isWarning() ) {
8372 warning = true;
8373 }
8374 $item = $( '<div>' )
8375 .addClass( 'oo-ui-processDialog-error' )
8376 .append( errors[ i ].getMessage() );
8377 items.push( $item[ 0 ] );
8378 }
8379 this.$errorItems = $( items );
8380 if ( recoverable ) {
8381 abilities[this.currentAction] = true;
8382 // Copy the flags from the first matching action
8383 actions = this.actions.get( { actions: this.currentAction } );
8384 if ( actions.length ) {
8385 this.retryButton.clearFlags().setFlags( actions[0].getFlags() );
8386 }
8387 } else {
8388 abilities[this.currentAction] = false;
8389 this.actions.setAbilities( abilities );
8390 }
8391 if ( warning ) {
8392 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-continue' ) );
8393 } else {
8394 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-retry' ) );
8395 }
8396 this.retryButton.toggle( recoverable );
8397 this.$errorsTitle.after( this.$errorItems );
8398 this.$errors.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 );
8399 };
8400
8401 /**
8402 * Hide errors.
8403 *
8404 * @private
8405 */
8406 OO.ui.ProcessDialog.prototype.hideErrors = function () {
8407 this.$errors.addClass( 'oo-ui-element-hidden' );
8408 if ( this.$errorItems ) {
8409 this.$errorItems.remove();
8410 this.$errorItems = null;
8411 }
8412 };
8413
8414 /**
8415 * @inheritdoc
8416 */
8417 OO.ui.ProcessDialog.prototype.getTeardownProcess = function ( data ) {
8418 // Parent method
8419 return OO.ui.ProcessDialog.parent.prototype.getTeardownProcess.call( this, data )
8420 .first( function () {
8421 // Make sure to hide errors
8422 this.hideErrors();
8423 }, this );
8424 };
8425
8426 /**
8427 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
8428 * which is a widget that is specified by reference before any optional configuration settings.
8429 *
8430 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
8431 *
8432 * - **left**: The label is placed before the field-widget and aligned with the left margin.
8433 * A left-alignment is used for forms with many fields.
8434 * - **right**: The label is placed before the field-widget and aligned to the right margin.
8435 * A right-alignment is used for long but familiar forms which users tab through,
8436 * verifying the current field with a quick glance at the label.
8437 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
8438 * that users fill out from top to bottom.
8439 * - **inline**: The label is placed after the field-widget and aligned to the left.
8440 * An inline-alignment is best used with checkboxes or radio buttons.
8441 *
8442 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
8443 * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
8444 *
8445 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
8446 * @class
8447 * @extends OO.ui.Layout
8448 * @mixins OO.ui.mixin.LabelElement
8449 *
8450 * @constructor
8451 * @param {OO.ui.Widget} fieldWidget Field widget
8452 * @param {Object} [config] Configuration options
8453 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
8454 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a help icon will appear
8455 * in the upper-right corner of the rendered field.
8456 */
8457 OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
8458 // Allow passing positional parameters inside the config object
8459 if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
8460 config = fieldWidget;
8461 fieldWidget = config.fieldWidget;
8462 }
8463
8464 var hasInputWidget = fieldWidget.constructor.static.supportsSimpleLabel,
8465 div;
8466
8467 // Configuration initialization
8468 config = $.extend( { align: 'left' }, config );
8469
8470 // Parent constructor
8471 OO.ui.FieldLayout.parent.call( this, config );
8472
8473 // Mixin constructors
8474 OO.ui.mixin.LabelElement.call( this, config );
8475
8476 // Properties
8477 this.fieldWidget = fieldWidget;
8478 this.$field = $( '<div>' );
8479 this.$body = $( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
8480 this.align = null;
8481 if ( config.help ) {
8482 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
8483 classes: [ 'oo-ui-fieldLayout-help' ],
8484 framed: false,
8485 icon: 'info'
8486 } );
8487
8488 div = $( '<div>' );
8489 if ( config.help instanceof OO.ui.HtmlSnippet ) {
8490 div.html( config.help.toString() );
8491 } else {
8492 div.text( config.help );
8493 }
8494 this.popupButtonWidget.getPopup().$body.append(
8495 div.addClass( 'oo-ui-fieldLayout-help-content' )
8496 );
8497 this.$help = this.popupButtonWidget.$element;
8498 } else {
8499 this.$help = $( [] );
8500 }
8501
8502 // Events
8503 if ( hasInputWidget ) {
8504 this.$label.on( 'click', this.onLabelClick.bind( this ) );
8505 }
8506 this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
8507
8508 // Initialization
8509 this.$element
8510 .addClass( 'oo-ui-fieldLayout' )
8511 .append( this.$help, this.$body );
8512 this.$body.addClass( 'oo-ui-fieldLayout-body' );
8513 this.$field
8514 .addClass( 'oo-ui-fieldLayout-field' )
8515 .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget.isDisabled() )
8516 .append( this.fieldWidget.$element );
8517
8518 this.setAlignment( config.align );
8519 };
8520
8521 /* Setup */
8522
8523 OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
8524 OO.mixinClass( OO.ui.FieldLayout, OO.ui.mixin.LabelElement );
8525
8526 /* Methods */
8527
8528 /**
8529 * Handle field disable events.
8530 *
8531 * @private
8532 * @param {boolean} value Field is disabled
8533 */
8534 OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
8535 this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
8536 };
8537
8538 /**
8539 * Handle label mouse click events.
8540 *
8541 * @private
8542 * @param {jQuery.Event} e Mouse click event
8543 */
8544 OO.ui.FieldLayout.prototype.onLabelClick = function () {
8545 this.fieldWidget.simulateLabelClick();
8546 return false;
8547 };
8548
8549 /**
8550 * Get the widget contained by the field.
8551 *
8552 * @return {OO.ui.Widget} Field widget
8553 */
8554 OO.ui.FieldLayout.prototype.getField = function () {
8555 return this.fieldWidget;
8556 };
8557
8558 /**
8559 * Set the field alignment mode.
8560 *
8561 * @private
8562 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
8563 * @chainable
8564 */
8565 OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
8566 if ( value !== this.align ) {
8567 // Default to 'left'
8568 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) {
8569 value = 'left';
8570 }
8571 // Reorder elements
8572 if ( value === 'inline' ) {
8573 this.$body.append( this.$field, this.$label );
8574 } else {
8575 this.$body.append( this.$label, this.$field );
8576 }
8577 // Set classes. The following classes can be used here:
8578 // * oo-ui-fieldLayout-align-left
8579 // * oo-ui-fieldLayout-align-right
8580 // * oo-ui-fieldLayout-align-top
8581 // * oo-ui-fieldLayout-align-inline
8582 if ( this.align ) {
8583 this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
8584 }
8585 this.$element.addClass( 'oo-ui-fieldLayout-align-' + value );
8586 this.align = value;
8587 }
8588
8589 return this;
8590 };
8591
8592 /**
8593 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
8594 * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
8595 * is required and is specified before any optional configuration settings.
8596 *
8597 * Labels can be aligned in one of four ways:
8598 *
8599 * - **left**: The label is placed before the field-widget and aligned with the left margin.
8600 * A left-alignment is used for forms with many fields.
8601 * - **right**: The label is placed before the field-widget and aligned to the right margin.
8602 * A right-alignment is used for long but familiar forms which users tab through,
8603 * verifying the current field with a quick glance at the label.
8604 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
8605 * that users fill out from top to bottom.
8606 * - **inline**: The label is placed after the field-widget and aligned to the left.
8607 * An inline-alignment is best used with checkboxes or radio buttons.
8608 *
8609 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
8610 * text is specified.
8611 *
8612 * @example
8613 * // Example of an ActionFieldLayout
8614 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
8615 * new OO.ui.TextInputWidget( {
8616 * placeholder: 'Field widget'
8617 * } ),
8618 * new OO.ui.ButtonWidget( {
8619 * label: 'Button'
8620 * } ),
8621 * {
8622 * label: 'An ActionFieldLayout. This label is aligned top',
8623 * align: 'top',
8624 * help: 'This is help text'
8625 * }
8626 * );
8627 *
8628 * $( 'body' ).append( actionFieldLayout.$element );
8629 *
8630 *
8631 * @class
8632 * @extends OO.ui.FieldLayout
8633 *
8634 * @constructor
8635 * @param {OO.ui.Widget} fieldWidget Field widget
8636 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
8637 */
8638 OO.ui.ActionFieldLayout = function OoUiActionFieldLayout( fieldWidget, buttonWidget, config ) {
8639 // Allow passing positional parameters inside the config object
8640 if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
8641 config = fieldWidget;
8642 fieldWidget = config.fieldWidget;
8643 buttonWidget = config.buttonWidget;
8644 }
8645
8646 // Parent constructor
8647 OO.ui.ActionFieldLayout.parent.call( this, fieldWidget, config );
8648
8649 // Properties
8650 this.buttonWidget = buttonWidget;
8651 this.$button = $( '<div>' );
8652 this.$input = $( '<div>' );
8653
8654 // Initialization
8655 this.$element
8656 .addClass( 'oo-ui-actionFieldLayout' );
8657 this.$button
8658 .addClass( 'oo-ui-actionFieldLayout-button' )
8659 .append( this.buttonWidget.$element );
8660 this.$input
8661 .addClass( 'oo-ui-actionFieldLayout-input' )
8662 .append( this.fieldWidget.$element );
8663 this.$field
8664 .append( this.$input, this.$button );
8665 };
8666
8667 /* Setup */
8668
8669 OO.inheritClass( OO.ui.ActionFieldLayout, OO.ui.FieldLayout );
8670
8671 /**
8672 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
8673 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
8674 * configured with a label as well. For more information and examples,
8675 * please see the [OOjs UI documentation on MediaWiki][1].
8676 *
8677 * @example
8678 * // Example of a fieldset layout
8679 * var input1 = new OO.ui.TextInputWidget( {
8680 * placeholder: 'A text input field'
8681 * } );
8682 *
8683 * var input2 = new OO.ui.TextInputWidget( {
8684 * placeholder: 'A text input field'
8685 * } );
8686 *
8687 * var fieldset = new OO.ui.FieldsetLayout( {
8688 * label: 'Example of a fieldset layout'
8689 * } );
8690 *
8691 * fieldset.addItems( [
8692 * new OO.ui.FieldLayout( input1, {
8693 * label: 'Field One'
8694 * } ),
8695 * new OO.ui.FieldLayout( input2, {
8696 * label: 'Field Two'
8697 * } )
8698 * ] );
8699 * $( 'body' ).append( fieldset.$element );
8700 *
8701 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
8702 *
8703 * @class
8704 * @extends OO.ui.Layout
8705 * @mixins OO.ui.mixin.IconElement
8706 * @mixins OO.ui.mixin.LabelElement
8707 * @mixins OO.ui.mixin.GroupElement
8708 *
8709 * @constructor
8710 * @param {Object} [config] Configuration options
8711 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
8712 */
8713 OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
8714 // Configuration initialization
8715 config = config || {};
8716
8717 // Parent constructor
8718 OO.ui.FieldsetLayout.parent.call( this, config );
8719
8720 // Mixin constructors
8721 OO.ui.mixin.IconElement.call( this, config );
8722 OO.ui.mixin.LabelElement.call( this, config );
8723 OO.ui.mixin.GroupElement.call( this, config );
8724
8725 if ( config.help ) {
8726 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
8727 classes: [ 'oo-ui-fieldsetLayout-help' ],
8728 framed: false,
8729 icon: 'info'
8730 } );
8731
8732 this.popupButtonWidget.getPopup().$body.append(
8733 $( '<div>' )
8734 .text( config.help )
8735 .addClass( 'oo-ui-fieldsetLayout-help-content' )
8736 );
8737 this.$help = this.popupButtonWidget.$element;
8738 } else {
8739 this.$help = $( [] );
8740 }
8741
8742 // Initialization
8743 this.$element
8744 .addClass( 'oo-ui-fieldsetLayout' )
8745 .prepend( this.$help, this.$icon, this.$label, this.$group );
8746 if ( Array.isArray( config.items ) ) {
8747 this.addItems( config.items );
8748 }
8749 };
8750
8751 /* Setup */
8752
8753 OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
8754 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.IconElement );
8755 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.LabelElement );
8756 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.GroupElement );
8757
8758 /**
8759 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
8760 * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
8761 * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
8762 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
8763 *
8764 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
8765 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
8766 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
8767 * some fancier controls. Some controls have both regular and InputWidget variants, for example
8768 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
8769 * often have simplified APIs to match the capabilities of HTML forms.
8770 * See the [OOjs UI Inputs documentation on MediaWiki] [2] for more information about InputWidgets.
8771 *
8772 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Forms
8773 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8774 *
8775 * @example
8776 * // Example of a form layout that wraps a fieldset layout
8777 * var input1 = new OO.ui.TextInputWidget( {
8778 * placeholder: 'Username'
8779 * } );
8780 * var input2 = new OO.ui.TextInputWidget( {
8781 * placeholder: 'Password',
8782 * type: 'password'
8783 * } );
8784 * var submit = new OO.ui.ButtonInputWidget( {
8785 * label: 'Submit'
8786 * } );
8787 *
8788 * var fieldset = new OO.ui.FieldsetLayout( {
8789 * label: 'A form layout'
8790 * } );
8791 * fieldset.addItems( [
8792 * new OO.ui.FieldLayout( input1, {
8793 * label: 'Username',
8794 * align: 'top'
8795 * } ),
8796 * new OO.ui.FieldLayout( input2, {
8797 * label: 'Password',
8798 * align: 'top'
8799 * } ),
8800 * new OO.ui.FieldLayout( submit )
8801 * ] );
8802 * var form = new OO.ui.FormLayout( {
8803 * items: [ fieldset ],
8804 * action: '/api/formhandler',
8805 * method: 'get'
8806 * } )
8807 * $( 'body' ).append( form.$element );
8808 *
8809 * @class
8810 * @extends OO.ui.Layout
8811 * @mixins OO.ui.mixin.GroupElement
8812 *
8813 * @constructor
8814 * @param {Object} [config] Configuration options
8815 * @cfg {string} [method] HTML form `method` attribute
8816 * @cfg {string} [action] HTML form `action` attribute
8817 * @cfg {string} [enctype] HTML form `enctype` attribute
8818 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
8819 */
8820 OO.ui.FormLayout = function OoUiFormLayout( config ) {
8821 // Configuration initialization
8822 config = config || {};
8823
8824 // Parent constructor
8825 OO.ui.FormLayout.parent.call( this, config );
8826
8827 // Mixin constructors
8828 OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
8829
8830 // Events
8831 this.$element.on( 'submit', this.onFormSubmit.bind( this ) );
8832
8833 // Initialization
8834 this.$element
8835 .addClass( 'oo-ui-formLayout' )
8836 .attr( {
8837 method: config.method,
8838 action: config.action,
8839 enctype: config.enctype
8840 } );
8841 if ( Array.isArray( config.items ) ) {
8842 this.addItems( config.items );
8843 }
8844 };
8845
8846 /* Setup */
8847
8848 OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout );
8849 OO.mixinClass( OO.ui.FormLayout, OO.ui.mixin.GroupElement );
8850
8851 /* Events */
8852
8853 /**
8854 * A 'submit' event is emitted when the form is submitted.
8855 *
8856 * @event submit
8857 */
8858
8859 /* Static Properties */
8860
8861 OO.ui.FormLayout.static.tagName = 'form';
8862
8863 /* Methods */
8864
8865 /**
8866 * Handle form submit events.
8867 *
8868 * @private
8869 * @param {jQuery.Event} e Submit event
8870 * @fires submit
8871 */
8872 OO.ui.FormLayout.prototype.onFormSubmit = function () {
8873 if ( this.emit( 'submit' ) ) {
8874 return false;
8875 }
8876 };
8877
8878 /**
8879 * 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)
8880 * and its size is customized with the #menuSize config. The content area will fill all remaining space.
8881 *
8882 * @example
8883 * var menuLayout = new OO.ui.MenuLayout( {
8884 * position: 'top'
8885 * } ),
8886 * menuPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
8887 * contentPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
8888 * select = new OO.ui.SelectWidget( {
8889 * items: [
8890 * new OO.ui.OptionWidget( {
8891 * data: 'before',
8892 * label: 'Before',
8893 * } ),
8894 * new OO.ui.OptionWidget( {
8895 * data: 'after',
8896 * label: 'After',
8897 * } ),
8898 * new OO.ui.OptionWidget( {
8899 * data: 'top',
8900 * label: 'Top',
8901 * } ),
8902 * new OO.ui.OptionWidget( {
8903 * data: 'bottom',
8904 * label: 'Bottom',
8905 * } )
8906 * ]
8907 * } ).on( 'select', function ( item ) {
8908 * menuLayout.setMenuPosition( item.getData() );
8909 * } );
8910 *
8911 * menuLayout.$menu.append(
8912 * menuPanel.$element.append( '<b>Menu panel</b>', select.$element )
8913 * );
8914 * menuLayout.$content.append(
8915 * contentPanel.$element.append( '<b>Content panel</b>', '<p>Note that the menu is positioned relative to the content panel: top, bottom, after, before.</p>')
8916 * );
8917 * $( 'body' ).append( menuLayout.$element );
8918 *
8919 * If menu size needs to be overridden, it can be accomplished using CSS similar to the snippet
8920 * below. MenuLayout's CSS will override the appropriate values with 'auto' or '0' to display the
8921 * menu correctly. If `menuPosition` is known beforehand, CSS rules corresponding to other positions
8922 * may be omitted.
8923 *
8924 * .oo-ui-menuLayout-menu {
8925 * height: 200px;
8926 * width: 200px;
8927 * }
8928 * .oo-ui-menuLayout-content {
8929 * top: 200px;
8930 * left: 200px;
8931 * right: 200px;
8932 * bottom: 200px;
8933 * }
8934 *
8935 * @class
8936 * @extends OO.ui.Layout
8937 *
8938 * @constructor
8939 * @param {Object} [config] Configuration options
8940 * @cfg {boolean} [showMenu=true] Show menu
8941 * @cfg {string} [menuPosition='before'] Position of menu: `top`, `after`, `bottom` or `before`
8942 */
8943 OO.ui.MenuLayout = function OoUiMenuLayout( config ) {
8944 // Configuration initialization
8945 config = $.extend( {
8946 showMenu: true,
8947 menuPosition: 'before'
8948 }, config );
8949
8950 // Parent constructor
8951 OO.ui.MenuLayout.parent.call( this, config );
8952
8953 /**
8954 * Menu DOM node
8955 *
8956 * @property {jQuery}
8957 */
8958 this.$menu = $( '<div>' );
8959 /**
8960 * Content DOM node
8961 *
8962 * @property {jQuery}
8963 */
8964 this.$content = $( '<div>' );
8965
8966 // Initialization
8967 this.$menu
8968 .addClass( 'oo-ui-menuLayout-menu' );
8969 this.$content.addClass( 'oo-ui-menuLayout-content' );
8970 this.$element
8971 .addClass( 'oo-ui-menuLayout' )
8972 .append( this.$content, this.$menu );
8973 this.setMenuPosition( config.menuPosition );
8974 this.toggleMenu( config.showMenu );
8975 };
8976
8977 /* Setup */
8978
8979 OO.inheritClass( OO.ui.MenuLayout, OO.ui.Layout );
8980
8981 /* Methods */
8982
8983 /**
8984 * Toggle menu.
8985 *
8986 * @param {boolean} showMenu Show menu, omit to toggle
8987 * @chainable
8988 */
8989 OO.ui.MenuLayout.prototype.toggleMenu = function ( showMenu ) {
8990 showMenu = showMenu === undefined ? !this.showMenu : !!showMenu;
8991
8992 if ( this.showMenu !== showMenu ) {
8993 this.showMenu = showMenu;
8994 this.$element
8995 .toggleClass( 'oo-ui-menuLayout-showMenu', this.showMenu )
8996 .toggleClass( 'oo-ui-menuLayout-hideMenu', !this.showMenu );
8997 }
8998
8999 return this;
9000 };
9001
9002 /**
9003 * Check if menu is visible
9004 *
9005 * @return {boolean} Menu is visible
9006 */
9007 OO.ui.MenuLayout.prototype.isMenuVisible = function () {
9008 return this.showMenu;
9009 };
9010
9011 /**
9012 * Set menu position.
9013 *
9014 * @param {string} position Position of menu, either `top`, `after`, `bottom` or `before`
9015 * @throws {Error} If position value is not supported
9016 * @chainable
9017 */
9018 OO.ui.MenuLayout.prototype.setMenuPosition = function ( position ) {
9019 this.$element.removeClass( 'oo-ui-menuLayout-' + this.menuPosition );
9020 this.menuPosition = position;
9021 this.$element.addClass( 'oo-ui-menuLayout-' + position );
9022
9023 return this;
9024 };
9025
9026 /**
9027 * Get menu position.
9028 *
9029 * @return {string} Menu position
9030 */
9031 OO.ui.MenuLayout.prototype.getMenuPosition = function () {
9032 return this.menuPosition;
9033 };
9034
9035 /**
9036 * BookletLayouts contain {@link OO.ui.PageLayout page layouts} as well as
9037 * an {@link OO.ui.OutlineSelectWidget outline} that allows users to easily navigate
9038 * through the pages and select which one to display. By default, only one page is
9039 * displayed at a time and the outline is hidden. When a user navigates to a new page,
9040 * the booklet layout automatically focuses on the first focusable element, unless the
9041 * default setting is changed. Optionally, booklets can be configured to show
9042 * {@link OO.ui.OutlineControlsWidget controls} for adding, moving, and removing items.
9043 *
9044 * @example
9045 * // Example of a BookletLayout that contains two PageLayouts.
9046 *
9047 * function PageOneLayout( name, config ) {
9048 * PageOneLayout.parent.call( this, name, config );
9049 * this.$element.append( '<p>First page</p><p>(This booklet has an outline, displayed on the left)</p>' );
9050 * }
9051 * OO.inheritClass( PageOneLayout, OO.ui.PageLayout );
9052 * PageOneLayout.prototype.setupOutlineItem = function () {
9053 * this.outlineItem.setLabel( 'Page One' );
9054 * };
9055 *
9056 * function PageTwoLayout( name, config ) {
9057 * PageTwoLayout.parent.call( this, name, config );
9058 * this.$element.append( '<p>Second page</p>' );
9059 * }
9060 * OO.inheritClass( PageTwoLayout, OO.ui.PageLayout );
9061 * PageTwoLayout.prototype.setupOutlineItem = function () {
9062 * this.outlineItem.setLabel( 'Page Two' );
9063 * };
9064 *
9065 * var page1 = new PageOneLayout( 'one' ),
9066 * page2 = new PageTwoLayout( 'two' );
9067 *
9068 * var booklet = new OO.ui.BookletLayout( {
9069 * outlined: true
9070 * } );
9071 *
9072 * booklet.addPages ( [ page1, page2 ] );
9073 * $( 'body' ).append( booklet.$element );
9074 *
9075 * @class
9076 * @extends OO.ui.MenuLayout
9077 *
9078 * @constructor
9079 * @param {Object} [config] Configuration options
9080 * @cfg {boolean} [continuous=false] Show all pages, one after another
9081 * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new page is displayed.
9082 * @cfg {boolean} [outlined=false] Show the outline. The outline is used to navigate through the pages of the booklet.
9083 * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
9084 */
9085 OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
9086 // Configuration initialization
9087 config = config || {};
9088
9089 // Parent constructor
9090 OO.ui.BookletLayout.parent.call( this, config );
9091
9092 // Properties
9093 this.currentPageName = null;
9094 this.pages = {};
9095 this.ignoreFocus = false;
9096 this.stackLayout = new OO.ui.StackLayout( { continuous: !!config.continuous } );
9097 this.$content.append( this.stackLayout.$element );
9098 this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
9099 this.outlineVisible = false;
9100 this.outlined = !!config.outlined;
9101 if ( this.outlined ) {
9102 this.editable = !!config.editable;
9103 this.outlineControlsWidget = null;
9104 this.outlineSelectWidget = new OO.ui.OutlineSelectWidget();
9105 this.outlinePanel = new OO.ui.PanelLayout( { scrollable: true } );
9106 this.$menu.append( this.outlinePanel.$element );
9107 this.outlineVisible = true;
9108 if ( this.editable ) {
9109 this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
9110 this.outlineSelectWidget
9111 );
9112 }
9113 }
9114 this.toggleMenu( this.outlined );
9115
9116 // Events
9117 this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
9118 if ( this.outlined ) {
9119 this.outlineSelectWidget.connect( this, { select: 'onOutlineSelectWidgetSelect' } );
9120 }
9121 if ( this.autoFocus ) {
9122 // Event 'focus' does not bubble, but 'focusin' does
9123 this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
9124 }
9125
9126 // Initialization
9127 this.$element.addClass( 'oo-ui-bookletLayout' );
9128 this.stackLayout.$element.addClass( 'oo-ui-bookletLayout-stackLayout' );
9129 if ( this.outlined ) {
9130 this.outlinePanel.$element
9131 .addClass( 'oo-ui-bookletLayout-outlinePanel' )
9132 .append( this.outlineSelectWidget.$element );
9133 if ( this.editable ) {
9134 this.outlinePanel.$element
9135 .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
9136 .append( this.outlineControlsWidget.$element );
9137 }
9138 }
9139 };
9140
9141 /* Setup */
9142
9143 OO.inheritClass( OO.ui.BookletLayout, OO.ui.MenuLayout );
9144
9145 /* Events */
9146
9147 /**
9148 * A 'set' event is emitted when a page is {@link #setPage set} to be displayed by the booklet layout.
9149 * @event set
9150 * @param {OO.ui.PageLayout} page Current page
9151 */
9152
9153 /**
9154 * An 'add' event is emitted when pages are {@link #addPages added} to the booklet layout.
9155 *
9156 * @event add
9157 * @param {OO.ui.PageLayout[]} page Added pages
9158 * @param {number} index Index pages were added at
9159 */
9160
9161 /**
9162 * A 'remove' event is emitted when pages are {@link #clearPages cleared} or
9163 * {@link #removePages removed} from the booklet.
9164 *
9165 * @event remove
9166 * @param {OO.ui.PageLayout[]} pages Removed pages
9167 */
9168
9169 /* Methods */
9170
9171 /**
9172 * Handle stack layout focus.
9173 *
9174 * @private
9175 * @param {jQuery.Event} e Focusin event
9176 */
9177 OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) {
9178 var name, $target;
9179
9180 // Find the page that an element was focused within
9181 $target = $( e.target ).closest( '.oo-ui-pageLayout' );
9182 for ( name in this.pages ) {
9183 // Check for page match, exclude current page to find only page changes
9184 if ( this.pages[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentPageName ) {
9185 this.setPage( name );
9186 break;
9187 }
9188 }
9189 };
9190
9191 /**
9192 * Handle stack layout set events.
9193 *
9194 * @private
9195 * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
9196 */
9197 OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
9198 var layout = this;
9199 if ( page ) {
9200 page.scrollElementIntoView( { complete: function () {
9201 if ( layout.autoFocus ) {
9202 layout.focus();
9203 }
9204 } } );
9205 }
9206 };
9207
9208 /**
9209 * Focus the first input in the current page.
9210 *
9211 * If no page is selected, the first selectable page will be selected.
9212 * If the focus is already in an element on the current page, nothing will happen.
9213 * @param {number} [itemIndex] A specific item to focus on
9214 */
9215 OO.ui.BookletLayout.prototype.focus = function ( itemIndex ) {
9216 var $input, page,
9217 items = this.stackLayout.getItems();
9218
9219 if ( itemIndex !== undefined && items[ itemIndex ] ) {
9220 page = items[ itemIndex ];
9221 } else {
9222 page = this.stackLayout.getCurrentItem();
9223 }
9224
9225 if ( !page && this.outlined ) {
9226 this.selectFirstSelectablePage();
9227 page = this.stackLayout.getCurrentItem();
9228 }
9229 if ( !page ) {
9230 return;
9231 }
9232 // Only change the focus if is not already in the current page
9233 if ( !page.$element.find( ':focus' ).length ) {
9234 $input = page.$element.find( ':input:first' );
9235 if ( $input.length ) {
9236 $input[ 0 ].focus();
9237 }
9238 }
9239 };
9240
9241 /**
9242 * Find the first focusable input in the booklet layout and focus
9243 * on it.
9244 */
9245 OO.ui.BookletLayout.prototype.focusFirstFocusable = function () {
9246 var i, len,
9247 found = false,
9248 items = this.stackLayout.getItems(),
9249 checkAndFocus = function () {
9250 if ( OO.ui.isFocusableElement( $( this ) ) ) {
9251 $( this ).focus();
9252 found = true;
9253 return false;
9254 }
9255 };
9256
9257 for ( i = 0, len = items.length; i < len; i++ ) {
9258 if ( found ) {
9259 break;
9260 }
9261 // Find all potentially focusable elements in the item
9262 // and check if they are focusable
9263 items[i].$element
9264 .find( 'input, select, textarea, button, object' )
9265 /* jshint loopfunc:true */
9266 .each( checkAndFocus );
9267 }
9268 };
9269
9270 /**
9271 * Handle outline widget select events.
9272 *
9273 * @private
9274 * @param {OO.ui.OptionWidget|null} item Selected item
9275 */
9276 OO.ui.BookletLayout.prototype.onOutlineSelectWidgetSelect = function ( item ) {
9277 if ( item ) {
9278 this.setPage( item.getData() );
9279 }
9280 };
9281
9282 /**
9283 * Check if booklet has an outline.
9284 *
9285 * @return {boolean} Booklet has an outline
9286 */
9287 OO.ui.BookletLayout.prototype.isOutlined = function () {
9288 return this.outlined;
9289 };
9290
9291 /**
9292 * Check if booklet has editing controls.
9293 *
9294 * @return {boolean} Booklet is editable
9295 */
9296 OO.ui.BookletLayout.prototype.isEditable = function () {
9297 return this.editable;
9298 };
9299
9300 /**
9301 * Check if booklet has a visible outline.
9302 *
9303 * @return {boolean} Outline is visible
9304 */
9305 OO.ui.BookletLayout.prototype.isOutlineVisible = function () {
9306 return this.outlined && this.outlineVisible;
9307 };
9308
9309 /**
9310 * Hide or show the outline.
9311 *
9312 * @param {boolean} [show] Show outline, omit to invert current state
9313 * @chainable
9314 */
9315 OO.ui.BookletLayout.prototype.toggleOutline = function ( show ) {
9316 if ( this.outlined ) {
9317 show = show === undefined ? !this.outlineVisible : !!show;
9318 this.outlineVisible = show;
9319 this.toggleMenu( show );
9320 }
9321
9322 return this;
9323 };
9324
9325 /**
9326 * Get the page closest to the specified page.
9327 *
9328 * @param {OO.ui.PageLayout} page Page to use as a reference point
9329 * @return {OO.ui.PageLayout|null} Page closest to the specified page
9330 */
9331 OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) {
9332 var next, prev, level,
9333 pages = this.stackLayout.getItems(),
9334 index = $.inArray( page, pages );
9335
9336 if ( index !== -1 ) {
9337 next = pages[ index + 1 ];
9338 prev = pages[ index - 1 ];
9339 // Prefer adjacent pages at the same level
9340 if ( this.outlined ) {
9341 level = this.outlineSelectWidget.getItemFromData( page.getName() ).getLevel();
9342 if (
9343 prev &&
9344 level === this.outlineSelectWidget.getItemFromData( prev.getName() ).getLevel()
9345 ) {
9346 return prev;
9347 }
9348 if (
9349 next &&
9350 level === this.outlineSelectWidget.getItemFromData( next.getName() ).getLevel()
9351 ) {
9352 return next;
9353 }
9354 }
9355 }
9356 return prev || next || null;
9357 };
9358
9359 /**
9360 * Get the outline widget.
9361 *
9362 * If the booklet is not outlined, the method will return `null`.
9363 *
9364 * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if the booklet is not outlined
9365 */
9366 OO.ui.BookletLayout.prototype.getOutline = function () {
9367 return this.outlineSelectWidget;
9368 };
9369
9370 /**
9371 * Get the outline controls widget.
9372 *
9373 * If the outline is not editable, the method will return `null`.
9374 *
9375 * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
9376 */
9377 OO.ui.BookletLayout.prototype.getOutlineControls = function () {
9378 return this.outlineControlsWidget;
9379 };
9380
9381 /**
9382 * Get a page by its symbolic name.
9383 *
9384 * @param {string} name Symbolic name of page
9385 * @return {OO.ui.PageLayout|undefined} Page, if found
9386 */
9387 OO.ui.BookletLayout.prototype.getPage = function ( name ) {
9388 return this.pages[ name ];
9389 };
9390
9391 /**
9392 * Get the current page.
9393 *
9394 * @return {OO.ui.PageLayout|undefined} Current page, if found
9395 */
9396 OO.ui.BookletLayout.prototype.getCurrentPage = function () {
9397 var name = this.getCurrentPageName();
9398 return name ? this.getPage( name ) : undefined;
9399 };
9400
9401 /**
9402 * Get the symbolic name of the current page.
9403 *
9404 * @return {string|null} Symbolic name of the current page
9405 */
9406 OO.ui.BookletLayout.prototype.getCurrentPageName = function () {
9407 return this.currentPageName;
9408 };
9409
9410 /**
9411 * Add pages to the booklet layout
9412 *
9413 * When pages are added with the same names as existing pages, the existing pages will be
9414 * automatically removed before the new pages are added.
9415 *
9416 * @param {OO.ui.PageLayout[]} pages Pages to add
9417 * @param {number} index Index of the insertion point
9418 * @fires add
9419 * @chainable
9420 */
9421 OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) {
9422 var i, len, name, page, item, currentIndex,
9423 stackLayoutPages = this.stackLayout.getItems(),
9424 remove = [],
9425 items = [];
9426
9427 // Remove pages with same names
9428 for ( i = 0, len = pages.length; i < len; i++ ) {
9429 page = pages[ i ];
9430 name = page.getName();
9431
9432 if ( Object.prototype.hasOwnProperty.call( this.pages, name ) ) {
9433 // Correct the insertion index
9434 currentIndex = $.inArray( this.pages[ name ], stackLayoutPages );
9435 if ( currentIndex !== -1 && currentIndex + 1 < index ) {
9436 index--;
9437 }
9438 remove.push( this.pages[ name ] );
9439 }
9440 }
9441 if ( remove.length ) {
9442 this.removePages( remove );
9443 }
9444
9445 // Add new pages
9446 for ( i = 0, len = pages.length; i < len; i++ ) {
9447 page = pages[ i ];
9448 name = page.getName();
9449 this.pages[ page.getName() ] = page;
9450 if ( this.outlined ) {
9451 item = new OO.ui.OutlineOptionWidget( { data: name } );
9452 page.setOutlineItem( item );
9453 items.push( item );
9454 }
9455 }
9456
9457 if ( this.outlined && items.length ) {
9458 this.outlineSelectWidget.addItems( items, index );
9459 this.selectFirstSelectablePage();
9460 }
9461 this.stackLayout.addItems( pages, index );
9462 this.emit( 'add', pages, index );
9463
9464 return this;
9465 };
9466
9467 /**
9468 * Remove the specified pages from the booklet layout.
9469 *
9470 * To remove all pages from the booklet, you may wish to use the #clearPages method instead.
9471 *
9472 * @param {OO.ui.PageLayout[]} pages An array of pages to remove
9473 * @fires remove
9474 * @chainable
9475 */
9476 OO.ui.BookletLayout.prototype.removePages = function ( pages ) {
9477 var i, len, name, page,
9478 items = [];
9479
9480 for ( i = 0, len = pages.length; i < len; i++ ) {
9481 page = pages[ i ];
9482 name = page.getName();
9483 delete this.pages[ name ];
9484 if ( this.outlined ) {
9485 items.push( this.outlineSelectWidget.getItemFromData( name ) );
9486 page.setOutlineItem( null );
9487 }
9488 }
9489 if ( this.outlined && items.length ) {
9490 this.outlineSelectWidget.removeItems( items );
9491 this.selectFirstSelectablePage();
9492 }
9493 this.stackLayout.removeItems( pages );
9494 this.emit( 'remove', pages );
9495
9496 return this;
9497 };
9498
9499 /**
9500 * Clear all pages from the booklet layout.
9501 *
9502 * To remove only a subset of pages from the booklet, use the #removePages method.
9503 *
9504 * @fires remove
9505 * @chainable
9506 */
9507 OO.ui.BookletLayout.prototype.clearPages = function () {
9508 var i, len,
9509 pages = this.stackLayout.getItems();
9510
9511 this.pages = {};
9512 this.currentPageName = null;
9513 if ( this.outlined ) {
9514 this.outlineSelectWidget.clearItems();
9515 for ( i = 0, len = pages.length; i < len; i++ ) {
9516 pages[ i ].setOutlineItem( null );
9517 }
9518 }
9519 this.stackLayout.clearItems();
9520
9521 this.emit( 'remove', pages );
9522
9523 return this;
9524 };
9525
9526 /**
9527 * Set the current page by symbolic name.
9528 *
9529 * @fires set
9530 * @param {string} name Symbolic name of page
9531 */
9532 OO.ui.BookletLayout.prototype.setPage = function ( name ) {
9533 var selectedItem,
9534 $focused,
9535 page = this.pages[ name ];
9536
9537 if ( name !== this.currentPageName ) {
9538 if ( this.outlined ) {
9539 selectedItem = this.outlineSelectWidget.getSelectedItem();
9540 if ( selectedItem && selectedItem.getData() !== name ) {
9541 this.outlineSelectWidget.selectItemByData( name );
9542 }
9543 }
9544 if ( page ) {
9545 if ( this.currentPageName && this.pages[ this.currentPageName ] ) {
9546 this.pages[ this.currentPageName ].setActive( false );
9547 // Blur anything focused if the next page doesn't have anything focusable - this
9548 // is not needed if the next page has something focusable because once it is focused
9549 // this blur happens automatically
9550 if ( this.autoFocus && !page.$element.find( ':input' ).length ) {
9551 $focused = this.pages[ this.currentPageName ].$element.find( ':focus' );
9552 if ( $focused.length ) {
9553 $focused[ 0 ].blur();
9554 }
9555 }
9556 }
9557 this.currentPageName = name;
9558 this.stackLayout.setItem( page );
9559 page.setActive( true );
9560 this.emit( 'set', page );
9561 }
9562 }
9563 };
9564
9565 /**
9566 * Select the first selectable page.
9567 *
9568 * @chainable
9569 */
9570 OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () {
9571 if ( !this.outlineSelectWidget.getSelectedItem() ) {
9572 this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getFirstSelectableItem() );
9573 }
9574
9575 return this;
9576 };
9577
9578 /**
9579 * IndexLayouts contain {@link OO.ui.CardLayout card layouts} as well as
9580 * {@link OO.ui.TabSelectWidget tabs} that allow users to easily navigate through the cards and
9581 * select which one to display. By default, only one card is displayed at a time. When a user
9582 * navigates to a new card, the index layout automatically focuses on the first focusable element,
9583 * unless the default setting is changed.
9584 *
9585 * TODO: This class is similar to BookletLayout, we may want to refactor to reduce duplication
9586 *
9587 * @example
9588 * // Example of a IndexLayout that contains two CardLayouts.
9589 *
9590 * function CardOneLayout( name, config ) {
9591 * CardOneLayout.parent.call( this, name, config );
9592 * this.$element.append( '<p>First card</p>' );
9593 * }
9594 * OO.inheritClass( CardOneLayout, OO.ui.CardLayout );
9595 * CardOneLayout.prototype.setupTabItem = function () {
9596 * this.tabItem.setLabel( 'Card One' );
9597 * };
9598 *
9599 * function CardTwoLayout( name, config ) {
9600 * CardTwoLayout.parent.call( this, name, config );
9601 * this.$element.append( '<p>Second card</p>' );
9602 * }
9603 * OO.inheritClass( CardTwoLayout, OO.ui.CardLayout );
9604 * CardTwoLayout.prototype.setupTabItem = function () {
9605 * this.tabItem.setLabel( 'Card Two' );
9606 * };
9607 *
9608 * var card1 = new CardOneLayout( 'one' ),
9609 * card2 = new CardTwoLayout( 'two' );
9610 *
9611 * var index = new OO.ui.IndexLayout();
9612 *
9613 * index.addCards ( [ card1, card2 ] );
9614 * $( 'body' ).append( index.$element );
9615 *
9616 * @class
9617 * @extends OO.ui.MenuLayout
9618 *
9619 * @constructor
9620 * @param {Object} [config] Configuration options
9621 * @cfg {boolean} [continuous=false] Show all cards, one after another
9622 * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new card is displayed.
9623 */
9624 OO.ui.IndexLayout = function OoUiIndexLayout( config ) {
9625 // Configuration initialization
9626 config = $.extend( {}, config, { menuPosition: 'top' } );
9627
9628 // Parent constructor
9629 OO.ui.IndexLayout.parent.call( this, config );
9630
9631 // Properties
9632 this.currentCardName = null;
9633 this.cards = {};
9634 this.ignoreFocus = false;
9635 this.stackLayout = new OO.ui.StackLayout( { continuous: !!config.continuous } );
9636 this.$content.append( this.stackLayout.$element );
9637 this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
9638
9639 this.tabSelectWidget = new OO.ui.TabSelectWidget();
9640 this.tabPanel = new OO.ui.PanelLayout();
9641 this.$menu.append( this.tabPanel.$element );
9642
9643 this.toggleMenu( true );
9644
9645 // Events
9646 this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
9647 this.tabSelectWidget.connect( this, { select: 'onTabSelectWidgetSelect' } );
9648 if ( this.autoFocus ) {
9649 // Event 'focus' does not bubble, but 'focusin' does
9650 this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
9651 }
9652
9653 // Initialization
9654 this.$element.addClass( 'oo-ui-indexLayout' );
9655 this.stackLayout.$element.addClass( 'oo-ui-indexLayout-stackLayout' );
9656 this.tabPanel.$element
9657 .addClass( 'oo-ui-indexLayout-tabPanel' )
9658 .append( this.tabSelectWidget.$element );
9659 };
9660
9661 /* Setup */
9662
9663 OO.inheritClass( OO.ui.IndexLayout, OO.ui.MenuLayout );
9664
9665 /* Events */
9666
9667 /**
9668 * A 'set' event is emitted when a card is {@link #setCard set} to be displayed by the index layout.
9669 * @event set
9670 * @param {OO.ui.CardLayout} card Current card
9671 */
9672
9673 /**
9674 * An 'add' event is emitted when cards are {@link #addCards added} to the index layout.
9675 *
9676 * @event add
9677 * @param {OO.ui.CardLayout[]} card Added cards
9678 * @param {number} index Index cards were added at
9679 */
9680
9681 /**
9682 * A 'remove' event is emitted when cards are {@link #clearCards cleared} or
9683 * {@link #removeCards removed} from the index.
9684 *
9685 * @event remove
9686 * @param {OO.ui.CardLayout[]} cards Removed cards
9687 */
9688
9689 /* Methods */
9690
9691 /**
9692 * Handle stack layout focus.
9693 *
9694 * @private
9695 * @param {jQuery.Event} e Focusin event
9696 */
9697 OO.ui.IndexLayout.prototype.onStackLayoutFocus = function ( e ) {
9698 var name, $target;
9699
9700 // Find the card that an element was focused within
9701 $target = $( e.target ).closest( '.oo-ui-cardLayout' );
9702 for ( name in this.cards ) {
9703 // Check for card match, exclude current card to find only card changes
9704 if ( this.cards[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentCardName ) {
9705 this.setCard( name );
9706 break;
9707 }
9708 }
9709 };
9710
9711 /**
9712 * Handle stack layout set events.
9713 *
9714 * @private
9715 * @param {OO.ui.PanelLayout|null} card The card panel that is now the current panel
9716 */
9717 OO.ui.IndexLayout.prototype.onStackLayoutSet = function ( card ) {
9718 var layout = this;
9719 if ( card ) {
9720 card.scrollElementIntoView( { complete: function () {
9721 if ( layout.autoFocus ) {
9722 layout.focus();
9723 }
9724 } } );
9725 }
9726 };
9727
9728 /**
9729 * Focus the first input in the current card.
9730 *
9731 * If no card is selected, the first selectable card will be selected.
9732 * If the focus is already in an element on the current card, nothing will happen.
9733 * @param {number} [itemIndex] A specific item to focus on
9734 */
9735 OO.ui.IndexLayout.prototype.focus = function ( itemIndex ) {
9736 var $input, card,
9737 items = this.stackLayout.getItems();
9738
9739 if ( itemIndex !== undefined && items[ itemIndex ] ) {
9740 card = items[ itemIndex ];
9741 } else {
9742 card = this.stackLayout.getCurrentItem();
9743 }
9744
9745 if ( !card ) {
9746 this.selectFirstSelectableCard();
9747 card = this.stackLayout.getCurrentItem();
9748 }
9749 if ( !card ) {
9750 return;
9751 }
9752 // Only change the focus if is not already in the current card
9753 if ( !card.$element.find( ':focus' ).length ) {
9754 $input = card.$element.find( ':input:first' );
9755 if ( $input.length ) {
9756 $input[ 0 ].focus();
9757 }
9758 }
9759 };
9760
9761 /**
9762 * Find the first focusable input in the index layout and focus
9763 * on it.
9764 */
9765 OO.ui.IndexLayout.prototype.focusFirstFocusable = function () {
9766 var i, len,
9767 found = false,
9768 items = this.stackLayout.getItems(),
9769 checkAndFocus = function () {
9770 if ( OO.ui.isFocusableElement( $( this ) ) ) {
9771 $( this ).focus();
9772 found = true;
9773 return false;
9774 }
9775 };
9776
9777 for ( i = 0, len = items.length; i < len; i++ ) {
9778 if ( found ) {
9779 break;
9780 }
9781 // Find all potentially focusable elements in the item
9782 // and check if they are focusable
9783 items[i].$element
9784 .find( 'input, select, textarea, button, object' )
9785 .each( checkAndFocus );
9786 }
9787 };
9788
9789 /**
9790 * Handle tab widget select events.
9791 *
9792 * @private
9793 * @param {OO.ui.OptionWidget|null} item Selected item
9794 */
9795 OO.ui.IndexLayout.prototype.onTabSelectWidgetSelect = function ( item ) {
9796 if ( item ) {
9797 this.setCard( item.getData() );
9798 }
9799 };
9800
9801 /**
9802 * Get the card closest to the specified card.
9803 *
9804 * @param {OO.ui.CardLayout} card Card to use as a reference point
9805 * @return {OO.ui.CardLayout|null} Card closest to the specified card
9806 */
9807 OO.ui.IndexLayout.prototype.getClosestCard = function ( card ) {
9808 var next, prev, level,
9809 cards = this.stackLayout.getItems(),
9810 index = $.inArray( card, cards );
9811
9812 if ( index !== -1 ) {
9813 next = cards[ index + 1 ];
9814 prev = cards[ index - 1 ];
9815 // Prefer adjacent cards at the same level
9816 level = this.tabSelectWidget.getItemFromData( card.getName() ).getLevel();
9817 if (
9818 prev &&
9819 level === this.tabSelectWidget.getItemFromData( prev.getName() ).getLevel()
9820 ) {
9821 return prev;
9822 }
9823 if (
9824 next &&
9825 level === this.tabSelectWidget.getItemFromData( next.getName() ).getLevel()
9826 ) {
9827 return next;
9828 }
9829 }
9830 return prev || next || null;
9831 };
9832
9833 /**
9834 * Get the tabs widget.
9835 *
9836 * @return {OO.ui.TabSelectWidget} Tabs widget
9837 */
9838 OO.ui.IndexLayout.prototype.getTabs = function () {
9839 return this.tabSelectWidget;
9840 };
9841
9842 /**
9843 * Get a card by its symbolic name.
9844 *
9845 * @param {string} name Symbolic name of card
9846 * @return {OO.ui.CardLayout|undefined} Card, if found
9847 */
9848 OO.ui.IndexLayout.prototype.getCard = function ( name ) {
9849 return this.cards[ name ];
9850 };
9851
9852 /**
9853 * Get the current card.
9854 *
9855 * @return {OO.ui.CardLayout|undefined} Current card, if found
9856 */
9857 OO.ui.IndexLayout.prototype.getCurrentCard = function () {
9858 var name = this.getCurrentCardName();
9859 return name ? this.getCard( name ) : undefined;
9860 };
9861
9862 /**
9863 * Get the symbolic name of the current card.
9864 *
9865 * @return {string|null} Symbolic name of the current card
9866 */
9867 OO.ui.IndexLayout.prototype.getCurrentCardName = function () {
9868 return this.currentCardName;
9869 };
9870
9871 /**
9872 * Add cards to the index layout
9873 *
9874 * When cards are added with the same names as existing cards, the existing cards will be
9875 * automatically removed before the new cards are added.
9876 *
9877 * @param {OO.ui.CardLayout[]} cards Cards to add
9878 * @param {number} index Index of the insertion point
9879 * @fires add
9880 * @chainable
9881 */
9882 OO.ui.IndexLayout.prototype.addCards = function ( cards, index ) {
9883 var i, len, name, card, item, currentIndex,
9884 stackLayoutCards = this.stackLayout.getItems(),
9885 remove = [],
9886 items = [];
9887
9888 // Remove cards with same names
9889 for ( i = 0, len = cards.length; i < len; i++ ) {
9890 card = cards[ i ];
9891 name = card.getName();
9892
9893 if ( Object.prototype.hasOwnProperty.call( this.cards, name ) ) {
9894 // Correct the insertion index
9895 currentIndex = $.inArray( this.cards[ name ], stackLayoutCards );
9896 if ( currentIndex !== -1 && currentIndex + 1 < index ) {
9897 index--;
9898 }
9899 remove.push( this.cards[ name ] );
9900 }
9901 }
9902 if ( remove.length ) {
9903 this.removeCards( remove );
9904 }
9905
9906 // Add new cards
9907 for ( i = 0, len = cards.length; i < len; i++ ) {
9908 card = cards[ i ];
9909 name = card.getName();
9910 this.cards[ card.getName() ] = card;
9911 item = new OO.ui.TabOptionWidget( { data: name } );
9912 card.setTabItem( item );
9913 items.push( item );
9914 }
9915
9916 if ( items.length ) {
9917 this.tabSelectWidget.addItems( items, index );
9918 this.selectFirstSelectableCard();
9919 }
9920 this.stackLayout.addItems( cards, index );
9921 this.emit( 'add', cards, index );
9922
9923 return this;
9924 };
9925
9926 /**
9927 * Remove the specified cards from the index layout.
9928 *
9929 * To remove all cards from the index, you may wish to use the #clearCards method instead.
9930 *
9931 * @param {OO.ui.CardLayout[]} cards An array of cards to remove
9932 * @fires remove
9933 * @chainable
9934 */
9935 OO.ui.IndexLayout.prototype.removeCards = function ( cards ) {
9936 var i, len, name, card,
9937 items = [];
9938
9939 for ( i = 0, len = cards.length; i < len; i++ ) {
9940 card = cards[ i ];
9941 name = card.getName();
9942 delete this.cards[ name ];
9943 items.push( this.tabSelectWidget.getItemFromData( name ) );
9944 card.setTabItem( null );
9945 }
9946 if ( items.length ) {
9947 this.tabSelectWidget.removeItems( items );
9948 this.selectFirstSelectableCard();
9949 }
9950 this.stackLayout.removeItems( cards );
9951 this.emit( 'remove', cards );
9952
9953 return this;
9954 };
9955
9956 /**
9957 * Clear all cards from the index layout.
9958 *
9959 * To remove only a subset of cards from the index, use the #removeCards method.
9960 *
9961 * @fires remove
9962 * @chainable
9963 */
9964 OO.ui.IndexLayout.prototype.clearCards = function () {
9965 var i, len,
9966 cards = this.stackLayout.getItems();
9967
9968 this.cards = {};
9969 this.currentCardName = null;
9970 this.tabSelectWidget.clearItems();
9971 for ( i = 0, len = cards.length; i < len; i++ ) {
9972 cards[ i ].setTabItem( null );
9973 }
9974 this.stackLayout.clearItems();
9975
9976 this.emit( 'remove', cards );
9977
9978 return this;
9979 };
9980
9981 /**
9982 * Set the current card by symbolic name.
9983 *
9984 * @fires set
9985 * @param {string} name Symbolic name of card
9986 */
9987 OO.ui.IndexLayout.prototype.setCard = function ( name ) {
9988 var selectedItem,
9989 $focused,
9990 card = this.cards[ name ];
9991
9992 if ( name !== this.currentCardName ) {
9993 selectedItem = this.tabSelectWidget.getSelectedItem();
9994 if ( selectedItem && selectedItem.getData() !== name ) {
9995 this.tabSelectWidget.selectItemByData( name );
9996 }
9997 if ( card ) {
9998 if ( this.currentCardName && this.cards[ this.currentCardName ] ) {
9999 this.cards[ this.currentCardName ].setActive( false );
10000 // Blur anything focused if the next card doesn't have anything focusable - this
10001 // is not needed if the next card has something focusable because once it is focused
10002 // this blur happens automatically
10003 if ( this.autoFocus && !card.$element.find( ':input' ).length ) {
10004 $focused = this.cards[ this.currentCardName ].$element.find( ':focus' );
10005 if ( $focused.length ) {
10006 $focused[ 0 ].blur();
10007 }
10008 }
10009 }
10010 this.currentCardName = name;
10011 this.stackLayout.setItem( card );
10012 card.setActive( true );
10013 this.emit( 'set', card );
10014 }
10015 }
10016 };
10017
10018 /**
10019 * Select the first selectable card.
10020 *
10021 * @chainable
10022 */
10023 OO.ui.IndexLayout.prototype.selectFirstSelectableCard = function () {
10024 if ( !this.tabSelectWidget.getSelectedItem() ) {
10025 this.tabSelectWidget.selectItem( this.tabSelectWidget.getFirstSelectableItem() );
10026 }
10027
10028 return this;
10029 };
10030
10031 /**
10032 * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
10033 * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
10034 *
10035 * @example
10036 * // Example of a panel layout
10037 * var panel = new OO.ui.PanelLayout( {
10038 * expanded: false,
10039 * framed: true,
10040 * padded: true,
10041 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
10042 * } );
10043 * $( 'body' ).append( panel.$element );
10044 *
10045 * @class
10046 * @extends OO.ui.Layout
10047 *
10048 * @constructor
10049 * @param {Object} [config] Configuration options
10050 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
10051 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
10052 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
10053 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
10054 */
10055 OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
10056 // Configuration initialization
10057 config = $.extend( {
10058 scrollable: false,
10059 padded: false,
10060 expanded: true,
10061 framed: false
10062 }, config );
10063
10064 // Parent constructor
10065 OO.ui.PanelLayout.parent.call( this, config );
10066
10067 // Initialization
10068 this.$element.addClass( 'oo-ui-panelLayout' );
10069 if ( config.scrollable ) {
10070 this.$element.addClass( 'oo-ui-panelLayout-scrollable' );
10071 }
10072 if ( config.padded ) {
10073 this.$element.addClass( 'oo-ui-panelLayout-padded' );
10074 }
10075 if ( config.expanded ) {
10076 this.$element.addClass( 'oo-ui-panelLayout-expanded' );
10077 }
10078 if ( config.framed ) {
10079 this.$element.addClass( 'oo-ui-panelLayout-framed' );
10080 }
10081 };
10082
10083 /* Setup */
10084
10085 OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
10086
10087 /**
10088 * CardLayouts are used within {@link OO.ui.IndexLayout index layouts} to create cards that users can select and display
10089 * from the index's optional {@link OO.ui.TabSelectWidget tab} navigation. Cards are usually not instantiated directly,
10090 * rather extended to include the required content and functionality.
10091 *
10092 * Each card must have a unique symbolic name, which is passed to the constructor. In addition, the card's tab
10093 * item is customized (with a label) using the #setupTabItem method. See
10094 * {@link OO.ui.IndexLayout IndexLayout} for an example.
10095 *
10096 * @class
10097 * @extends OO.ui.PanelLayout
10098 *
10099 * @constructor
10100 * @param {string} name Unique symbolic name of card
10101 * @param {Object} [config] Configuration options
10102 */
10103 OO.ui.CardLayout = function OoUiCardLayout( name, config ) {
10104 // Allow passing positional parameters inside the config object
10105 if ( OO.isPlainObject( name ) && config === undefined ) {
10106 config = name;
10107 name = config.name;
10108 }
10109
10110 // Configuration initialization
10111 config = $.extend( { scrollable: true }, config );
10112
10113 // Parent constructor
10114 OO.ui.CardLayout.parent.call( this, config );
10115
10116 // Properties
10117 this.name = name;
10118 this.tabItem = null;
10119 this.active = false;
10120
10121 // Initialization
10122 this.$element.addClass( 'oo-ui-cardLayout' );
10123 };
10124
10125 /* Setup */
10126
10127 OO.inheritClass( OO.ui.CardLayout, OO.ui.PanelLayout );
10128
10129 /* Events */
10130
10131 /**
10132 * An 'active' event is emitted when the card becomes active. Cards become active when they are
10133 * shown in a index layout that is configured to display only one card at a time.
10134 *
10135 * @event active
10136 * @param {boolean} active Card is active
10137 */
10138
10139 /* Methods */
10140
10141 /**
10142 * Get the symbolic name of the card.
10143 *
10144 * @return {string} Symbolic name of card
10145 */
10146 OO.ui.CardLayout.prototype.getName = function () {
10147 return this.name;
10148 };
10149
10150 /**
10151 * Check if card is active.
10152 *
10153 * Cards become active when they are shown in a {@link OO.ui.IndexLayout index layout} that is configured to display
10154 * only one card at a time. Additional CSS is applied to the card's tab item to reflect the active state.
10155 *
10156 * @return {boolean} Card is active
10157 */
10158 OO.ui.CardLayout.prototype.isActive = function () {
10159 return this.active;
10160 };
10161
10162 /**
10163 * Get tab item.
10164 *
10165 * The tab item allows users to access the card from the index's tab
10166 * navigation. The tab item itself can be customized (with a label, level, etc.) using the #setupTabItem method.
10167 *
10168 * @return {OO.ui.TabOptionWidget|null} Tab option widget
10169 */
10170 OO.ui.CardLayout.prototype.getTabItem = function () {
10171 return this.tabItem;
10172 };
10173
10174 /**
10175 * Set or unset the tab item.
10176 *
10177 * Specify a {@link OO.ui.TabOptionWidget tab option} to set it,
10178 * or `null` to clear the tab item. To customize the tab item itself (e.g., to set a label or tab
10179 * level), use #setupTabItem instead of this method.
10180 *
10181 * @param {OO.ui.TabOptionWidget|null} tabItem Tab option widget, null to clear
10182 * @chainable
10183 */
10184 OO.ui.CardLayout.prototype.setTabItem = function ( tabItem ) {
10185 this.tabItem = tabItem || null;
10186 if ( tabItem ) {
10187 this.setupTabItem();
10188 }
10189 return this;
10190 };
10191
10192 /**
10193 * Set up the tab item.
10194 *
10195 * Use this method to customize the tab item (e.g., to add a label or tab level). To set or unset
10196 * the tab item itself (with a {@link OO.ui.TabOptionWidget tab option} or `null`), use
10197 * the #setTabItem method instead.
10198 *
10199 * @param {OO.ui.TabOptionWidget} tabItem Tab option widget to set up
10200 * @chainable
10201 */
10202 OO.ui.CardLayout.prototype.setupTabItem = function () {
10203 return this;
10204 };
10205
10206 /**
10207 * Set the card to its 'active' state.
10208 *
10209 * Cards become active when they are shown in a index layout that is configured to display only one card at a time. Additional
10210 * CSS is applied to the tab item to reflect the card's active state. Outside of the index
10211 * context, setting the active state on a card does nothing.
10212 *
10213 * @param {boolean} value Card is active
10214 * @fires active
10215 */
10216 OO.ui.CardLayout.prototype.setActive = function ( active ) {
10217 active = !!active;
10218
10219 if ( active !== this.active ) {
10220 this.active = active;
10221 this.$element.toggleClass( 'oo-ui-cardLayout-active', this.active );
10222 this.emit( 'active', this.active );
10223 }
10224 };
10225
10226 /**
10227 * PageLayouts are used within {@link OO.ui.BookletLayout booklet layouts} to create pages that users can select and display
10228 * from the booklet's optional {@link OO.ui.OutlineSelectWidget outline} navigation. Pages are usually not instantiated directly,
10229 * rather extended to include the required content and functionality.
10230 *
10231 * Each page must have a unique symbolic name, which is passed to the constructor. In addition, the page's outline
10232 * item is customized (with a label, outline level, etc.) using the #setupOutlineItem method. See
10233 * {@link OO.ui.BookletLayout BookletLayout} for an example.
10234 *
10235 * @class
10236 * @extends OO.ui.PanelLayout
10237 *
10238 * @constructor
10239 * @param {string} name Unique symbolic name of page
10240 * @param {Object} [config] Configuration options
10241 */
10242 OO.ui.PageLayout = function OoUiPageLayout( name, config ) {
10243 // Allow passing positional parameters inside the config object
10244 if ( OO.isPlainObject( name ) && config === undefined ) {
10245 config = name;
10246 name = config.name;
10247 }
10248
10249 // Configuration initialization
10250 config = $.extend( { scrollable: true }, config );
10251
10252 // Parent constructor
10253 OO.ui.PageLayout.parent.call( this, config );
10254
10255 // Properties
10256 this.name = name;
10257 this.outlineItem = null;
10258 this.active = false;
10259
10260 // Initialization
10261 this.$element.addClass( 'oo-ui-pageLayout' );
10262 };
10263
10264 /* Setup */
10265
10266 OO.inheritClass( OO.ui.PageLayout, OO.ui.PanelLayout );
10267
10268 /* Events */
10269
10270 /**
10271 * An 'active' event is emitted when the page becomes active. Pages become active when they are
10272 * shown in a booklet layout that is configured to display only one page at a time.
10273 *
10274 * @event active
10275 * @param {boolean} active Page is active
10276 */
10277
10278 /* Methods */
10279
10280 /**
10281 * Get the symbolic name of the page.
10282 *
10283 * @return {string} Symbolic name of page
10284 */
10285 OO.ui.PageLayout.prototype.getName = function () {
10286 return this.name;
10287 };
10288
10289 /**
10290 * Check if page is active.
10291 *
10292 * Pages become active when they are shown in a {@link OO.ui.BookletLayout booklet layout} that is configured to display
10293 * only one page at a time. Additional CSS is applied to the page's outline item to reflect the active state.
10294 *
10295 * @return {boolean} Page is active
10296 */
10297 OO.ui.PageLayout.prototype.isActive = function () {
10298 return this.active;
10299 };
10300
10301 /**
10302 * Get outline item.
10303 *
10304 * The outline item allows users to access the page from the booklet's outline
10305 * navigation. The outline item itself can be customized (with a label, level, etc.) using the #setupOutlineItem method.
10306 *
10307 * @return {OO.ui.OutlineOptionWidget|null} Outline option widget
10308 */
10309 OO.ui.PageLayout.prototype.getOutlineItem = function () {
10310 return this.outlineItem;
10311 };
10312
10313 /**
10314 * Set or unset the outline item.
10315 *
10316 * Specify an {@link OO.ui.OutlineOptionWidget outline option} to set it,
10317 * or `null` to clear the outline item. To customize the outline item itself (e.g., to set a label or outline
10318 * level), use #setupOutlineItem instead of this method.
10319 *
10320 * @param {OO.ui.OutlineOptionWidget|null} outlineItem Outline option widget, null to clear
10321 * @chainable
10322 */
10323 OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) {
10324 this.outlineItem = outlineItem || null;
10325 if ( outlineItem ) {
10326 this.setupOutlineItem();
10327 }
10328 return this;
10329 };
10330
10331 /**
10332 * Set up the outline item.
10333 *
10334 * Use this method to customize the outline item (e.g., to add a label or outline level). To set or unset
10335 * the outline item itself (with an {@link OO.ui.OutlineOptionWidget outline option} or `null`), use
10336 * the #setOutlineItem method instead.
10337 *
10338 * @param {OO.ui.OutlineOptionWidget} outlineItem Outline option widget to set up
10339 * @chainable
10340 */
10341 OO.ui.PageLayout.prototype.setupOutlineItem = function () {
10342 return this;
10343 };
10344
10345 /**
10346 * Set the page to its 'active' state.
10347 *
10348 * Pages become active when they are shown in a booklet layout that is configured to display only one page at a time. Additional
10349 * CSS is applied to the outline item to reflect the page's active state. Outside of the booklet
10350 * context, setting the active state on a page does nothing.
10351 *
10352 * @param {boolean} value Page is active
10353 * @fires active
10354 */
10355 OO.ui.PageLayout.prototype.setActive = function ( active ) {
10356 active = !!active;
10357
10358 if ( active !== this.active ) {
10359 this.active = active;
10360 this.$element.toggleClass( 'oo-ui-pageLayout-active', active );
10361 this.emit( 'active', this.active );
10362 }
10363 };
10364
10365 /**
10366 * StackLayouts contain a series of {@link OO.ui.PanelLayout panel layouts}. By default, only one panel is displayed
10367 * at a time, though the stack layout can also be configured to show all contained panels, one after another,
10368 * by setting the #continuous option to 'true'.
10369 *
10370 * @example
10371 * // A stack layout with two panels, configured to be displayed continously
10372 * var myStack = new OO.ui.StackLayout( {
10373 * items: [
10374 * new OO.ui.PanelLayout( {
10375 * $content: $( '<p>Panel One</p>' ),
10376 * padded: true,
10377 * framed: true
10378 * } ),
10379 * new OO.ui.PanelLayout( {
10380 * $content: $( '<p>Panel Two</p>' ),
10381 * padded: true,
10382 * framed: true
10383 * } )
10384 * ],
10385 * continuous: true
10386 * } );
10387 * $( 'body' ).append( myStack.$element );
10388 *
10389 * @class
10390 * @extends OO.ui.PanelLayout
10391 * @mixins OO.ui.mixin.GroupElement
10392 *
10393 * @constructor
10394 * @param {Object} [config] Configuration options
10395 * @cfg {boolean} [continuous=false] Show all panels, one after another. By default, only one panel is displayed at a time.
10396 * @cfg {OO.ui.Layout[]} [items] Panel layouts to add to the stack layout.
10397 */
10398 OO.ui.StackLayout = function OoUiStackLayout( config ) {
10399 // Configuration initialization
10400 config = $.extend( { scrollable: true }, config );
10401
10402 // Parent constructor
10403 OO.ui.StackLayout.parent.call( this, config );
10404
10405 // Mixin constructors
10406 OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
10407
10408 // Properties
10409 this.currentItem = null;
10410 this.continuous = !!config.continuous;
10411
10412 // Initialization
10413 this.$element.addClass( 'oo-ui-stackLayout' );
10414 if ( this.continuous ) {
10415 this.$element.addClass( 'oo-ui-stackLayout-continuous' );
10416 }
10417 if ( Array.isArray( config.items ) ) {
10418 this.addItems( config.items );
10419 }
10420 };
10421
10422 /* Setup */
10423
10424 OO.inheritClass( OO.ui.StackLayout, OO.ui.PanelLayout );
10425 OO.mixinClass( OO.ui.StackLayout, OO.ui.mixin.GroupElement );
10426
10427 /* Events */
10428
10429 /**
10430 * A 'set' event is emitted when panels are {@link #addItems added}, {@link #removeItems removed},
10431 * {@link #clearItems cleared} or {@link #setItem displayed}.
10432 *
10433 * @event set
10434 * @param {OO.ui.Layout|null} item Current panel or `null` if no panel is shown
10435 */
10436
10437 /* Methods */
10438
10439 /**
10440 * Get the current panel.
10441 *
10442 * @return {OO.ui.Layout|null}
10443 */
10444 OO.ui.StackLayout.prototype.getCurrentItem = function () {
10445 return this.currentItem;
10446 };
10447
10448 /**
10449 * Unset the current item.
10450 *
10451 * @private
10452 * @param {OO.ui.StackLayout} layout
10453 * @fires set
10454 */
10455 OO.ui.StackLayout.prototype.unsetCurrentItem = function () {
10456 var prevItem = this.currentItem;
10457 if ( prevItem === null ) {
10458 return;
10459 }
10460
10461 this.currentItem = null;
10462 this.emit( 'set', null );
10463 };
10464
10465 /**
10466 * Add panel layouts to the stack layout.
10467 *
10468 * Panels will be added to the end of the stack layout array unless the optional index parameter specifies a different
10469 * insertion point. Adding a panel that is already in the stack will move it to the end of the array or the point specified
10470 * by the index.
10471 *
10472 * @param {OO.ui.Layout[]} items Panels to add
10473 * @param {number} [index] Index of the insertion point
10474 * @chainable
10475 */
10476 OO.ui.StackLayout.prototype.addItems = function ( items, index ) {
10477 // Update the visibility
10478 this.updateHiddenState( items, this.currentItem );
10479
10480 // Mixin method
10481 OO.ui.mixin.GroupElement.prototype.addItems.call( this, items, index );
10482
10483 if ( !this.currentItem && items.length ) {
10484 this.setItem( items[ 0 ] );
10485 }
10486
10487 return this;
10488 };
10489
10490 /**
10491 * Remove the specified panels from the stack layout.
10492 *
10493 * Removed panels are detached from the DOM, not removed, so that they may be reused. To remove all panels,
10494 * you may wish to use the #clearItems method instead.
10495 *
10496 * @param {OO.ui.Layout[]} items Panels to remove
10497 * @chainable
10498 * @fires set
10499 */
10500 OO.ui.StackLayout.prototype.removeItems = function ( items ) {
10501 // Mixin method
10502 OO.ui.mixin.GroupElement.prototype.removeItems.call( this, items );
10503
10504 if ( $.inArray( this.currentItem, items ) !== -1 ) {
10505 if ( this.items.length ) {
10506 this.setItem( this.items[ 0 ] );
10507 } else {
10508 this.unsetCurrentItem();
10509 }
10510 }
10511
10512 return this;
10513 };
10514
10515 /**
10516 * Clear all panels from the stack layout.
10517 *
10518 * Cleared panels are detached from the DOM, not removed, so that they may be reused. To remove only
10519 * a subset of panels, use the #removeItems method.
10520 *
10521 * @chainable
10522 * @fires set
10523 */
10524 OO.ui.StackLayout.prototype.clearItems = function () {
10525 this.unsetCurrentItem();
10526 OO.ui.mixin.GroupElement.prototype.clearItems.call( this );
10527
10528 return this;
10529 };
10530
10531 /**
10532 * Show the specified panel.
10533 *
10534 * If another panel is currently displayed, it will be hidden.
10535 *
10536 * @param {OO.ui.Layout} item Panel to show
10537 * @chainable
10538 * @fires set
10539 */
10540 OO.ui.StackLayout.prototype.setItem = function ( item ) {
10541 if ( item !== this.currentItem ) {
10542 this.updateHiddenState( this.items, item );
10543
10544 if ( $.inArray( item, this.items ) !== -1 ) {
10545 this.currentItem = item;
10546 this.emit( 'set', item );
10547 } else {
10548 this.unsetCurrentItem();
10549 }
10550 }
10551
10552 return this;
10553 };
10554
10555 /**
10556 * Update the visibility of all items in case of non-continuous view.
10557 *
10558 * Ensure all items are hidden except for the selected one.
10559 * This method does nothing when the stack is continuous.
10560 *
10561 * @private
10562 * @param {OO.ui.Layout[]} items Item list iterate over
10563 * @param {OO.ui.Layout} [selectedItem] Selected item to show
10564 */
10565 OO.ui.StackLayout.prototype.updateHiddenState = function ( items, selectedItem ) {
10566 var i, len;
10567
10568 if ( !this.continuous ) {
10569 for ( i = 0, len = items.length; i < len; i++ ) {
10570 if ( !selectedItem || selectedItem !== items[ i ] ) {
10571 items[ i ].$element.addClass( 'oo-ui-element-hidden' );
10572 }
10573 }
10574 if ( selectedItem ) {
10575 selectedItem.$element.removeClass( 'oo-ui-element-hidden' );
10576 }
10577 }
10578 };
10579
10580 /**
10581 * BarToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
10582 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.MenuToolGroup MenuToolGroup}
10583 * and {@link OO.ui.ListToolGroup ListToolGroup}). The {@link OO.ui.Tool tools} in a BarToolGroup are
10584 * displayed by icon in a single row. The title of the tool is displayed when users move the mouse over
10585 * the tool.
10586 *
10587 * BarToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar is
10588 * set up.
10589 *
10590 * @example
10591 * // Example of a BarToolGroup with two tools
10592 * var toolFactory = new OO.ui.ToolFactory();
10593 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
10594 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
10595 *
10596 * // We will be placing status text in this element when tools are used
10597 * var $area = $( '<p>' ).text( 'Example of a BarToolGroup with two tools.' );
10598 *
10599 * // Define the tools that we're going to place in our toolbar
10600 *
10601 * // Create a class inheriting from OO.ui.Tool
10602 * function PictureTool() {
10603 * PictureTool.parent.apply( this, arguments );
10604 * }
10605 * OO.inheritClass( PictureTool, OO.ui.Tool );
10606 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
10607 * // of 'icon' and 'title' (displayed icon and text).
10608 * PictureTool.static.name = 'picture';
10609 * PictureTool.static.icon = 'picture';
10610 * PictureTool.static.title = 'Insert picture';
10611 * // Defines the action that will happen when this tool is selected (clicked).
10612 * PictureTool.prototype.onSelect = function () {
10613 * $area.text( 'Picture tool clicked!' );
10614 * // Never display this tool as "active" (selected).
10615 * this.setActive( false );
10616 * };
10617 * // Make this tool available in our toolFactory and thus our toolbar
10618 * toolFactory.register( PictureTool );
10619 *
10620 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
10621 * // little popup window (a PopupWidget).
10622 * function HelpTool( toolGroup, config ) {
10623 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
10624 * padded: true,
10625 * label: 'Help',
10626 * head: true
10627 * } }, config ) );
10628 * this.popup.$body.append( '<p>I am helpful!</p>' );
10629 * }
10630 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
10631 * HelpTool.static.name = 'help';
10632 * HelpTool.static.icon = 'help';
10633 * HelpTool.static.title = 'Help';
10634 * toolFactory.register( HelpTool );
10635 *
10636 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
10637 * // used once (but not all defined tools must be used).
10638 * toolbar.setup( [
10639 * {
10640 * // 'bar' tool groups display tools by icon only
10641 * type: 'bar',
10642 * include: [ 'picture', 'help' ]
10643 * }
10644 * ] );
10645 *
10646 * // Create some UI around the toolbar and place it in the document
10647 * var frame = new OO.ui.PanelLayout( {
10648 * expanded: false,
10649 * framed: true
10650 * } );
10651 * var contentFrame = new OO.ui.PanelLayout( {
10652 * expanded: false,
10653 * padded: true
10654 * } );
10655 * frame.$element.append(
10656 * toolbar.$element,
10657 * contentFrame.$element.append( $area )
10658 * );
10659 * $( 'body' ).append( frame.$element );
10660 *
10661 * // Here is where the toolbar is actually built. This must be done after inserting it into the
10662 * // document.
10663 * toolbar.initialize();
10664 *
10665 * For more information about how to add tools to a bar tool group, please see {@link OO.ui.ToolGroup toolgroup}.
10666 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
10667 *
10668 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
10669 *
10670 * @class
10671 * @extends OO.ui.ToolGroup
10672 *
10673 * @constructor
10674 * @param {OO.ui.Toolbar} toolbar
10675 * @param {Object} [config] Configuration options
10676 */
10677 OO.ui.BarToolGroup = function OoUiBarToolGroup( toolbar, config ) {
10678 // Allow passing positional parameters inside the config object
10679 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
10680 config = toolbar;
10681 toolbar = config.toolbar;
10682 }
10683
10684 // Parent constructor
10685 OO.ui.BarToolGroup.parent.call( this, toolbar, config );
10686
10687 // Initialization
10688 this.$element.addClass( 'oo-ui-barToolGroup' );
10689 };
10690
10691 /* Setup */
10692
10693 OO.inheritClass( OO.ui.BarToolGroup, OO.ui.ToolGroup );
10694
10695 /* Static Properties */
10696
10697 OO.ui.BarToolGroup.static.titleTooltips = true;
10698
10699 OO.ui.BarToolGroup.static.accelTooltips = true;
10700
10701 OO.ui.BarToolGroup.static.name = 'bar';
10702
10703 /**
10704 * PopupToolGroup is an abstract base class used by both {@link OO.ui.MenuToolGroup MenuToolGroup}
10705 * and {@link OO.ui.ListToolGroup ListToolGroup} to provide a popup--an overlaid menu or list of tools with an
10706 * optional icon and label. This class can be used for other base classes that also use this functionality.
10707 *
10708 * @abstract
10709 * @class
10710 * @extends OO.ui.ToolGroup
10711 * @mixins OO.ui.mixin.IconElement
10712 * @mixins OO.ui.mixin.IndicatorElement
10713 * @mixins OO.ui.mixin.LabelElement
10714 * @mixins OO.ui.mixin.TitledElement
10715 * @mixins OO.ui.mixin.ClippableElement
10716 * @mixins OO.ui.mixin.TabIndexedElement
10717 *
10718 * @constructor
10719 * @param {OO.ui.Toolbar} toolbar
10720 * @param {Object} [config] Configuration options
10721 * @cfg {string} [header] Text to display at the top of the popup
10722 */
10723 OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
10724 // Allow passing positional parameters inside the config object
10725 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
10726 config = toolbar;
10727 toolbar = config.toolbar;
10728 }
10729
10730 // Configuration initialization
10731 config = config || {};
10732
10733 // Parent constructor
10734 OO.ui.PopupToolGroup.parent.call( this, toolbar, config );
10735
10736 // Properties
10737 this.active = false;
10738 this.dragging = false;
10739 this.onBlurHandler = this.onBlur.bind( this );
10740 this.$handle = $( '<span>' );
10741
10742 // Mixin constructors
10743 OO.ui.mixin.IconElement.call( this, config );
10744 OO.ui.mixin.IndicatorElement.call( this, config );
10745 OO.ui.mixin.LabelElement.call( this, config );
10746 OO.ui.mixin.TitledElement.call( this, config );
10747 OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
10748 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
10749
10750 // Events
10751 this.$handle.on( {
10752 keydown: this.onHandleMouseKeyDown.bind( this ),
10753 keyup: this.onHandleMouseKeyUp.bind( this ),
10754 mousedown: this.onHandleMouseKeyDown.bind( this ),
10755 mouseup: this.onHandleMouseKeyUp.bind( this )
10756 } );
10757
10758 // Initialization
10759 this.$handle
10760 .addClass( 'oo-ui-popupToolGroup-handle' )
10761 .append( this.$icon, this.$label, this.$indicator );
10762 // If the pop-up should have a header, add it to the top of the toolGroup.
10763 // Note: If this feature is useful for other widgets, we could abstract it into an
10764 // OO.ui.HeaderedElement mixin constructor.
10765 if ( config.header !== undefined ) {
10766 this.$group
10767 .prepend( $( '<span>' )
10768 .addClass( 'oo-ui-popupToolGroup-header' )
10769 .text( config.header )
10770 );
10771 }
10772 this.$element
10773 .addClass( 'oo-ui-popupToolGroup' )
10774 .prepend( this.$handle );
10775 };
10776
10777 /* Setup */
10778
10779 OO.inheritClass( OO.ui.PopupToolGroup, OO.ui.ToolGroup );
10780 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.IconElement );
10781 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.IndicatorElement );
10782 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.LabelElement );
10783 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.TitledElement );
10784 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.ClippableElement );
10785 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.TabIndexedElement );
10786
10787 /* Methods */
10788
10789 /**
10790 * @inheritdoc
10791 */
10792 OO.ui.PopupToolGroup.prototype.setDisabled = function () {
10793 // Parent method
10794 OO.ui.PopupToolGroup.parent.prototype.setDisabled.apply( this, arguments );
10795
10796 if ( this.isDisabled() && this.isElementAttached() ) {
10797 this.setActive( false );
10798 }
10799 };
10800
10801 /**
10802 * Handle focus being lost.
10803 *
10804 * The event is actually generated from a mouseup/keyup, so it is not a normal blur event object.
10805 *
10806 * @protected
10807 * @param {jQuery.Event} e Mouse up or key up event
10808 */
10809 OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) {
10810 // Only deactivate when clicking outside the dropdown element
10811 if ( $( e.target ).closest( '.oo-ui-popupToolGroup' )[ 0 ] !== this.$element[ 0 ] ) {
10812 this.setActive( false );
10813 }
10814 };
10815
10816 /**
10817 * @inheritdoc
10818 */
10819 OO.ui.PopupToolGroup.prototype.onMouseKeyUp = function ( e ) {
10820 // Only close toolgroup when a tool was actually selected
10821 if (
10822 !this.isDisabled() && this.pressed && this.pressed === this.getTargetTool( e ) &&
10823 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
10824 ) {
10825 this.setActive( false );
10826 }
10827 return OO.ui.PopupToolGroup.parent.prototype.onMouseKeyUp.call( this, e );
10828 };
10829
10830 /**
10831 * Handle mouse up and key up events.
10832 *
10833 * @protected
10834 * @param {jQuery.Event} e Mouse up or key up event
10835 */
10836 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyUp = function ( e ) {
10837 if (
10838 !this.isDisabled() &&
10839 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
10840 ) {
10841 return false;
10842 }
10843 };
10844
10845 /**
10846 * Handle mouse down and key down events.
10847 *
10848 * @protected
10849 * @param {jQuery.Event} e Mouse down or key down event
10850 */
10851 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyDown = function ( e ) {
10852 if (
10853 !this.isDisabled() &&
10854 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
10855 ) {
10856 this.setActive( !this.active );
10857 return false;
10858 }
10859 };
10860
10861 /**
10862 * Switch into 'active' mode.
10863 *
10864 * When active, the popup is visible. A mouseup event anywhere in the document will trigger
10865 * deactivation.
10866 */
10867 OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
10868 value = !!value;
10869 if ( this.active !== value ) {
10870 this.active = value;
10871 if ( value ) {
10872 this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler, true );
10873 this.getElementDocument().addEventListener( 'keyup', this.onBlurHandler, true );
10874
10875 // Try anchoring the popup to the left first
10876 this.$element.addClass( 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left' );
10877 this.toggleClipping( true );
10878 if ( this.isClippedHorizontally() ) {
10879 // Anchoring to the left caused the popup to clip, so anchor it to the right instead
10880 this.toggleClipping( false );
10881 this.$element
10882 .removeClass( 'oo-ui-popupToolGroup-left' )
10883 .addClass( 'oo-ui-popupToolGroup-right' );
10884 this.toggleClipping( true );
10885 }
10886 } else {
10887 this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler, true );
10888 this.getElementDocument().removeEventListener( 'keyup', this.onBlurHandler, true );
10889 this.$element.removeClass(
10890 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left oo-ui-popupToolGroup-right'
10891 );
10892 this.toggleClipping( false );
10893 }
10894 }
10895 };
10896
10897 /**
10898 * ListToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
10899 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.MenuToolGroup MenuToolGroup}
10900 * and {@link OO.ui.BarToolGroup BarToolGroup}). The {@link OO.ui.Tool tools} in a ListToolGroup are displayed
10901 * by label in a dropdown menu. The title of the tool is used as the label text. The menu itself can be configured
10902 * with a label, icon, indicator, header, and title.
10903 *
10904 * ListToolGroups can be configured to be expanded and collapsed. Collapsed lists will have a ‘More’ option that
10905 * users can select to see the full list of tools. If a collapsed toolgroup is expanded, a ‘Fewer’ option permits
10906 * users to collapse the list again.
10907 *
10908 * ListToolGroups are created by a {@link OO.ui.ToolGroupFactory toolgroup factory} when the toolbar is set up. The factory
10909 * requires the ListToolGroup's symbolic name, 'list', which is specified along with the other configurations. For more
10910 * information about how to add tools to a ListToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
10911 *
10912 * @example
10913 * // Example of a ListToolGroup
10914 * var toolFactory = new OO.ui.ToolFactory();
10915 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
10916 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
10917 *
10918 * // Configure and register two tools
10919 * function SettingsTool() {
10920 * SettingsTool.parent.apply( this, arguments );
10921 * }
10922 * OO.inheritClass( SettingsTool, OO.ui.Tool );
10923 * SettingsTool.static.name = 'settings';
10924 * SettingsTool.static.icon = 'settings';
10925 * SettingsTool.static.title = 'Change settings';
10926 * SettingsTool.prototype.onSelect = function () {
10927 * this.setActive( false );
10928 * };
10929 * toolFactory.register( SettingsTool );
10930 * // Register two more tools, nothing interesting here
10931 * function StuffTool() {
10932 * StuffTool.parent.apply( this, arguments );
10933 * }
10934 * OO.inheritClass( StuffTool, OO.ui.Tool );
10935 * StuffTool.static.name = 'stuff';
10936 * StuffTool.static.icon = 'ellipsis';
10937 * StuffTool.static.title = 'Change the world';
10938 * StuffTool.prototype.onSelect = function () {
10939 * this.setActive( false );
10940 * };
10941 * toolFactory.register( StuffTool );
10942 * toolbar.setup( [
10943 * {
10944 * // Configurations for list toolgroup.
10945 * type: 'list',
10946 * label: 'ListToolGroup',
10947 * indicator: 'down',
10948 * icon: 'picture',
10949 * title: 'This is the title, displayed when user moves the mouse over the list toolgroup',
10950 * header: 'This is the header',
10951 * include: [ 'settings', 'stuff' ],
10952 * allowCollapse: ['stuff']
10953 * }
10954 * ] );
10955 *
10956 * // Create some UI around the toolbar and place it in the document
10957 * var frame = new OO.ui.PanelLayout( {
10958 * expanded: false,
10959 * framed: true
10960 * } );
10961 * frame.$element.append(
10962 * toolbar.$element
10963 * );
10964 * $( 'body' ).append( frame.$element );
10965 * // Build the toolbar. This must be done after the toolbar has been appended to the document.
10966 * toolbar.initialize();
10967 *
10968 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
10969 *
10970 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
10971 *
10972 * @class
10973 * @extends OO.ui.PopupToolGroup
10974 *
10975 * @constructor
10976 * @param {OO.ui.Toolbar} toolbar
10977 * @param {Object} [config] Configuration options
10978 * @cfg {Array} [allowCollapse] Allow the specified tools to be collapsed. By default, collapsible tools
10979 * will only be displayed if users click the ‘More’ option displayed at the bottom of the list. If
10980 * the list is expanded, a ‘Fewer’ option permits users to collapse the list again. Any tools that
10981 * are included in the toolgroup, but are not designated as collapsible, will always be displayed.
10982 * To open a collapsible list in its expanded state, set #expanded to 'true'.
10983 * @cfg {Array} [forceExpand] Expand the specified tools. All other tools will be designated as collapsible.
10984 * Unless #expanded is set to true, the collapsible tools will be collapsed when the list is first opened.
10985 * @cfg {boolean} [expanded=false] Expand collapsible tools. This config is only relevant if tools have
10986 * been designated as collapsible. When expanded is set to true, all tools in the group will be displayed
10987 * when the list is first opened. Users can collapse the list with a ‘Fewer’ option at the bottom.
10988 */
10989 OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
10990 // Allow passing positional parameters inside the config object
10991 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
10992 config = toolbar;
10993 toolbar = config.toolbar;
10994 }
10995
10996 // Configuration initialization
10997 config = config || {};
10998
10999 // Properties (must be set before parent constructor, which calls #populate)
11000 this.allowCollapse = config.allowCollapse;
11001 this.forceExpand = config.forceExpand;
11002 this.expanded = config.expanded !== undefined ? config.expanded : false;
11003 this.collapsibleTools = [];
11004
11005 // Parent constructor
11006 OO.ui.ListToolGroup.parent.call( this, toolbar, config );
11007
11008 // Initialization
11009 this.$element.addClass( 'oo-ui-listToolGroup' );
11010 };
11011
11012 /* Setup */
11013
11014 OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup );
11015
11016 /* Static Properties */
11017
11018 OO.ui.ListToolGroup.static.name = 'list';
11019
11020 /* Methods */
11021
11022 /**
11023 * @inheritdoc
11024 */
11025 OO.ui.ListToolGroup.prototype.populate = function () {
11026 var i, len, allowCollapse = [];
11027
11028 OO.ui.ListToolGroup.parent.prototype.populate.call( this );
11029
11030 // Update the list of collapsible tools
11031 if ( this.allowCollapse !== undefined ) {
11032 allowCollapse = this.allowCollapse;
11033 } else if ( this.forceExpand !== undefined ) {
11034 allowCollapse = OO.simpleArrayDifference( Object.keys( this.tools ), this.forceExpand );
11035 }
11036
11037 this.collapsibleTools = [];
11038 for ( i = 0, len = allowCollapse.length; i < len; i++ ) {
11039 if ( this.tools[ allowCollapse[ i ] ] !== undefined ) {
11040 this.collapsibleTools.push( this.tools[ allowCollapse[ i ] ] );
11041 }
11042 }
11043
11044 // Keep at the end, even when tools are added
11045 this.$group.append( this.getExpandCollapseTool().$element );
11046
11047 this.getExpandCollapseTool().toggle( this.collapsibleTools.length !== 0 );
11048 this.updateCollapsibleState();
11049 };
11050
11051 OO.ui.ListToolGroup.prototype.getExpandCollapseTool = function () {
11052 if ( this.expandCollapseTool === undefined ) {
11053 var ExpandCollapseTool = function () {
11054 ExpandCollapseTool.parent.apply( this, arguments );
11055 };
11056
11057 OO.inheritClass( ExpandCollapseTool, OO.ui.Tool );
11058
11059 ExpandCollapseTool.prototype.onSelect = function () {
11060 this.toolGroup.expanded = !this.toolGroup.expanded;
11061 this.toolGroup.updateCollapsibleState();
11062 this.setActive( false );
11063 };
11064 ExpandCollapseTool.prototype.onUpdateState = function () {
11065 // Do nothing. Tool interface requires an implementation of this function.
11066 };
11067
11068 ExpandCollapseTool.static.name = 'more-fewer';
11069
11070 this.expandCollapseTool = new ExpandCollapseTool( this );
11071 }
11072 return this.expandCollapseTool;
11073 };
11074
11075 /**
11076 * @inheritdoc
11077 */
11078 OO.ui.ListToolGroup.prototype.onMouseKeyUp = function ( e ) {
11079 // Do not close the popup when the user wants to show more/fewer tools
11080 if (
11081 $( e.target ).closest( '.oo-ui-tool-name-more-fewer' ).length &&
11082 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
11083 ) {
11084 // HACK: Prevent the popup list from being hidden. Skip the PopupToolGroup implementation (which
11085 // hides the popup list when a tool is selected) and call ToolGroup's implementation directly.
11086 return OO.ui.ListToolGroup.parent.parent.prototype.onMouseKeyUp.call( this, e );
11087 } else {
11088 return OO.ui.ListToolGroup.parent.prototype.onMouseKeyUp.call( this, e );
11089 }
11090 };
11091
11092 OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () {
11093 var i, len;
11094
11095 this.getExpandCollapseTool()
11096 .setIcon( this.expanded ? 'collapse' : 'expand' )
11097 .setTitle( OO.ui.msg( this.expanded ? 'ooui-toolgroup-collapse' : 'ooui-toolgroup-expand' ) );
11098
11099 for ( i = 0, len = this.collapsibleTools.length; i < len; i++ ) {
11100 this.collapsibleTools[ i ].toggle( this.expanded );
11101 }
11102 };
11103
11104 /**
11105 * MenuToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
11106 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.BarToolGroup BarToolGroup}
11107 * and {@link OO.ui.ListToolGroup ListToolGroup}). MenuToolGroups contain selectable {@link OO.ui.Tool tools},
11108 * which are displayed by label in a dropdown menu. The tool's title is used as the label text, and the
11109 * menu label is updated to reflect which tool or tools are currently selected. If no tools are selected,
11110 * the menu label is empty. The menu can be configured with an indicator, icon, title, and/or header.
11111 *
11112 * MenuToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar
11113 * is set up. Note that all tools must define an {@link OO.ui.Tool#onUpdateState onUpdateState} method if
11114 * a MenuToolGroup is used.
11115 *
11116 * @example
11117 * // Example of a MenuToolGroup
11118 * var toolFactory = new OO.ui.ToolFactory();
11119 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
11120 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
11121 *
11122 * // We will be placing status text in this element when tools are used
11123 * var $area = $( '<p>' ).text( 'An example of a MenuToolGroup. Select a tool from the dropdown menu.' );
11124 *
11125 * // Define the tools that we're going to place in our toolbar
11126 *
11127 * function SettingsTool() {
11128 * SettingsTool.parent.apply( this, arguments );
11129 * this.reallyActive = false;
11130 * }
11131 * OO.inheritClass( SettingsTool, OO.ui.Tool );
11132 * SettingsTool.static.name = 'settings';
11133 * SettingsTool.static.icon = 'settings';
11134 * SettingsTool.static.title = 'Change settings';
11135 * SettingsTool.prototype.onSelect = function () {
11136 * $area.text( 'Settings tool clicked!' );
11137 * // Toggle the active state on each click
11138 * this.reallyActive = !this.reallyActive;
11139 * this.setActive( this.reallyActive );
11140 * // To update the menu label
11141 * this.toolbar.emit( 'updateState' );
11142 * };
11143 * SettingsTool.prototype.onUpdateState = function () {
11144 * };
11145 * toolFactory.register( SettingsTool );
11146 *
11147 * function StuffTool() {
11148 * StuffTool.parent.apply( this, arguments );
11149 * this.reallyActive = false;
11150 * }
11151 * OO.inheritClass( StuffTool, OO.ui.Tool );
11152 * StuffTool.static.name = 'stuff';
11153 * StuffTool.static.icon = 'ellipsis';
11154 * StuffTool.static.title = 'More stuff';
11155 * StuffTool.prototype.onSelect = function () {
11156 * $area.text( 'More stuff tool clicked!' );
11157 * // Toggle the active state on each click
11158 * this.reallyActive = !this.reallyActive;
11159 * this.setActive( this.reallyActive );
11160 * // To update the menu label
11161 * this.toolbar.emit( 'updateState' );
11162 * };
11163 * StuffTool.prototype.onUpdateState = function () {
11164 * };
11165 * toolFactory.register( StuffTool );
11166 *
11167 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
11168 * // used once (but not all defined tools must be used).
11169 * toolbar.setup( [
11170 * {
11171 * type: 'menu',
11172 * header: 'This is the (optional) header',
11173 * title: 'This is the (optional) title',
11174 * indicator: 'down',
11175 * include: [ 'settings', 'stuff' ]
11176 * }
11177 * ] );
11178 *
11179 * // Create some UI around the toolbar and place it in the document
11180 * var frame = new OO.ui.PanelLayout( {
11181 * expanded: false,
11182 * framed: true
11183 * } );
11184 * var contentFrame = new OO.ui.PanelLayout( {
11185 * expanded: false,
11186 * padded: true
11187 * } );
11188 * frame.$element.append(
11189 * toolbar.$element,
11190 * contentFrame.$element.append( $area )
11191 * );
11192 * $( 'body' ).append( frame.$element );
11193 *
11194 * // Here is where the toolbar is actually built. This must be done after inserting it into the
11195 * // document.
11196 * toolbar.initialize();
11197 * toolbar.emit( 'updateState' );
11198 *
11199 * For more information about how to add tools to a MenuToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
11200 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki] [1].
11201 *
11202 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
11203 *
11204 * @class
11205 * @extends OO.ui.PopupToolGroup
11206 *
11207 * @constructor
11208 * @param {OO.ui.Toolbar} toolbar
11209 * @param {Object} [config] Configuration options
11210 */
11211 OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
11212 // Allow passing positional parameters inside the config object
11213 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
11214 config = toolbar;
11215 toolbar = config.toolbar;
11216 }
11217
11218 // Configuration initialization
11219 config = config || {};
11220
11221 // Parent constructor
11222 OO.ui.MenuToolGroup.parent.call( this, toolbar, config );
11223
11224 // Events
11225 this.toolbar.connect( this, { updateState: 'onUpdateState' } );
11226
11227 // Initialization
11228 this.$element.addClass( 'oo-ui-menuToolGroup' );
11229 };
11230
11231 /* Setup */
11232
11233 OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup );
11234
11235 /* Static Properties */
11236
11237 OO.ui.MenuToolGroup.static.name = 'menu';
11238
11239 /* Methods */
11240
11241 /**
11242 * Handle the toolbar state being updated.
11243 *
11244 * When the state changes, the title of each active item in the menu will be joined together and
11245 * used as a label for the group. The label will be empty if none of the items are active.
11246 *
11247 * @private
11248 */
11249 OO.ui.MenuToolGroup.prototype.onUpdateState = function () {
11250 var name,
11251 labelTexts = [];
11252
11253 for ( name in this.tools ) {
11254 if ( this.tools[ name ].isActive() ) {
11255 labelTexts.push( this.tools[ name ].getTitle() );
11256 }
11257 }
11258
11259 this.setLabel( labelTexts.join( ', ' ) || ' ' );
11260 };
11261
11262 /**
11263 * Popup tools open a popup window when they are selected from the {@link OO.ui.Toolbar toolbar}. Each popup tool is configured
11264 * 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
11265 * an #onSelect or #onUpdateState method, as these methods have been implemented already.
11266 *
11267 * // Example of a popup tool. When selected, a popup tool displays
11268 * // a popup window.
11269 * function HelpTool( toolGroup, config ) {
11270 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
11271 * padded: true,
11272 * label: 'Help',
11273 * head: true
11274 * } }, config ) );
11275 * this.popup.$body.append( '<p>I am helpful!</p>' );
11276 * };
11277 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
11278 * HelpTool.static.name = 'help';
11279 * HelpTool.static.icon = 'help';
11280 * HelpTool.static.title = 'Help';
11281 * toolFactory.register( HelpTool );
11282 *
11283 * For an example of a toolbar that contains a popup tool, see {@link OO.ui.Toolbar toolbars}. For more information about
11284 * toolbars in genreral, please see the [OOjs UI documentation on MediaWiki][1].
11285 *
11286 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
11287 *
11288 * @abstract
11289 * @class
11290 * @extends OO.ui.Tool
11291 * @mixins OO.ui.mixin.PopupElement
11292 *
11293 * @constructor
11294 * @param {OO.ui.ToolGroup} toolGroup
11295 * @param {Object} [config] Configuration options
11296 */
11297 OO.ui.PopupTool = function OoUiPopupTool( toolGroup, config ) {
11298 // Allow passing positional parameters inside the config object
11299 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
11300 config = toolGroup;
11301 toolGroup = config.toolGroup;
11302 }
11303
11304 // Parent constructor
11305 OO.ui.PopupTool.parent.call( this, toolGroup, config );
11306
11307 // Mixin constructors
11308 OO.ui.mixin.PopupElement.call( this, config );
11309
11310 // Initialization
11311 this.$element
11312 .addClass( 'oo-ui-popupTool' )
11313 .append( this.popup.$element );
11314 };
11315
11316 /* Setup */
11317
11318 OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool );
11319 OO.mixinClass( OO.ui.PopupTool, OO.ui.mixin.PopupElement );
11320
11321 /* Methods */
11322
11323 /**
11324 * Handle the tool being selected.
11325 *
11326 * @inheritdoc
11327 */
11328 OO.ui.PopupTool.prototype.onSelect = function () {
11329 if ( !this.isDisabled() ) {
11330 this.popup.toggle();
11331 }
11332 this.setActive( false );
11333 return false;
11334 };
11335
11336 /**
11337 * Handle the toolbar state being updated.
11338 *
11339 * @inheritdoc
11340 */
11341 OO.ui.PopupTool.prototype.onUpdateState = function () {
11342 this.setActive( false );
11343 };
11344
11345 /**
11346 * A ToolGroupTool is a special sort of tool that can contain other {@link OO.ui.Tool tools}
11347 * and {@link OO.ui.ToolGroup toolgroups}. The ToolGroupTool was specifically designed to be used
11348 * inside a {@link OO.ui.BarToolGroup bar} toolgroup to provide access to additional tools from
11349 * the bar item. Included tools will be displayed in a dropdown {@link OO.ui.ListToolGroup list}
11350 * when the ToolGroupTool is selected.
11351 *
11352 * // Example: ToolGroupTool with two nested tools, 'setting1' and 'setting2', defined elsewhere.
11353 *
11354 * function SettingsTool() {
11355 * SettingsTool.parent.apply( this, arguments );
11356 * };
11357 * OO.inheritClass( SettingsTool, OO.ui.ToolGroupTool );
11358 * SettingsTool.static.name = 'settings';
11359 * SettingsTool.static.title = 'Change settings';
11360 * SettingsTool.static.groupConfig = {
11361 * icon: 'settings',
11362 * label: 'ToolGroupTool',
11363 * include: [ 'setting1', 'setting2' ]
11364 * };
11365 * toolFactory.register( SettingsTool );
11366 *
11367 * For more information, please see the [OOjs UI documentation on MediaWiki][1].
11368 *
11369 * Please note that this implementation is subject to change per [T74159] [2].
11370 *
11371 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars#ToolGroupTool
11372 * [2]: https://phabricator.wikimedia.org/T74159
11373 *
11374 * @abstract
11375 * @class
11376 * @extends OO.ui.Tool
11377 *
11378 * @constructor
11379 * @param {OO.ui.ToolGroup} toolGroup
11380 * @param {Object} [config] Configuration options
11381 */
11382 OO.ui.ToolGroupTool = function OoUiToolGroupTool( toolGroup, config ) {
11383 // Allow passing positional parameters inside the config object
11384 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
11385 config = toolGroup;
11386 toolGroup = config.toolGroup;
11387 }
11388
11389 // Parent constructor
11390 OO.ui.ToolGroupTool.parent.call( this, toolGroup, config );
11391
11392 // Properties
11393 this.innerToolGroup = this.createGroup( this.constructor.static.groupConfig );
11394
11395 // Events
11396 this.innerToolGroup.connect( this, { disable: 'onToolGroupDisable' } );
11397
11398 // Initialization
11399 this.$link.remove();
11400 this.$element
11401 .addClass( 'oo-ui-toolGroupTool' )
11402 .append( this.innerToolGroup.$element );
11403 };
11404
11405 /* Setup */
11406
11407 OO.inheritClass( OO.ui.ToolGroupTool, OO.ui.Tool );
11408
11409 /* Static Properties */
11410
11411 /**
11412 * Toolgroup configuration.
11413 *
11414 * The toolgroup configuration consists of the tools to include, as well as an icon and label
11415 * to use for the bar item. Tools can be included by symbolic name, group, or with the
11416 * wildcard selector. Please see {@link OO.ui.ToolGroup toolgroup} for more information.
11417 *
11418 * @property {Object.<string,Array>}
11419 */
11420 OO.ui.ToolGroupTool.static.groupConfig = {};
11421
11422 /* Methods */
11423
11424 /**
11425 * Handle the tool being selected.
11426 *
11427 * @inheritdoc
11428 */
11429 OO.ui.ToolGroupTool.prototype.onSelect = function () {
11430 this.innerToolGroup.setActive( !this.innerToolGroup.active );
11431 return false;
11432 };
11433
11434 /**
11435 * Synchronize disabledness state of the tool with the inner toolgroup.
11436 *
11437 * @private
11438 * @param {boolean} disabled Element is disabled
11439 */
11440 OO.ui.ToolGroupTool.prototype.onToolGroupDisable = function ( disabled ) {
11441 this.setDisabled( disabled );
11442 };
11443
11444 /**
11445 * Handle the toolbar state being updated.
11446 *
11447 * @inheritdoc
11448 */
11449 OO.ui.ToolGroupTool.prototype.onUpdateState = function () {
11450 this.setActive( false );
11451 };
11452
11453 /**
11454 * Build a {@link OO.ui.ToolGroup toolgroup} from the specified configuration.
11455 *
11456 * @param {Object.<string,Array>} group Toolgroup configuration. Please see {@link OO.ui.ToolGroup toolgroup} for
11457 * more information.
11458 * @return {OO.ui.ListToolGroup}
11459 */
11460 OO.ui.ToolGroupTool.prototype.createGroup = function ( group ) {
11461 if ( group.include === '*' ) {
11462 // Apply defaults to catch-all groups
11463 if ( group.label === undefined ) {
11464 group.label = OO.ui.msg( 'ooui-toolbar-more' );
11465 }
11466 }
11467
11468 return this.toolbar.getToolGroupFactory().create( 'list', this.toolbar, group );
11469 };
11470
11471 /**
11472 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
11473 *
11474 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
11475 *
11476 * @private
11477 * @abstract
11478 * @class
11479 * @extends OO.ui.mixin.GroupElement
11480 *
11481 * @constructor
11482 * @param {Object} [config] Configuration options
11483 */
11484 OO.ui.mixin.GroupWidget = function OoUiMixinGroupWidget( config ) {
11485 // Parent constructor
11486 OO.ui.mixin.GroupWidget.parent.call( this, config );
11487 };
11488
11489 /* Setup */
11490
11491 OO.inheritClass( OO.ui.mixin.GroupWidget, OO.ui.mixin.GroupElement );
11492
11493 /* Methods */
11494
11495 /**
11496 * Set the disabled state of the widget.
11497 *
11498 * This will also update the disabled state of child widgets.
11499 *
11500 * @param {boolean} disabled Disable widget
11501 * @chainable
11502 */
11503 OO.ui.mixin.GroupWidget.prototype.setDisabled = function ( disabled ) {
11504 var i, len;
11505
11506 // Parent method
11507 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
11508 OO.ui.Widget.prototype.setDisabled.call( this, disabled );
11509
11510 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
11511 if ( this.items ) {
11512 for ( i = 0, len = this.items.length; i < len; i++ ) {
11513 this.items[ i ].updateDisabled();
11514 }
11515 }
11516
11517 return this;
11518 };
11519
11520 /**
11521 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
11522 *
11523 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
11524 * allows bidirectional communication.
11525 *
11526 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
11527 *
11528 * @private
11529 * @abstract
11530 * @class
11531 *
11532 * @constructor
11533 */
11534 OO.ui.mixin.ItemWidget = function OoUiMixinItemWidget() {
11535 //
11536 };
11537
11538 /* Methods */
11539
11540 /**
11541 * Check if widget is disabled.
11542 *
11543 * Checks parent if present, making disabled state inheritable.
11544 *
11545 * @return {boolean} Widget is disabled
11546 */
11547 OO.ui.mixin.ItemWidget.prototype.isDisabled = function () {
11548 return this.disabled ||
11549 ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() );
11550 };
11551
11552 /**
11553 * Set group element is in.
11554 *
11555 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
11556 * @chainable
11557 */
11558 OO.ui.mixin.ItemWidget.prototype.setElementGroup = function ( group ) {
11559 // Parent method
11560 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
11561 OO.ui.Element.prototype.setElementGroup.call( this, group );
11562
11563 // Initialize item disabled states
11564 this.updateDisabled();
11565
11566 return this;
11567 };
11568
11569 /**
11570 * OutlineControlsWidget is a set of controls for an {@link OO.ui.OutlineSelectWidget outline select widget}.
11571 * Controls include moving items up and down, removing items, and adding different kinds of items.
11572 *
11573 * **Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.**
11574 *
11575 * @class
11576 * @extends OO.ui.Widget
11577 * @mixins OO.ui.mixin.GroupElement
11578 * @mixins OO.ui.mixin.IconElement
11579 *
11580 * @constructor
11581 * @param {OO.ui.OutlineSelectWidget} outline Outline to control
11582 * @param {Object} [config] Configuration options
11583 * @cfg {Object} [abilities] List of abilties
11584 * @cfg {boolean} [abilities.move=true] Allow moving movable items
11585 * @cfg {boolean} [abilities.remove=true] Allow removing removable items
11586 */
11587 OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
11588 // Allow passing positional parameters inside the config object
11589 if ( OO.isPlainObject( outline ) && config === undefined ) {
11590 config = outline;
11591 outline = config.outline;
11592 }
11593
11594 // Configuration initialization
11595 config = $.extend( { icon: 'add' }, config );
11596
11597 // Parent constructor
11598 OO.ui.OutlineControlsWidget.parent.call( this, config );
11599
11600 // Mixin constructors
11601 OO.ui.mixin.GroupElement.call( this, config );
11602 OO.ui.mixin.IconElement.call( this, config );
11603
11604 // Properties
11605 this.outline = outline;
11606 this.$movers = $( '<div>' );
11607 this.upButton = new OO.ui.ButtonWidget( {
11608 framed: false,
11609 icon: 'collapse',
11610 title: OO.ui.msg( 'ooui-outline-control-move-up' )
11611 } );
11612 this.downButton = new OO.ui.ButtonWidget( {
11613 framed: false,
11614 icon: 'expand',
11615 title: OO.ui.msg( 'ooui-outline-control-move-down' )
11616 } );
11617 this.removeButton = new OO.ui.ButtonWidget( {
11618 framed: false,
11619 icon: 'remove',
11620 title: OO.ui.msg( 'ooui-outline-control-remove' )
11621 } );
11622 this.abilities = { move: true, remove: true };
11623
11624 // Events
11625 outline.connect( this, {
11626 select: 'onOutlineChange',
11627 add: 'onOutlineChange',
11628 remove: 'onOutlineChange'
11629 } );
11630 this.upButton.connect( this, { click: [ 'emit', 'move', -1 ] } );
11631 this.downButton.connect( this, { click: [ 'emit', 'move', 1 ] } );
11632 this.removeButton.connect( this, { click: [ 'emit', 'remove' ] } );
11633
11634 // Initialization
11635 this.$element.addClass( 'oo-ui-outlineControlsWidget' );
11636 this.$group.addClass( 'oo-ui-outlineControlsWidget-items' );
11637 this.$movers
11638 .addClass( 'oo-ui-outlineControlsWidget-movers' )
11639 .append( this.removeButton.$element, this.upButton.$element, this.downButton.$element );
11640 this.$element.append( this.$icon, this.$group, this.$movers );
11641 this.setAbilities( config.abilities || {} );
11642 };
11643
11644 /* Setup */
11645
11646 OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget );
11647 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.mixin.GroupElement );
11648 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.mixin.IconElement );
11649
11650 /* Events */
11651
11652 /**
11653 * @event move
11654 * @param {number} places Number of places to move
11655 */
11656
11657 /**
11658 * @event remove
11659 */
11660
11661 /* Methods */
11662
11663 /**
11664 * Set abilities.
11665 *
11666 * @param {Object} abilities List of abilties
11667 * @param {boolean} [abilities.move] Allow moving movable items
11668 * @param {boolean} [abilities.remove] Allow removing removable items
11669 */
11670 OO.ui.OutlineControlsWidget.prototype.setAbilities = function ( abilities ) {
11671 var ability;
11672
11673 for ( ability in this.abilities ) {
11674 if ( abilities[ability] !== undefined ) {
11675 this.abilities[ability] = !!abilities[ability];
11676 }
11677 }
11678
11679 this.onOutlineChange();
11680 };
11681
11682 /**
11683 *
11684 * @private
11685 * Handle outline change events.
11686 */
11687 OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () {
11688 var i, len, firstMovable, lastMovable,
11689 items = this.outline.getItems(),
11690 selectedItem = this.outline.getSelectedItem(),
11691 movable = this.abilities.move && selectedItem && selectedItem.isMovable(),
11692 removable = this.abilities.remove && selectedItem && selectedItem.isRemovable();
11693
11694 if ( movable ) {
11695 i = -1;
11696 len = items.length;
11697 while ( ++i < len ) {
11698 if ( items[ i ].isMovable() ) {
11699 firstMovable = items[ i ];
11700 break;
11701 }
11702 }
11703 i = len;
11704 while ( i-- ) {
11705 if ( items[ i ].isMovable() ) {
11706 lastMovable = items[ i ];
11707 break;
11708 }
11709 }
11710 }
11711 this.upButton.setDisabled( !movable || selectedItem === firstMovable );
11712 this.downButton.setDisabled( !movable || selectedItem === lastMovable );
11713 this.removeButton.setDisabled( !removable );
11714 };
11715
11716 /**
11717 * ToggleWidget implements basic behavior of widgets with an on/off state.
11718 * Please see OO.ui.ToggleButtonWidget and OO.ui.ToggleSwitchWidget for examples.
11719 *
11720 * @abstract
11721 * @class
11722 * @extends OO.ui.Widget
11723 *
11724 * @constructor
11725 * @param {Object} [config] Configuration options
11726 * @cfg {boolean} [value=false] The toggle’s initial on/off state.
11727 * By default, the toggle is in the 'off' state.
11728 */
11729 OO.ui.ToggleWidget = function OoUiToggleWidget( config ) {
11730 // Configuration initialization
11731 config = config || {};
11732
11733 // Parent constructor
11734 OO.ui.ToggleWidget.parent.call( this, config );
11735
11736 // Properties
11737 this.value = null;
11738
11739 // Initialization
11740 this.$element.addClass( 'oo-ui-toggleWidget' );
11741 this.setValue( !!config.value );
11742 };
11743
11744 /* Setup */
11745
11746 OO.inheritClass( OO.ui.ToggleWidget, OO.ui.Widget );
11747
11748 /* Events */
11749
11750 /**
11751 * @event change
11752 *
11753 * A change event is emitted when the on/off state of the toggle changes.
11754 *
11755 * @param {boolean} value Value representing the new state of the toggle
11756 */
11757
11758 /* Methods */
11759
11760 /**
11761 * Get the value representing the toggle’s state.
11762 *
11763 * @return {boolean} The on/off state of the toggle
11764 */
11765 OO.ui.ToggleWidget.prototype.getValue = function () {
11766 return this.value;
11767 };
11768
11769 /**
11770 * Set the state of the toggle: `true` for 'on', `false' for 'off'.
11771 *
11772 * @param {boolean} value The state of the toggle
11773 * @fires change
11774 * @chainable
11775 */
11776 OO.ui.ToggleWidget.prototype.setValue = function ( value ) {
11777 value = !!value;
11778 if ( this.value !== value ) {
11779 this.value = value;
11780 this.emit( 'change', value );
11781 this.$element.toggleClass( 'oo-ui-toggleWidget-on', value );
11782 this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value );
11783 this.$element.attr( 'aria-checked', value.toString() );
11784 }
11785 return this;
11786 };
11787
11788 /**
11789 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
11790 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
11791 * removed, and cleared from the group.
11792 *
11793 * @example
11794 * // Example: A ButtonGroupWidget with two buttons
11795 * var button1 = new OO.ui.PopupButtonWidget( {
11796 * label: 'Select a category',
11797 * icon: 'menu',
11798 * popup: {
11799 * $content: $( '<p>List of categories...</p>' ),
11800 * padded: true,
11801 * align: 'left'
11802 * }
11803 * } );
11804 * var button2 = new OO.ui.ButtonWidget( {
11805 * label: 'Add item'
11806 * });
11807 * var buttonGroup = new OO.ui.ButtonGroupWidget( {
11808 * items: [button1, button2]
11809 * } );
11810 * $( 'body' ).append( buttonGroup.$element );
11811 *
11812 * @class
11813 * @extends OO.ui.Widget
11814 * @mixins OO.ui.mixin.GroupElement
11815 *
11816 * @constructor
11817 * @param {Object} [config] Configuration options
11818 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
11819 */
11820 OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
11821 // Configuration initialization
11822 config = config || {};
11823
11824 // Parent constructor
11825 OO.ui.ButtonGroupWidget.parent.call( this, config );
11826
11827 // Mixin constructors
11828 OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
11829
11830 // Initialization
11831 this.$element.addClass( 'oo-ui-buttonGroupWidget' );
11832 if ( Array.isArray( config.items ) ) {
11833 this.addItems( config.items );
11834 }
11835 };
11836
11837 /* Setup */
11838
11839 OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget );
11840 OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.mixin.GroupElement );
11841
11842 /**
11843 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
11844 * feels, and functionality can be customized via the class’s configuration options
11845 * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
11846 * and examples.
11847 *
11848 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
11849 *
11850 * @example
11851 * // A button widget
11852 * var button = new OO.ui.ButtonWidget( {
11853 * label: 'Button with Icon',
11854 * icon: 'remove',
11855 * iconTitle: 'Remove'
11856 * } );
11857 * $( 'body' ).append( button.$element );
11858 *
11859 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
11860 *
11861 * @class
11862 * @extends OO.ui.Widget
11863 * @mixins OO.ui.mixin.ButtonElement
11864 * @mixins OO.ui.mixin.IconElement
11865 * @mixins OO.ui.mixin.IndicatorElement
11866 * @mixins OO.ui.mixin.LabelElement
11867 * @mixins OO.ui.mixin.TitledElement
11868 * @mixins OO.ui.mixin.FlaggedElement
11869 * @mixins OO.ui.mixin.TabIndexedElement
11870 *
11871 * @constructor
11872 * @param {Object} [config] Configuration options
11873 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
11874 * @cfg {string} [target] The frame or window in which to open the hyperlink.
11875 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
11876 */
11877 OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
11878 // Configuration initialization
11879 config = config || {};
11880
11881 // Parent constructor
11882 OO.ui.ButtonWidget.parent.call( this, config );
11883
11884 // Mixin constructors
11885 OO.ui.mixin.ButtonElement.call( this, config );
11886 OO.ui.mixin.IconElement.call( this, config );
11887 OO.ui.mixin.IndicatorElement.call( this, config );
11888 OO.ui.mixin.LabelElement.call( this, config );
11889 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
11890 OO.ui.mixin.FlaggedElement.call( this, config );
11891 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
11892
11893 // Properties
11894 this.href = null;
11895 this.target = null;
11896 this.noFollow = false;
11897
11898 // Events
11899 this.connect( this, { disable: 'onDisable' } );
11900
11901 // Initialization
11902 this.$button.append( this.$icon, this.$label, this.$indicator );
11903 this.$element
11904 .addClass( 'oo-ui-buttonWidget' )
11905 .append( this.$button );
11906 this.setHref( config.href );
11907 this.setTarget( config.target );
11908 this.setNoFollow( config.noFollow );
11909 };
11910
11911 /* Setup */
11912
11913 OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget );
11914 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.ButtonElement );
11915 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.IconElement );
11916 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.IndicatorElement );
11917 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.LabelElement );
11918 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.TitledElement );
11919 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.FlaggedElement );
11920 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.TabIndexedElement );
11921
11922 /* Methods */
11923
11924 /**
11925 * @inheritdoc
11926 */
11927 OO.ui.ButtonWidget.prototype.onMouseDown = function ( e ) {
11928 if ( !this.isDisabled() ) {
11929 // Remove the tab-index while the button is down to prevent the button from stealing focus
11930 this.$button.removeAttr( 'tabindex' );
11931 }
11932
11933 return OO.ui.mixin.ButtonElement.prototype.onMouseDown.call( this, e );
11934 };
11935
11936 /**
11937 * @inheritdoc
11938 */
11939 OO.ui.ButtonWidget.prototype.onMouseUp = function ( e ) {
11940 if ( !this.isDisabled() ) {
11941 // Restore the tab-index after the button is up to restore the button's accessibility
11942 this.$button.attr( 'tabindex', this.tabIndex );
11943 }
11944
11945 return OO.ui.mixin.ButtonElement.prototype.onMouseUp.call( this, e );
11946 };
11947
11948 /**
11949 * Get hyperlink location.
11950 *
11951 * @return {string} Hyperlink location
11952 */
11953 OO.ui.ButtonWidget.prototype.getHref = function () {
11954 return this.href;
11955 };
11956
11957 /**
11958 * Get hyperlink target.
11959 *
11960 * @return {string} Hyperlink target
11961 */
11962 OO.ui.ButtonWidget.prototype.getTarget = function () {
11963 return this.target;
11964 };
11965
11966 /**
11967 * Get search engine traversal hint.
11968 *
11969 * @return {boolean} Whether search engines should avoid traversing this hyperlink
11970 */
11971 OO.ui.ButtonWidget.prototype.getNoFollow = function () {
11972 return this.noFollow;
11973 };
11974
11975 /**
11976 * Set hyperlink location.
11977 *
11978 * @param {string|null} href Hyperlink location, null to remove
11979 */
11980 OO.ui.ButtonWidget.prototype.setHref = function ( href ) {
11981 href = typeof href === 'string' ? href : null;
11982
11983 if ( href !== this.href ) {
11984 this.href = href;
11985 this.updateHref();
11986 }
11987
11988 return this;
11989 };
11990
11991 /**
11992 * Update the `href` attribute, in case of changes to href or
11993 * disabled state.
11994 *
11995 * @private
11996 * @chainable
11997 */
11998 OO.ui.ButtonWidget.prototype.updateHref = function () {
11999 if ( this.href !== null && !this.isDisabled() ) {
12000 this.$button.attr( 'href', this.href );
12001 } else {
12002 this.$button.removeAttr( 'href' );
12003 }
12004
12005 return this;
12006 };
12007
12008 /**
12009 * Handle disable events.
12010 *
12011 * @private
12012 * @param {boolean} disabled Element is disabled
12013 */
12014 OO.ui.ButtonWidget.prototype.onDisable = function () {
12015 this.updateHref();
12016 };
12017
12018 /**
12019 * Set hyperlink target.
12020 *
12021 * @param {string|null} target Hyperlink target, null to remove
12022 */
12023 OO.ui.ButtonWidget.prototype.setTarget = function ( target ) {
12024 target = typeof target === 'string' ? target : null;
12025
12026 if ( target !== this.target ) {
12027 this.target = target;
12028 if ( target !== null ) {
12029 this.$button.attr( 'target', target );
12030 } else {
12031 this.$button.removeAttr( 'target' );
12032 }
12033 }
12034
12035 return this;
12036 };
12037
12038 /**
12039 * Set search engine traversal hint.
12040 *
12041 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
12042 */
12043 OO.ui.ButtonWidget.prototype.setNoFollow = function ( noFollow ) {
12044 noFollow = typeof noFollow === 'boolean' ? noFollow : true;
12045
12046 if ( noFollow !== this.noFollow ) {
12047 this.noFollow = noFollow;
12048 if ( noFollow ) {
12049 this.$button.attr( 'rel', 'nofollow' );
12050 } else {
12051 this.$button.removeAttr( 'rel' );
12052 }
12053 }
12054
12055 return this;
12056 };
12057
12058 /**
12059 * An ActionWidget is a {@link OO.ui.ButtonWidget button widget} that executes an action.
12060 * Action widgets are used with OO.ui.ActionSet, which manages the behavior and availability
12061 * of the actions.
12062 *
12063 * Both actions and action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
12064 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information
12065 * and examples.
12066 *
12067 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
12068 *
12069 * @class
12070 * @extends OO.ui.ButtonWidget
12071 * @mixins OO.ui.mixin.PendingElement
12072 *
12073 * @constructor
12074 * @param {Object} [config] Configuration options
12075 * @cfg {string} [action] Symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
12076 * @cfg {string[]} [modes] Symbolic names of the modes (e.g., ‘edit’ or ‘read’) in which the action
12077 * should be made available. See the action set's {@link OO.ui.ActionSet#setMode setMode} method
12078 * for more information about setting modes.
12079 * @cfg {boolean} [framed=false] Render the action button with a frame
12080 */
12081 OO.ui.ActionWidget = function OoUiActionWidget( config ) {
12082 // Configuration initialization
12083 config = $.extend( { framed: false }, config );
12084
12085 // Parent constructor
12086 OO.ui.ActionWidget.parent.call( this, config );
12087
12088 // Mixin constructors
12089 OO.ui.mixin.PendingElement.call( this, config );
12090
12091 // Properties
12092 this.action = config.action || '';
12093 this.modes = config.modes || [];
12094 this.width = 0;
12095 this.height = 0;
12096
12097 // Initialization
12098 this.$element.addClass( 'oo-ui-actionWidget' );
12099 };
12100
12101 /* Setup */
12102
12103 OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget );
12104 OO.mixinClass( OO.ui.ActionWidget, OO.ui.mixin.PendingElement );
12105
12106 /* Events */
12107
12108 /**
12109 * A resize event is emitted when the size of the widget changes.
12110 *
12111 * @event resize
12112 */
12113
12114 /* Methods */
12115
12116 /**
12117 * Check if the action is configured to be available in the specified `mode`.
12118 *
12119 * @param {string} mode Name of mode
12120 * @return {boolean} The action is configured with the mode
12121 */
12122 OO.ui.ActionWidget.prototype.hasMode = function ( mode ) {
12123 return this.modes.indexOf( mode ) !== -1;
12124 };
12125
12126 /**
12127 * Get the symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
12128 *
12129 * @return {string}
12130 */
12131 OO.ui.ActionWidget.prototype.getAction = function () {
12132 return this.action;
12133 };
12134
12135 /**
12136 * Get the symbolic name of the mode or modes for which the action is configured to be available.
12137 *
12138 * The current mode is set with the action set's {@link OO.ui.ActionSet#setMode setMode} method.
12139 * Only actions that are configured to be avaiable in the current mode will be visible. All other actions
12140 * are hidden.
12141 *
12142 * @return {string[]}
12143 */
12144 OO.ui.ActionWidget.prototype.getModes = function () {
12145 return this.modes.slice();
12146 };
12147
12148 /**
12149 * Emit a resize event if the size has changed.
12150 *
12151 * @private
12152 * @chainable
12153 */
12154 OO.ui.ActionWidget.prototype.propagateResize = function () {
12155 var width, height;
12156
12157 if ( this.isElementAttached() ) {
12158 width = this.$element.width();
12159 height = this.$element.height();
12160
12161 if ( width !== this.width || height !== this.height ) {
12162 this.width = width;
12163 this.height = height;
12164 this.emit( 'resize' );
12165 }
12166 }
12167
12168 return this;
12169 };
12170
12171 /**
12172 * @inheritdoc
12173 */
12174 OO.ui.ActionWidget.prototype.setIcon = function () {
12175 // Mixin method
12176 OO.ui.mixin.IconElement.prototype.setIcon.apply( this, arguments );
12177 this.propagateResize();
12178
12179 return this;
12180 };
12181
12182 /**
12183 * @inheritdoc
12184 */
12185 OO.ui.ActionWidget.prototype.setLabel = function () {
12186 // Mixin method
12187 OO.ui.mixin.LabelElement.prototype.setLabel.apply( this, arguments );
12188 this.propagateResize();
12189
12190 return this;
12191 };
12192
12193 /**
12194 * @inheritdoc
12195 */
12196 OO.ui.ActionWidget.prototype.setFlags = function () {
12197 // Mixin method
12198 OO.ui.mixin.FlaggedElement.prototype.setFlags.apply( this, arguments );
12199 this.propagateResize();
12200
12201 return this;
12202 };
12203
12204 /**
12205 * @inheritdoc
12206 */
12207 OO.ui.ActionWidget.prototype.clearFlags = function () {
12208 // Mixin method
12209 OO.ui.mixin.FlaggedElement.prototype.clearFlags.apply( this, arguments );
12210 this.propagateResize();
12211
12212 return this;
12213 };
12214
12215 /**
12216 * Toggle the visibility of the action button.
12217 *
12218 * @param {boolean} [show] Show button, omit to toggle visibility
12219 * @chainable
12220 */
12221 OO.ui.ActionWidget.prototype.toggle = function () {
12222 // Parent method
12223 OO.ui.ActionWidget.parent.prototype.toggle.apply( this, arguments );
12224 this.propagateResize();
12225
12226 return this;
12227 };
12228
12229 /**
12230 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
12231 * which is used to display additional information or options.
12232 *
12233 * @example
12234 * // Example of a popup button.
12235 * var popupButton = new OO.ui.PopupButtonWidget( {
12236 * label: 'Popup button with options',
12237 * icon: 'menu',
12238 * popup: {
12239 * $content: $( '<p>Additional options here.</p>' ),
12240 * padded: true,
12241 * align: 'force-left'
12242 * }
12243 * } );
12244 * // Append the button to the DOM.
12245 * $( 'body' ).append( popupButton.$element );
12246 *
12247 * @class
12248 * @extends OO.ui.ButtonWidget
12249 * @mixins OO.ui.mixin.PopupElement
12250 *
12251 * @constructor
12252 * @param {Object} [config] Configuration options
12253 */
12254 OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
12255 // Parent constructor
12256 OO.ui.PopupButtonWidget.parent.call( this, config );
12257
12258 // Mixin constructors
12259 OO.ui.mixin.PopupElement.call( this, config );
12260
12261 // Events
12262 this.connect( this, { click: 'onAction' } );
12263
12264 // Initialization
12265 this.$element
12266 .addClass( 'oo-ui-popupButtonWidget' )
12267 .attr( 'aria-haspopup', 'true' )
12268 .append( this.popup.$element );
12269 };
12270
12271 /* Setup */
12272
12273 OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
12274 OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.mixin.PopupElement );
12275
12276 /* Methods */
12277
12278 /**
12279 * Handle the button action being triggered.
12280 *
12281 * @private
12282 */
12283 OO.ui.PopupButtonWidget.prototype.onAction = function () {
12284 this.popup.toggle();
12285 };
12286
12287 /**
12288 * ToggleButtons are buttons that have a state (‘on’ or ‘off’) that is represented by a
12289 * Boolean value. Like other {@link OO.ui.ButtonWidget buttons}, toggle buttons can be
12290 * configured with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators},
12291 * {@link OO.ui.mixin.TitledElement titles}, {@link OO.ui.mixin.FlaggedElement styling flags},
12292 * and {@link OO.ui.mixin.LabelElement labels}. Please see
12293 * the [OOjs UI documentation][1] on MediaWiki for more information.
12294 *
12295 * @example
12296 * // Toggle buttons in the 'off' and 'on' state.
12297 * var toggleButton1 = new OO.ui.ToggleButtonWidget( {
12298 * label: 'Toggle Button off'
12299 * } );
12300 * var toggleButton2 = new OO.ui.ToggleButtonWidget( {
12301 * label: 'Toggle Button on',
12302 * value: true
12303 * } );
12304 * // Append the buttons to the DOM.
12305 * $( 'body' ).append( toggleButton1.$element, toggleButton2.$element );
12306 *
12307 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Toggle_buttons
12308 *
12309 * @class
12310 * @extends OO.ui.ToggleWidget
12311 * @mixins OO.ui.mixin.ButtonElement
12312 * @mixins OO.ui.mixin.IconElement
12313 * @mixins OO.ui.mixin.IndicatorElement
12314 * @mixins OO.ui.mixin.LabelElement
12315 * @mixins OO.ui.mixin.TitledElement
12316 * @mixins OO.ui.mixin.FlaggedElement
12317 * @mixins OO.ui.mixin.TabIndexedElement
12318 *
12319 * @constructor
12320 * @param {Object} [config] Configuration options
12321 * @cfg {boolean} [value=false] The toggle button’s initial on/off
12322 * state. By default, the button is in the 'off' state.
12323 */
12324 OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) {
12325 // Configuration initialization
12326 config = config || {};
12327
12328 // Parent constructor
12329 OO.ui.ToggleButtonWidget.parent.call( this, config );
12330
12331 // Mixin constructors
12332 OO.ui.mixin.ButtonElement.call( this, config );
12333 OO.ui.mixin.IconElement.call( this, config );
12334 OO.ui.mixin.IndicatorElement.call( this, config );
12335 OO.ui.mixin.LabelElement.call( this, config );
12336 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
12337 OO.ui.mixin.FlaggedElement.call( this, config );
12338 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
12339
12340 // Events
12341 this.connect( this, { click: 'onAction' } );
12342
12343 // Initialization
12344 this.$button.append( this.$icon, this.$label, this.$indicator );
12345 this.$element
12346 .addClass( 'oo-ui-toggleButtonWidget' )
12347 .append( this.$button );
12348 };
12349
12350 /* Setup */
12351
12352 OO.inheritClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget );
12353 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.ButtonElement );
12354 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.IconElement );
12355 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.IndicatorElement );
12356 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.LabelElement );
12357 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.TitledElement );
12358 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.FlaggedElement );
12359 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.TabIndexedElement );
12360
12361 /* Methods */
12362
12363 /**
12364 * Handle the button action being triggered.
12365 *
12366 * @private
12367 */
12368 OO.ui.ToggleButtonWidget.prototype.onAction = function () {
12369 this.setValue( !this.value );
12370 };
12371
12372 /**
12373 * @inheritdoc
12374 */
12375 OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
12376 value = !!value;
12377 if ( value !== this.value ) {
12378 // Might be called from parent constructor before ButtonElement constructor
12379 if ( this.$button ) {
12380 this.$button.attr( 'aria-pressed', value.toString() );
12381 }
12382 this.setActive( value );
12383 }
12384
12385 // Parent method
12386 OO.ui.ToggleButtonWidget.parent.prototype.setValue.call( this, value );
12387
12388 return this;
12389 };
12390
12391 /**
12392 * @inheritdoc
12393 */
12394 OO.ui.ToggleButtonWidget.prototype.setButtonElement = function ( $button ) {
12395 if ( this.$button ) {
12396 this.$button.removeAttr( 'aria-pressed' );
12397 }
12398 OO.ui.mixin.ButtonElement.prototype.setButtonElement.call( this, $button );
12399 this.$button.attr( 'aria-pressed', this.value.toString() );
12400 };
12401
12402 /**
12403 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
12404 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
12405 * users can interact with it.
12406 *
12407 * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
12408 * OO.ui.DropdownInputWidget instead.
12409 *
12410 * @example
12411 * // Example: A DropdownWidget with a menu that contains three options
12412 * var dropDown = new OO.ui.DropdownWidget( {
12413 * label: 'Dropdown menu: Select a menu option',
12414 * menu: {
12415 * items: [
12416 * new OO.ui.MenuOptionWidget( {
12417 * data: 'a',
12418 * label: 'First'
12419 * } ),
12420 * new OO.ui.MenuOptionWidget( {
12421 * data: 'b',
12422 * label: 'Second'
12423 * } ),
12424 * new OO.ui.MenuOptionWidget( {
12425 * data: 'c',
12426 * label: 'Third'
12427 * } )
12428 * ]
12429 * }
12430 * } );
12431 *
12432 * $( 'body' ).append( dropDown.$element );
12433 *
12434 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
12435 *
12436 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
12437 *
12438 * @class
12439 * @extends OO.ui.Widget
12440 * @mixins OO.ui.mixin.IconElement
12441 * @mixins OO.ui.mixin.IndicatorElement
12442 * @mixins OO.ui.mixin.LabelElement
12443 * @mixins OO.ui.mixin.TitledElement
12444 * @mixins OO.ui.mixin.TabIndexedElement
12445 *
12446 * @constructor
12447 * @param {Object} [config] Configuration options
12448 * @cfg {Object} [menu] Configuration options to pass to menu widget
12449 */
12450 OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
12451 // Configuration initialization
12452 config = $.extend( { indicator: 'down' }, config );
12453
12454 // Parent constructor
12455 OO.ui.DropdownWidget.parent.call( this, config );
12456
12457 // Properties (must be set before TabIndexedElement constructor call)
12458 this.$handle = this.$( '<span>' );
12459
12460 // Mixin constructors
12461 OO.ui.mixin.IconElement.call( this, config );
12462 OO.ui.mixin.IndicatorElement.call( this, config );
12463 OO.ui.mixin.LabelElement.call( this, config );
12464 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
12465 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
12466
12467 // Properties
12468 this.menu = new OO.ui.MenuSelectWidget( $.extend( { widget: this }, config.menu ) );
12469
12470 // Events
12471 this.$handle.on( {
12472 click: this.onClick.bind( this ),
12473 keypress: this.onKeyPress.bind( this )
12474 } );
12475 this.menu.connect( this, { select: 'onMenuSelect' } );
12476
12477 // Initialization
12478 this.$handle
12479 .addClass( 'oo-ui-dropdownWidget-handle' )
12480 .append( this.$icon, this.$label, this.$indicator );
12481 this.$element
12482 .addClass( 'oo-ui-dropdownWidget' )
12483 .append( this.$handle, this.menu.$element );
12484 };
12485
12486 /* Setup */
12487
12488 OO.inheritClass( OO.ui.DropdownWidget, OO.ui.Widget );
12489 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.IconElement );
12490 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.IndicatorElement );
12491 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.LabelElement );
12492 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.TitledElement );
12493 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.TabIndexedElement );
12494
12495 /* Methods */
12496
12497 /**
12498 * Get the menu.
12499 *
12500 * @return {OO.ui.MenuSelectWidget} Menu of widget
12501 */
12502 OO.ui.DropdownWidget.prototype.getMenu = function () {
12503 return this.menu;
12504 };
12505
12506 /**
12507 * Handles menu select events.
12508 *
12509 * @private
12510 * @param {OO.ui.MenuOptionWidget} item Selected menu item
12511 */
12512 OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
12513 var selectedLabel;
12514
12515 if ( !item ) {
12516 this.setLabel( null );
12517 return;
12518 }
12519
12520 selectedLabel = item.getLabel();
12521
12522 // If the label is a DOM element, clone it, because setLabel will append() it
12523 if ( selectedLabel instanceof jQuery ) {
12524 selectedLabel = selectedLabel.clone();
12525 }
12526
12527 this.setLabel( selectedLabel );
12528 };
12529
12530 /**
12531 * Handle mouse click events.
12532 *
12533 * @private
12534 * @param {jQuery.Event} e Mouse click event
12535 */
12536 OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
12537 if ( !this.isDisabled() && e.which === 1 ) {
12538 this.menu.toggle();
12539 }
12540 return false;
12541 };
12542
12543 /**
12544 * Handle key press events.
12545 *
12546 * @private
12547 * @param {jQuery.Event} e Key press event
12548 */
12549 OO.ui.DropdownWidget.prototype.onKeyPress = function ( e ) {
12550 if ( !this.isDisabled() &&
12551 ( ( e.which === OO.ui.Keys.SPACE && !this.menu.isVisible() ) || e.which === OO.ui.Keys.ENTER )
12552 ) {
12553 this.menu.toggle();
12554 return false;
12555 }
12556 };
12557
12558 /**
12559 * SelectFileWidgets allow for selecting files, using the HTML5 File API. These
12560 * widgets can be configured with {@link OO.ui.mixin.IconElement icons} and {@link
12561 * OO.ui.mixin.IndicatorElement indicators}.
12562 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
12563 *
12564 * @example
12565 * // Example of a file select widget
12566 * var selectFile = new OO.ui.SelectFileWidget();
12567 * $( 'body' ).append( selectFile.$element );
12568 *
12569 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets
12570 *
12571 * @class
12572 * @extends OO.ui.Widget
12573 * @mixins OO.ui.mixin.IconElement
12574 * @mixins OO.ui.mixin.IndicatorElement
12575 * @mixins OO.ui.mixin.PendingElement
12576 * @mixins OO.ui.mixin.LabelElement
12577 * @mixins OO.ui.mixin.TabIndexedElement
12578 *
12579 * @constructor
12580 * @param {Object} [config] Configuration options
12581 * @cfg {string[]|null} [accept=null] MIME types to accept. null accepts all types.
12582 * @cfg {string} [placeholder] Text to display when no file is selected.
12583 * @cfg {string} [notsupported] Text to display when file support is missing in the browser.
12584 * @cfg {boolean} [droppable=true] Whether to accept files by drag and drop.
12585 */
12586 OO.ui.SelectFileWidget = function OoUiSelectFileWidget( config ) {
12587 var dragHandler;
12588
12589 // Configuration initialization
12590 config = $.extend( {
12591 accept: null,
12592 placeholder: OO.ui.msg( 'ooui-selectfile-placeholder' ),
12593 notsupported: OO.ui.msg( 'ooui-selectfile-not-supported' ),
12594 droppable: true
12595 }, config );
12596
12597 // Parent constructor
12598 OO.ui.SelectFileWidget.parent.call( this, config );
12599
12600 // Properties (must be set before TabIndexedElement constructor call)
12601 this.$handle = $( '<span>' );
12602
12603 // Mixin constructors
12604 OO.ui.mixin.IconElement.call( this, config );
12605 OO.ui.mixin.IndicatorElement.call( this, config );
12606 OO.ui.mixin.PendingElement.call( this, config );
12607 OO.ui.mixin.LabelElement.call( this, $.extend( config, { autoFitLabel: true } ) );
12608 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
12609
12610 // Properties
12611 this.isSupported = this.constructor.static.isSupported();
12612 this.currentFile = null;
12613 if ( Array.isArray( config.accept ) ) {
12614 this.accept = config.accept;
12615 } else {
12616 this.accept = null;
12617 }
12618 this.placeholder = config.placeholder;
12619 this.notsupported = config.notsupported;
12620 this.onFileSelectedHandler = this.onFileSelected.bind( this );
12621
12622 this.clearButton = new OO.ui.ButtonWidget( {
12623 classes: [ 'oo-ui-selectFileWidget-clearButton' ],
12624 framed: false,
12625 icon: 'remove',
12626 disabled: this.disabled
12627 } );
12628
12629 // Events
12630 this.$handle.on( {
12631 keypress: this.onKeyPress.bind( this )
12632 } );
12633 this.clearButton.connect( this, {
12634 click: 'onClearClick'
12635 } );
12636 if ( config.droppable ) {
12637 dragHandler = this.onDragEnterOrOver.bind( this );
12638 this.$handle.on( {
12639 dragenter: dragHandler,
12640 dragover: dragHandler,
12641 dragleave: this.onDragLeave.bind( this ),
12642 drop: this.onDrop.bind( this )
12643 } );
12644 }
12645
12646 // Initialization
12647 this.addInput();
12648 this.updateUI();
12649 this.$label.addClass( 'oo-ui-selectFileWidget-label' );
12650 this.$handle
12651 .addClass( 'oo-ui-selectFileWidget-handle' )
12652 .append( this.$icon, this.$label, this.clearButton.$element, this.$indicator );
12653 this.$element
12654 .addClass( 'oo-ui-selectFileWidget' )
12655 .append( this.$handle );
12656 if ( config.droppable ) {
12657 this.$element.addClass( 'oo-ui-selectFileWidget-droppable' );
12658 }
12659 };
12660
12661 /* Setup */
12662
12663 OO.inheritClass( OO.ui.SelectFileWidget, OO.ui.Widget );
12664 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.IconElement );
12665 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.IndicatorElement );
12666 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.PendingElement );
12667 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.LabelElement );
12668 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.TabIndexedElement );
12669
12670 /* Static properties */
12671
12672 /**
12673 * Check if this widget is supported
12674 *
12675 * @static
12676 * @return {boolean}
12677 */
12678 OO.ui.SelectFileWidget.static.isSupported = function () {
12679 var $input;
12680 if ( OO.ui.SelectFileWidget.static.isSupportedCache === null ) {
12681 $input = $( '<input type="file">' );
12682 OO.ui.SelectFileWidget.static.isSupportedCache = $input[0].files !== undefined;
12683 }
12684 return OO.ui.SelectFileWidget.static.isSupportedCache;
12685 };
12686
12687 OO.ui.SelectFileWidget.static.isSupportedCache = null;
12688
12689 /* Events */
12690
12691 /**
12692 * @event change
12693 *
12694 * A change event is emitted when the on/off state of the toggle changes.
12695 *
12696 * @param {File|null} value New value
12697 */
12698
12699 /* Methods */
12700
12701 /**
12702 * Get the current value of the field
12703 *
12704 * @return {File|null}
12705 */
12706 OO.ui.SelectFileWidget.prototype.getValue = function () {
12707 return this.currentFile;
12708 };
12709
12710 /**
12711 * Set the current value of the field
12712 *
12713 * @param {File|null} file File to select
12714 */
12715 OO.ui.SelectFileWidget.prototype.setValue = function ( file ) {
12716 if ( this.currentFile !== file ) {
12717 this.currentFile = file;
12718 this.updateUI();
12719 this.emit( 'change', this.currentFile );
12720 }
12721 };
12722
12723 /**
12724 * Update the user interface when a file is selected or unselected
12725 *
12726 * @protected
12727 */
12728 OO.ui.SelectFileWidget.prototype.updateUI = function () {
12729 if ( !this.isSupported ) {
12730 this.$element.addClass( 'oo-ui-selectFileWidget-notsupported' );
12731 this.$element.removeClass( 'oo-ui-selectFileWidget-empty' );
12732 this.setLabel( this.notsupported );
12733 } else if ( this.currentFile ) {
12734 this.$element.removeClass( 'oo-ui-selectFileWidget-empty' );
12735 this.setLabel( this.currentFile.name +
12736 ( this.currentFile.type !== '' ? OO.ui.msg( 'ooui-semicolon-separator' ) + this.currentFile.type : '' )
12737 );
12738 } else {
12739 this.$element.addClass( 'oo-ui-selectFileWidget-empty' );
12740 this.setLabel( this.placeholder );
12741 }
12742
12743 if ( this.$input ) {
12744 this.$input.attr( 'title', this.getLabel() );
12745 }
12746 };
12747
12748 /**
12749 * Add the input to the handle
12750 *
12751 * @private
12752 */
12753 OO.ui.SelectFileWidget.prototype.addInput = function () {
12754 if ( this.$input ) {
12755 this.$input.remove();
12756 }
12757
12758 if ( !this.isSupported ) {
12759 this.$input = null;
12760 return;
12761 }
12762
12763 this.$input = $( '<input type="file">' );
12764 this.$input.on( 'change', this.onFileSelectedHandler );
12765 this.$input.attr( {
12766 tabindex: -1,
12767 title: this.getLabel()
12768 } );
12769 if ( this.accept ) {
12770 this.$input.attr( 'accept', this.accept.join( ', ' ) );
12771 }
12772 this.$handle.append( this.$input );
12773 };
12774
12775 /**
12776 * Determine if we should accept this file
12777 *
12778 * @private
12779 * @param {File} file
12780 * @return {boolean}
12781 */
12782 OO.ui.SelectFileWidget.prototype.isFileAcceptable = function ( file ) {
12783 var i, mime, mimeTest;
12784
12785 if ( !this.accept || file.type === '' ) {
12786 return true;
12787 }
12788
12789 mime = file.type;
12790 for ( i = 0; i < this.accept.length; i++ ) {
12791 mimeTest = this.accept[i];
12792 if ( mimeTest === mime ) {
12793 return true;
12794 } else if ( mimeTest.substr( -2 ) === '/*' ) {
12795 mimeTest = mimeTest.substr( 0, mimeTest.length - 1 );
12796 if ( mime.substr( 0, mimeTest.length ) === mimeTest ) {
12797 return true;
12798 }
12799 }
12800 }
12801
12802 return false;
12803 };
12804
12805 /**
12806 * Handle file selection from the input
12807 *
12808 * @private
12809 * @param {jQuery.Event} e
12810 */
12811 OO.ui.SelectFileWidget.prototype.onFileSelected = function ( e ) {
12812 var file = null;
12813
12814 if ( e.target.files && e.target.files[0] ) {
12815 file = e.target.files[0];
12816 if ( !this.isFileAcceptable( file ) ) {
12817 file = null;
12818 }
12819 }
12820
12821 this.setValue( file );
12822 this.addInput();
12823 };
12824
12825 /**
12826 * Handle clear button click events.
12827 *
12828 * @private
12829 */
12830 OO.ui.SelectFileWidget.prototype.onClearClick = function () {
12831 this.setValue( null );
12832 return false;
12833 };
12834
12835 /**
12836 * Handle key press events.
12837 *
12838 * @private
12839 * @param {jQuery.Event} e Key press event
12840 */
12841 OO.ui.SelectFileWidget.prototype.onKeyPress = function ( e ) {
12842 if ( this.isSupported && !this.isDisabled() && this.$input &&
12843 ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
12844 ) {
12845 this.$input.click();
12846 return false;
12847 }
12848 };
12849
12850 /**
12851 * Handle drag enter and over events
12852 *
12853 * @private
12854 * @param {jQuery.Event} e Drag event
12855 */
12856 OO.ui.SelectFileWidget.prototype.onDragEnterOrOver = function ( e ) {
12857 var file = null,
12858 dt = e.originalEvent.dataTransfer;
12859
12860 e.preventDefault();
12861 e.stopPropagation();
12862
12863 if ( this.isDisabled() || !this.isSupported ) {
12864 this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
12865 dt.dropEffect = 'none';
12866 return false;
12867 }
12868
12869 if ( dt && dt.files && dt.files[0] ) {
12870 file = dt.files[0];
12871 if ( !this.isFileAcceptable( file ) ) {
12872 file = null;
12873 }
12874 } else if ( dt && dt.types && $.inArray( 'Files', dt.types ) ) {
12875 // We know we have files so set 'file' to something truthy, we just
12876 // can't know any details about them.
12877 // * https://bugzilla.mozilla.org/show_bug.cgi?id=640534
12878 file = 'Files exist, but details are unknown';
12879 }
12880 if ( file ) {
12881 this.$element.addClass( 'oo-ui-selectFileWidget-canDrop' );
12882 } else {
12883 this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
12884 dt.dropEffect = 'none';
12885 }
12886
12887 return false;
12888 };
12889
12890 /**
12891 * Handle drag leave events
12892 *
12893 * @private
12894 * @param {jQuery.Event} e Drag event
12895 */
12896 OO.ui.SelectFileWidget.prototype.onDragLeave = function () {
12897 this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
12898 };
12899
12900 /**
12901 * Handle drop events
12902 *
12903 * @private
12904 * @param {jQuery.Event} e Drop event
12905 */
12906 OO.ui.SelectFileWidget.prototype.onDrop = function ( e ) {
12907 var file = null,
12908 dt = e.originalEvent.dataTransfer;
12909
12910 e.preventDefault();
12911 e.stopPropagation();
12912 this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
12913
12914 if ( this.isDisabled() || !this.isSupported ) {
12915 return false;
12916 }
12917
12918 if ( dt && dt.files && dt.files[0] ) {
12919 file = dt.files[0];
12920 if ( !this.isFileAcceptable( file ) ) {
12921 file = null;
12922 }
12923 }
12924 if ( file ) {
12925 this.setValue( file );
12926 }
12927
12928 return false;
12929 };
12930
12931 /**
12932 * @inheritdoc
12933 */
12934 OO.ui.SelectFileWidget.prototype.setDisabled = function ( state ) {
12935 OO.ui.SelectFileWidget.parent.prototype.setDisabled.call( this, state );
12936 if ( this.clearButton ) {
12937 this.clearButton.setDisabled( state );
12938 }
12939 return this;
12940 };
12941
12942 /**
12943 * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
12944 * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
12945 * for a list of icons included in the library.
12946 *
12947 * @example
12948 * // An icon widget with a label
12949 * var myIcon = new OO.ui.IconWidget( {
12950 * icon: 'help',
12951 * iconTitle: 'Help'
12952 * } );
12953 * // Create a label.
12954 * var iconLabel = new OO.ui.LabelWidget( {
12955 * label: 'Help'
12956 * } );
12957 * $( 'body' ).append( myIcon.$element, iconLabel.$element );
12958 *
12959 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
12960 *
12961 * @class
12962 * @extends OO.ui.Widget
12963 * @mixins OO.ui.mixin.IconElement
12964 * @mixins OO.ui.mixin.TitledElement
12965 * @mixins OO.ui.mixin.FlaggedElement
12966 *
12967 * @constructor
12968 * @param {Object} [config] Configuration options
12969 */
12970 OO.ui.IconWidget = function OoUiIconWidget( config ) {
12971 // Configuration initialization
12972 config = config || {};
12973
12974 // Parent constructor
12975 OO.ui.IconWidget.parent.call( this, config );
12976
12977 // Mixin constructors
12978 OO.ui.mixin.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) );
12979 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
12980 OO.ui.mixin.FlaggedElement.call( this, $.extend( {}, config, { $flagged: this.$element } ) );
12981
12982 // Initialization
12983 this.$element.addClass( 'oo-ui-iconWidget' );
12984 };
12985
12986 /* Setup */
12987
12988 OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
12989 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.IconElement );
12990 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.TitledElement );
12991 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.FlaggedElement );
12992
12993 /* Static Properties */
12994
12995 OO.ui.IconWidget.static.tagName = 'span';
12996
12997 /**
12998 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
12999 * attention to the status of an item or to clarify the function of a control. For a list of
13000 * indicators included in the library, please see the [OOjs UI documentation on MediaWiki][1].
13001 *
13002 * @example
13003 * // Example of an indicator widget
13004 * var indicator1 = new OO.ui.IndicatorWidget( {
13005 * indicator: 'alert'
13006 * } );
13007 *
13008 * // Create a fieldset layout to add a label
13009 * var fieldset = new OO.ui.FieldsetLayout();
13010 * fieldset.addItems( [
13011 * new OO.ui.FieldLayout( indicator1, { label: 'An alert indicator:' } )
13012 * ] );
13013 * $( 'body' ).append( fieldset.$element );
13014 *
13015 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
13016 *
13017 * @class
13018 * @extends OO.ui.Widget
13019 * @mixins OO.ui.mixin.IndicatorElement
13020 * @mixins OO.ui.mixin.TitledElement
13021 *
13022 * @constructor
13023 * @param {Object} [config] Configuration options
13024 */
13025 OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
13026 // Configuration initialization
13027 config = config || {};
13028
13029 // Parent constructor
13030 OO.ui.IndicatorWidget.parent.call( this, config );
13031
13032 // Mixin constructors
13033 OO.ui.mixin.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) );
13034 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
13035
13036 // Initialization
13037 this.$element.addClass( 'oo-ui-indicatorWidget' );
13038 };
13039
13040 /* Setup */
13041
13042 OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
13043 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.mixin.IndicatorElement );
13044 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.mixin.TitledElement );
13045
13046 /* Static Properties */
13047
13048 OO.ui.IndicatorWidget.static.tagName = 'span';
13049
13050 /**
13051 * InputWidget is the base class for all input widgets, which
13052 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
13053 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
13054 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
13055 *
13056 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
13057 *
13058 * @abstract
13059 * @class
13060 * @extends OO.ui.Widget
13061 * @mixins OO.ui.mixin.FlaggedElement
13062 * @mixins OO.ui.mixin.TabIndexedElement
13063 *
13064 * @constructor
13065 * @param {Object} [config] Configuration options
13066 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
13067 * @cfg {string} [value=''] The value of the input.
13068 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
13069 * before it is accepted.
13070 */
13071 OO.ui.InputWidget = function OoUiInputWidget( config ) {
13072 // Configuration initialization
13073 config = config || {};
13074
13075 // Parent constructor
13076 OO.ui.InputWidget.parent.call( this, config );
13077
13078 // Properties
13079 this.$input = this.getInputElement( config );
13080 this.value = '';
13081 this.inputFilter = config.inputFilter;
13082
13083 // Mixin constructors
13084 OO.ui.mixin.FlaggedElement.call( this, config );
13085 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$input } ) );
13086
13087 // Events
13088 this.$input.on( 'keydown mouseup cut paste change input select', this.onEdit.bind( this ) );
13089
13090 // Initialization
13091 this.$input
13092 .attr( 'name', config.name )
13093 .prop( 'disabled', this.isDisabled() );
13094 this.$element
13095 .addClass( 'oo-ui-inputWidget' )
13096 .append( this.$input );
13097 this.setValue( config.value );
13098 };
13099
13100 /* Setup */
13101
13102 OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
13103 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.FlaggedElement );
13104 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.TabIndexedElement );
13105
13106 /* Static Properties */
13107
13108 OO.ui.InputWidget.static.supportsSimpleLabel = true;
13109
13110 /* Events */
13111
13112 /**
13113 * @event change
13114 *
13115 * A change event is emitted when the value of the input changes.
13116 *
13117 * @param {string} value
13118 */
13119
13120 /* Methods */
13121
13122 /**
13123 * Get input element.
13124 *
13125 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
13126 * different circumstances. The element must have a `value` property (like form elements).
13127 *
13128 * @protected
13129 * @param {Object} config Configuration options
13130 * @return {jQuery} Input element
13131 */
13132 OO.ui.InputWidget.prototype.getInputElement = function () {
13133 return $( '<input>' );
13134 };
13135
13136 /**
13137 * Handle potentially value-changing events.
13138 *
13139 * @private
13140 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
13141 */
13142 OO.ui.InputWidget.prototype.onEdit = function () {
13143 var widget = this;
13144 if ( !this.isDisabled() ) {
13145 // Allow the stack to clear so the value will be updated
13146 setTimeout( function () {
13147 widget.setValue( widget.$input.val() );
13148 } );
13149 }
13150 };
13151
13152 /**
13153 * Get the value of the input.
13154 *
13155 * @return {string} Input value
13156 */
13157 OO.ui.InputWidget.prototype.getValue = function () {
13158 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
13159 // it, and we won't know unless they're kind enough to trigger a 'change' event.
13160 var value = this.$input.val();
13161 if ( this.value !== value ) {
13162 this.setValue( value );
13163 }
13164 return this.value;
13165 };
13166
13167 /**
13168 * Set the direction of the input, either RTL (right-to-left) or LTR (left-to-right).
13169 *
13170 * @param {boolean} isRTL
13171 * Direction is right-to-left
13172 */
13173 OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
13174 this.$input.prop( 'dir', isRTL ? 'rtl' : 'ltr' );
13175 };
13176
13177 /**
13178 * Set the value of the input.
13179 *
13180 * @param {string} value New value
13181 * @fires change
13182 * @chainable
13183 */
13184 OO.ui.InputWidget.prototype.setValue = function ( value ) {
13185 value = this.cleanUpValue( value );
13186 // Update the DOM if it has changed. Note that with cleanUpValue, it
13187 // is possible for the DOM value to change without this.value changing.
13188 if ( this.$input.val() !== value ) {
13189 this.$input.val( value );
13190 }
13191 if ( this.value !== value ) {
13192 this.value = value;
13193 this.emit( 'change', this.value );
13194 }
13195 return this;
13196 };
13197
13198 /**
13199 * Clean up incoming value.
13200 *
13201 * Ensures value is a string, and converts undefined and null to empty string.
13202 *
13203 * @private
13204 * @param {string} value Original value
13205 * @return {string} Cleaned up value
13206 */
13207 OO.ui.InputWidget.prototype.cleanUpValue = function ( value ) {
13208 if ( value === undefined || value === null ) {
13209 return '';
13210 } else if ( this.inputFilter ) {
13211 return this.inputFilter( String( value ) );
13212 } else {
13213 return String( value );
13214 }
13215 };
13216
13217 /**
13218 * Simulate the behavior of clicking on a label bound to this input. This method is only called by
13219 * {@link OO.ui.LabelWidget LabelWidget} and {@link OO.ui.FieldLayout FieldLayout}. It should not be
13220 * called directly.
13221 */
13222 OO.ui.InputWidget.prototype.simulateLabelClick = function () {
13223 if ( !this.isDisabled() ) {
13224 if ( this.$input.is( ':checkbox, :radio' ) ) {
13225 this.$input.click();
13226 }
13227 if ( this.$input.is( ':input' ) ) {
13228 this.$input[ 0 ].focus();
13229 }
13230 }
13231 };
13232
13233 /**
13234 * @inheritdoc
13235 */
13236 OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
13237 OO.ui.InputWidget.parent.prototype.setDisabled.call( this, state );
13238 if ( this.$input ) {
13239 this.$input.prop( 'disabled', this.isDisabled() );
13240 }
13241 return this;
13242 };
13243
13244 /**
13245 * Focus the input.
13246 *
13247 * @chainable
13248 */
13249 OO.ui.InputWidget.prototype.focus = function () {
13250 this.$input[ 0 ].focus();
13251 return this;
13252 };
13253
13254 /**
13255 * Blur the input.
13256 *
13257 * @chainable
13258 */
13259 OO.ui.InputWidget.prototype.blur = function () {
13260 this.$input[ 0 ].blur();
13261 return this;
13262 };
13263
13264 /**
13265 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
13266 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
13267 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
13268 * HTML `<button/>` (the default) or an HTML `<input/>` tags. See the
13269 * [OOjs UI documentation on MediaWiki] [1] for more information.
13270 *
13271 * @example
13272 * // A ButtonInputWidget rendered as an HTML button, the default.
13273 * var button = new OO.ui.ButtonInputWidget( {
13274 * label: 'Input button',
13275 * icon: 'check',
13276 * value: 'check'
13277 * } );
13278 * $( 'body' ).append( button.$element );
13279 *
13280 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
13281 *
13282 * @class
13283 * @extends OO.ui.InputWidget
13284 * @mixins OO.ui.mixin.ButtonElement
13285 * @mixins OO.ui.mixin.IconElement
13286 * @mixins OO.ui.mixin.IndicatorElement
13287 * @mixins OO.ui.mixin.LabelElement
13288 * @mixins OO.ui.mixin.TitledElement
13289 *
13290 * @constructor
13291 * @param {Object} [config] Configuration options
13292 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
13293 * @cfg {boolean} [useInputTag=false] Use an `<input/>` tag instead of a `<button/>` tag, the default.
13294 * Widgets configured to be an `<input/>` do not support {@link #icon icons} and {@link #indicator indicators},
13295 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
13296 * be set to `true` when there’s need to support IE6 in a form with multiple buttons.
13297 */
13298 OO.ui.ButtonInputWidget = function OoUiButtonInputWidget( config ) {
13299 // Configuration initialization
13300 config = $.extend( { type: 'button', useInputTag: false }, config );
13301
13302 // Properties (must be set before parent constructor, which calls #setValue)
13303 this.useInputTag = config.useInputTag;
13304
13305 // Parent constructor
13306 OO.ui.ButtonInputWidget.parent.call( this, config );
13307
13308 // Mixin constructors
13309 OO.ui.mixin.ButtonElement.call( this, $.extend( {}, config, { $button: this.$input } ) );
13310 OO.ui.mixin.IconElement.call( this, config );
13311 OO.ui.mixin.IndicatorElement.call( this, config );
13312 OO.ui.mixin.LabelElement.call( this, config );
13313 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
13314
13315 // Initialization
13316 if ( !config.useInputTag ) {
13317 this.$input.append( this.$icon, this.$label, this.$indicator );
13318 }
13319 this.$element.addClass( 'oo-ui-buttonInputWidget' );
13320 };
13321
13322 /* Setup */
13323
13324 OO.inheritClass( OO.ui.ButtonInputWidget, OO.ui.InputWidget );
13325 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.ButtonElement );
13326 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IconElement );
13327 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IndicatorElement );
13328 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.LabelElement );
13329 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.TitledElement );
13330
13331 /* Static Properties */
13332
13333 /**
13334 * Disable generating `<label>` elements for buttons. One would very rarely need additional label
13335 * for a button, and it's already a big clickable target, and it causes unexpected rendering.
13336 */
13337 OO.ui.ButtonInputWidget.static.supportsSimpleLabel = false;
13338
13339 /* Methods */
13340
13341 /**
13342 * @inheritdoc
13343 * @protected
13344 */
13345 OO.ui.ButtonInputWidget.prototype.getInputElement = function ( config ) {
13346 var type = [ 'button', 'submit', 'reset' ].indexOf( config.type ) !== -1 ?
13347 config.type :
13348 'button';
13349 return $( '<' + ( config.useInputTag ? 'input' : 'button' ) + ' type="' + type + '">' );
13350 };
13351
13352 /**
13353 * Set label value.
13354 *
13355 * If #useInputTag is `true`, the label is set as the `value` of the `<input/>` tag.
13356 *
13357 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
13358 * text, or `null` for no label
13359 * @chainable
13360 */
13361 OO.ui.ButtonInputWidget.prototype.setLabel = function ( label ) {
13362 OO.ui.mixin.LabelElement.prototype.setLabel.call( this, label );
13363
13364 if ( this.useInputTag ) {
13365 if ( typeof label === 'function' ) {
13366 label = OO.ui.resolveMsg( label );
13367 }
13368 if ( label instanceof jQuery ) {
13369 label = label.text();
13370 }
13371 if ( !label ) {
13372 label = '';
13373 }
13374 this.$input.val( label );
13375 }
13376
13377 return this;
13378 };
13379
13380 /**
13381 * Set the value of the input.
13382 *
13383 * This method is disabled for button inputs configured as {@link #useInputTag <input/> tags}, as
13384 * they do not support {@link #value values}.
13385 *
13386 * @param {string} value New value
13387 * @chainable
13388 */
13389 OO.ui.ButtonInputWidget.prototype.setValue = function ( value ) {
13390 if ( !this.useInputTag ) {
13391 OO.ui.ButtonInputWidget.parent.prototype.setValue.call( this, value );
13392 }
13393 return this;
13394 };
13395
13396 /**
13397 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
13398 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
13399 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
13400 * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
13401 *
13402 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
13403 *
13404 * @example
13405 * // An example of selected, unselected, and disabled checkbox inputs
13406 * var checkbox1=new OO.ui.CheckboxInputWidget( {
13407 * value: 'a',
13408 * selected: true
13409 * } );
13410 * var checkbox2=new OO.ui.CheckboxInputWidget( {
13411 * value: 'b'
13412 * } );
13413 * var checkbox3=new OO.ui.CheckboxInputWidget( {
13414 * value:'c',
13415 * disabled: true
13416 * } );
13417 * // Create a fieldset layout with fields for each checkbox.
13418 * var fieldset = new OO.ui.FieldsetLayout( {
13419 * label: 'Checkboxes'
13420 * } );
13421 * fieldset.addItems( [
13422 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
13423 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
13424 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
13425 * ] );
13426 * $( 'body' ).append( fieldset.$element );
13427 *
13428 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
13429 *
13430 * @class
13431 * @extends OO.ui.InputWidget
13432 *
13433 * @constructor
13434 * @param {Object} [config] Configuration options
13435 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
13436 */
13437 OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
13438 // Configuration initialization
13439 config = config || {};
13440
13441 // Parent constructor
13442 OO.ui.CheckboxInputWidget.parent.call( this, config );
13443
13444 // Initialization
13445 this.$element
13446 .addClass( 'oo-ui-checkboxInputWidget' )
13447 // Required for pretty styling in MediaWiki theme
13448 .append( $( '<span>' ) );
13449 this.setSelected( config.selected !== undefined ? config.selected : false );
13450 };
13451
13452 /* Setup */
13453
13454 OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
13455
13456 /* Methods */
13457
13458 /**
13459 * @inheritdoc
13460 * @protected
13461 */
13462 OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
13463 return $( '<input type="checkbox" />' );
13464 };
13465
13466 /**
13467 * @inheritdoc
13468 */
13469 OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
13470 var widget = this;
13471 if ( !this.isDisabled() ) {
13472 // Allow the stack to clear so the value will be updated
13473 setTimeout( function () {
13474 widget.setSelected( widget.$input.prop( 'checked' ) );
13475 } );
13476 }
13477 };
13478
13479 /**
13480 * Set selection state of this checkbox.
13481 *
13482 * @param {boolean} state `true` for selected
13483 * @chainable
13484 */
13485 OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state ) {
13486 state = !!state;
13487 if ( this.selected !== state ) {
13488 this.selected = state;
13489 this.$input.prop( 'checked', this.selected );
13490 this.emit( 'change', this.selected );
13491 }
13492 return this;
13493 };
13494
13495 /**
13496 * Check if this checkbox is selected.
13497 *
13498 * @return {boolean} Checkbox is selected
13499 */
13500 OO.ui.CheckboxInputWidget.prototype.isSelected = function () {
13501 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
13502 // it, and we won't know unless they're kind enough to trigger a 'change' event.
13503 var selected = this.$input.prop( 'checked' );
13504 if ( this.selected !== selected ) {
13505 this.setSelected( selected );
13506 }
13507 return this.selected;
13508 };
13509
13510 /**
13511 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
13512 * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
13513 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
13514 * more information about input widgets.
13515 *
13516 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
13517 * are no options. If no `value` configuration option is provided, the first option is selected.
13518 * If you need a state representing no value (no option being selected), use a DropdownWidget.
13519 *
13520 * This and OO.ui.RadioSelectInputWidget support the same configuration options.
13521 *
13522 * @example
13523 * // Example: A DropdownInputWidget with three options
13524 * var dropdownInput = new OO.ui.DropdownInputWidget( {
13525 * options: [
13526 * { data: 'a', label: 'First' },
13527 * { data: 'b', label: 'Second'},
13528 * { data: 'c', label: 'Third' }
13529 * ]
13530 * } );
13531 * $( 'body' ).append( dropdownInput.$element );
13532 *
13533 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
13534 *
13535 * @class
13536 * @extends OO.ui.InputWidget
13537 *
13538 * @constructor
13539 * @param {Object} [config] Configuration options
13540 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
13541 */
13542 OO.ui.DropdownInputWidget = function OoUiDropdownInputWidget( config ) {
13543 // Configuration initialization
13544 config = config || {};
13545
13546 // Properties (must be done before parent constructor which calls #setDisabled)
13547 this.dropdownWidget = new OO.ui.DropdownWidget();
13548
13549 // Parent constructor
13550 OO.ui.DropdownInputWidget.parent.call( this, config );
13551
13552 // Events
13553 this.dropdownWidget.getMenu().connect( this, { select: 'onMenuSelect' } );
13554
13555 // Initialization
13556 this.setOptions( config.options || [] );
13557 this.$element
13558 .addClass( 'oo-ui-dropdownInputWidget' )
13559 .append( this.dropdownWidget.$element );
13560 };
13561
13562 /* Setup */
13563
13564 OO.inheritClass( OO.ui.DropdownInputWidget, OO.ui.InputWidget );
13565
13566 /* Methods */
13567
13568 /**
13569 * @inheritdoc
13570 * @protected
13571 */
13572 OO.ui.DropdownInputWidget.prototype.getInputElement = function () {
13573 return $( '<input type="hidden">' );
13574 };
13575
13576 /**
13577 * Handles menu select events.
13578 *
13579 * @private
13580 * @param {OO.ui.MenuOptionWidget} item Selected menu item
13581 */
13582 OO.ui.DropdownInputWidget.prototype.onMenuSelect = function ( item ) {
13583 this.setValue( item.getData() );
13584 };
13585
13586 /**
13587 * @inheritdoc
13588 */
13589 OO.ui.DropdownInputWidget.prototype.setValue = function ( value ) {
13590 value = this.cleanUpValue( value );
13591 this.dropdownWidget.getMenu().selectItemByData( value );
13592 OO.ui.DropdownInputWidget.parent.prototype.setValue.call( this, value );
13593 return this;
13594 };
13595
13596 /**
13597 * @inheritdoc
13598 */
13599 OO.ui.DropdownInputWidget.prototype.setDisabled = function ( state ) {
13600 this.dropdownWidget.setDisabled( state );
13601 OO.ui.DropdownInputWidget.parent.prototype.setDisabled.call( this, state );
13602 return this;
13603 };
13604
13605 /**
13606 * Set the options available for this input.
13607 *
13608 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
13609 * @chainable
13610 */
13611 OO.ui.DropdownInputWidget.prototype.setOptions = function ( options ) {
13612 var
13613 value = this.getValue(),
13614 widget = this;
13615
13616 // Rebuild the dropdown menu
13617 this.dropdownWidget.getMenu()
13618 .clearItems()
13619 .addItems( options.map( function ( opt ) {
13620 var optValue = widget.cleanUpValue( opt.data );
13621 return new OO.ui.MenuOptionWidget( {
13622 data: optValue,
13623 label: opt.label !== undefined ? opt.label : optValue
13624 } );
13625 } ) );
13626
13627 // Restore the previous value, or reset to something sensible
13628 if ( this.dropdownWidget.getMenu().getItemFromData( value ) ) {
13629 // Previous value is still available, ensure consistency with the dropdown
13630 this.setValue( value );
13631 } else {
13632 // No longer valid, reset
13633 if ( options.length ) {
13634 this.setValue( options[ 0 ].data );
13635 }
13636 }
13637
13638 return this;
13639 };
13640
13641 /**
13642 * @inheritdoc
13643 */
13644 OO.ui.DropdownInputWidget.prototype.focus = function () {
13645 this.dropdownWidget.getMenu().toggle( true );
13646 return this;
13647 };
13648
13649 /**
13650 * @inheritdoc
13651 */
13652 OO.ui.DropdownInputWidget.prototype.blur = function () {
13653 this.dropdownWidget.getMenu().toggle( false );
13654 return this;
13655 };
13656
13657 /**
13658 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
13659 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
13660 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
13661 * please see the [OOjs UI documentation on MediaWiki][1].
13662 *
13663 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
13664 *
13665 * @example
13666 * // An example of selected, unselected, and disabled radio inputs
13667 * var radio1 = new OO.ui.RadioInputWidget( {
13668 * value: 'a',
13669 * selected: true
13670 * } );
13671 * var radio2 = new OO.ui.RadioInputWidget( {
13672 * value: 'b'
13673 * } );
13674 * var radio3 = new OO.ui.RadioInputWidget( {
13675 * value: 'c',
13676 * disabled: true
13677 * } );
13678 * // Create a fieldset layout with fields for each radio button.
13679 * var fieldset = new OO.ui.FieldsetLayout( {
13680 * label: 'Radio inputs'
13681 * } );
13682 * fieldset.addItems( [
13683 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
13684 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
13685 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
13686 * ] );
13687 * $( 'body' ).append( fieldset.$element );
13688 *
13689 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
13690 *
13691 * @class
13692 * @extends OO.ui.InputWidget
13693 *
13694 * @constructor
13695 * @param {Object} [config] Configuration options
13696 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
13697 */
13698 OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) {
13699 // Configuration initialization
13700 config = config || {};
13701
13702 // Parent constructor
13703 OO.ui.RadioInputWidget.parent.call( this, config );
13704
13705 // Initialization
13706 this.$element
13707 .addClass( 'oo-ui-radioInputWidget' )
13708 // Required for pretty styling in MediaWiki theme
13709 .append( $( '<span>' ) );
13710 this.setSelected( config.selected !== undefined ? config.selected : false );
13711 };
13712
13713 /* Setup */
13714
13715 OO.inheritClass( OO.ui.RadioInputWidget, OO.ui.InputWidget );
13716
13717 /* Methods */
13718
13719 /**
13720 * @inheritdoc
13721 * @protected
13722 */
13723 OO.ui.RadioInputWidget.prototype.getInputElement = function () {
13724 return $( '<input type="radio" />' );
13725 };
13726
13727 /**
13728 * @inheritdoc
13729 */
13730 OO.ui.RadioInputWidget.prototype.onEdit = function () {
13731 // RadioInputWidget doesn't track its state.
13732 };
13733
13734 /**
13735 * Set selection state of this radio button.
13736 *
13737 * @param {boolean} state `true` for selected
13738 * @chainable
13739 */
13740 OO.ui.RadioInputWidget.prototype.setSelected = function ( state ) {
13741 // RadioInputWidget doesn't track its state.
13742 this.$input.prop( 'checked', state );
13743 return this;
13744 };
13745
13746 /**
13747 * Check if this radio button is selected.
13748 *
13749 * @return {boolean} Radio is selected
13750 */
13751 OO.ui.RadioInputWidget.prototype.isSelected = function () {
13752 return this.$input.prop( 'checked' );
13753 };
13754
13755 /**
13756 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
13757 * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
13758 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
13759 * more information about input widgets.
13760 *
13761 * This and OO.ui.DropdownInputWidget support the same configuration options.
13762 *
13763 * @example
13764 * // Example: A RadioSelectInputWidget with three options
13765 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
13766 * options: [
13767 * { data: 'a', label: 'First' },
13768 * { data: 'b', label: 'Second'},
13769 * { data: 'c', label: 'Third' }
13770 * ]
13771 * } );
13772 * $( 'body' ).append( radioSelectInput.$element );
13773 *
13774 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
13775 *
13776 * @class
13777 * @extends OO.ui.InputWidget
13778 *
13779 * @constructor
13780 * @param {Object} [config] Configuration options
13781 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
13782 */
13783 OO.ui.RadioSelectInputWidget = function OoUiRadioSelectInputWidget( config ) {
13784 // Configuration initialization
13785 config = config || {};
13786
13787 // Properties (must be done before parent constructor which calls #setDisabled)
13788 this.radioSelectWidget = new OO.ui.RadioSelectWidget();
13789
13790 // Parent constructor
13791 OO.ui.RadioSelectInputWidget.parent.call( this, config );
13792
13793 // Events
13794 this.radioSelectWidget.connect( this, { select: 'onMenuSelect' } );
13795
13796 // Initialization
13797 this.setOptions( config.options || [] );
13798 this.$element
13799 .addClass( 'oo-ui-radioSelectInputWidget' )
13800 .append( this.radioSelectWidget.$element );
13801 };
13802
13803 /* Setup */
13804
13805 OO.inheritClass( OO.ui.RadioSelectInputWidget, OO.ui.InputWidget );
13806
13807 /* Static Properties */
13808
13809 OO.ui.RadioSelectInputWidget.static.supportsSimpleLabel = false;
13810
13811 /* Methods */
13812
13813 /**
13814 * @inheritdoc
13815 * @protected
13816 */
13817 OO.ui.RadioSelectInputWidget.prototype.getInputElement = function () {
13818 return $( '<input type="hidden">' );
13819 };
13820
13821 /**
13822 * Handles menu select events.
13823 *
13824 * @private
13825 * @param {OO.ui.RadioOptionWidget} item Selected menu item
13826 */
13827 OO.ui.RadioSelectInputWidget.prototype.onMenuSelect = function ( item ) {
13828 this.setValue( item.getData() );
13829 };
13830
13831 /**
13832 * @inheritdoc
13833 */
13834 OO.ui.RadioSelectInputWidget.prototype.setValue = function ( value ) {
13835 value = this.cleanUpValue( value );
13836 this.radioSelectWidget.selectItemByData( value );
13837 OO.ui.RadioSelectInputWidget.parent.prototype.setValue.call( this, value );
13838 return this;
13839 };
13840
13841 /**
13842 * @inheritdoc
13843 */
13844 OO.ui.RadioSelectInputWidget.prototype.setDisabled = function ( state ) {
13845 this.radioSelectWidget.setDisabled( state );
13846 OO.ui.RadioSelectInputWidget.parent.prototype.setDisabled.call( this, state );
13847 return this;
13848 };
13849
13850 /**
13851 * Set the options available for this input.
13852 *
13853 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
13854 * @chainable
13855 */
13856 OO.ui.RadioSelectInputWidget.prototype.setOptions = function ( options ) {
13857 var
13858 value = this.getValue(),
13859 widget = this;
13860
13861 // Rebuild the radioSelect menu
13862 this.radioSelectWidget
13863 .clearItems()
13864 .addItems( options.map( function ( opt ) {
13865 var optValue = widget.cleanUpValue( opt.data );
13866 return new OO.ui.RadioOptionWidget( {
13867 data: optValue,
13868 label: opt.label !== undefined ? opt.label : optValue
13869 } );
13870 } ) );
13871
13872 // Restore the previous value, or reset to something sensible
13873 if ( this.radioSelectWidget.getItemFromData( value ) ) {
13874 // Previous value is still available, ensure consistency with the radioSelect
13875 this.setValue( value );
13876 } else {
13877 // No longer valid, reset
13878 if ( options.length ) {
13879 this.setValue( options[ 0 ].data );
13880 }
13881 }
13882
13883 return this;
13884 };
13885
13886 /**
13887 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
13888 * size of the field as well as its presentation. In addition, these widgets can be configured
13889 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
13890 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
13891 * which modifies incoming values rather than validating them.
13892 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
13893 *
13894 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
13895 *
13896 * @example
13897 * // Example of a text input widget
13898 * var textInput = new OO.ui.TextInputWidget( {
13899 * value: 'Text input'
13900 * } )
13901 * $( 'body' ).append( textInput.$element );
13902 *
13903 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
13904 *
13905 * @class
13906 * @extends OO.ui.InputWidget
13907 * @mixins OO.ui.mixin.IconElement
13908 * @mixins OO.ui.mixin.IndicatorElement
13909 * @mixins OO.ui.mixin.PendingElement
13910 * @mixins OO.ui.mixin.LabelElement
13911 *
13912 * @constructor
13913 * @param {Object} [config] Configuration options
13914 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password', 'search',
13915 * 'email' or 'url'. Ignored if `multiline` is true.
13916 * @cfg {string} [placeholder] Placeholder text
13917 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
13918 * instruct the browser to focus this widget.
13919 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
13920 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
13921 * @cfg {boolean} [multiline=false] Allow multiple lines of text
13922 * @cfg {number} [rows] If multiline, number of visible lines in textarea. If used with `autosize`,
13923 * specifies minimum number of rows to display.
13924 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
13925 * Use the #maxRows config to specify a maximum number of displayed rows.
13926 * @cfg {boolean} [maxRows] Maximum number of rows to display when #autosize is set to true.
13927 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
13928 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
13929 * the value or placeholder text: `'before'` or `'after'`
13930 * @cfg {boolean} [required=false] Mark the field as required
13931 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
13932 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
13933 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
13934 * (the value must contain only numbers); when RegExp, a regular expression that must match the
13935 * value for it to be considered valid; when Function, a function receiving the value as parameter
13936 * that must return true, or promise resolving to true, for it to be considered valid.
13937 */
13938 OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
13939 // Configuration initialization
13940 config = $.extend( {
13941 type: 'text',
13942 labelPosition: 'after'
13943 }, config );
13944
13945 // Parent constructor
13946 OO.ui.TextInputWidget.parent.call( this, config );
13947
13948 // Mixin constructors
13949 OO.ui.mixin.IconElement.call( this, config );
13950 OO.ui.mixin.IndicatorElement.call( this, config );
13951 OO.ui.mixin.PendingElement.call( this, config );
13952 OO.ui.mixin.LabelElement.call( this, config );
13953
13954 // Properties
13955 this.readOnly = false;
13956 this.multiline = !!config.multiline;
13957 this.autosize = !!config.autosize;
13958 this.minRows = config.rows !== undefined ? config.rows : '';
13959 this.maxRows = config.maxRows || Math.max( 2 * ( this.minRows || 0 ), 10 );
13960 this.validate = null;
13961
13962 // Clone for resizing
13963 if ( this.autosize ) {
13964 this.$clone = this.$input
13965 .clone()
13966 .insertAfter( this.$input )
13967 .attr( 'aria-hidden', 'true' )
13968 .addClass( 'oo-ui-element-hidden' );
13969 }
13970
13971 this.setValidation( config.validate );
13972 this.setLabelPosition( config.labelPosition );
13973
13974 // Events
13975 this.$input.on( {
13976 keypress: this.onKeyPress.bind( this ),
13977 blur: this.onBlur.bind( this )
13978 } );
13979 this.$input.one( {
13980 focus: this.onElementAttach.bind( this )
13981 } );
13982 this.$icon.on( 'mousedown', this.onIconMouseDown.bind( this ) );
13983 this.$indicator.on( 'mousedown', this.onIndicatorMouseDown.bind( this ) );
13984 this.on( 'labelChange', this.updatePosition.bind( this ) );
13985 this.connect( this, { change: 'onChange' } );
13986
13987 // Initialization
13988 this.$element
13989 .addClass( 'oo-ui-textInputWidget' )
13990 .append( this.$icon, this.$indicator );
13991 this.setReadOnly( !!config.readOnly );
13992 if ( config.placeholder ) {
13993 this.$input.attr( 'placeholder', config.placeholder );
13994 }
13995 if ( config.maxLength !== undefined ) {
13996 this.$input.attr( 'maxlength', config.maxLength );
13997 }
13998 if ( config.autofocus ) {
13999 this.$input.attr( 'autofocus', 'autofocus' );
14000 }
14001 if ( config.required ) {
14002 this.$input.attr( 'required', 'required' );
14003 this.$input.attr( 'aria-required', 'true' );
14004 }
14005 if ( config.autocomplete === false ) {
14006 this.$input.attr( 'autocomplete', 'off' );
14007 }
14008 if ( this.multiline && config.rows ) {
14009 this.$input.attr( 'rows', config.rows );
14010 }
14011 if ( this.label || config.autosize ) {
14012 this.installParentChangeDetector();
14013 }
14014 };
14015
14016 /* Setup */
14017
14018 OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
14019 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IconElement );
14020 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IndicatorElement );
14021 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.PendingElement );
14022 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.LabelElement );
14023
14024 /* Static properties */
14025
14026 OO.ui.TextInputWidget.static.validationPatterns = {
14027 'non-empty': /.+/,
14028 integer: /^\d+$/
14029 };
14030
14031 /* Events */
14032
14033 /**
14034 * An `enter` event is emitted when the user presses 'enter' inside the text box.
14035 *
14036 * Not emitted if the input is multiline.
14037 *
14038 * @event enter
14039 */
14040
14041 /* Methods */
14042
14043 /**
14044 * Handle icon mouse down events.
14045 *
14046 * @private
14047 * @param {jQuery.Event} e Mouse down event
14048 * @fires icon
14049 */
14050 OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
14051 if ( e.which === 1 ) {
14052 this.$input[ 0 ].focus();
14053 return false;
14054 }
14055 };
14056
14057 /**
14058 * Handle indicator mouse down events.
14059 *
14060 * @private
14061 * @param {jQuery.Event} e Mouse down event
14062 * @fires indicator
14063 */
14064 OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
14065 if ( e.which === 1 ) {
14066 this.$input[ 0 ].focus();
14067 return false;
14068 }
14069 };
14070
14071 /**
14072 * Handle key press events.
14073 *
14074 * @private
14075 * @param {jQuery.Event} e Key press event
14076 * @fires enter If enter key is pressed and input is not multiline
14077 */
14078 OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
14079 if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) {
14080 this.emit( 'enter', e );
14081 }
14082 };
14083
14084 /**
14085 * Handle blur events.
14086 *
14087 * @private
14088 * @param {jQuery.Event} e Blur event
14089 */
14090 OO.ui.TextInputWidget.prototype.onBlur = function () {
14091 this.setValidityFlag();
14092 };
14093
14094 /**
14095 * Handle element attach events.
14096 *
14097 * @private
14098 * @param {jQuery.Event} e Element attach event
14099 */
14100 OO.ui.TextInputWidget.prototype.onElementAttach = function () {
14101 // Any previously calculated size is now probably invalid if we reattached elsewhere
14102 this.valCache = null;
14103 this.adjustSize();
14104 this.positionLabel();
14105 };
14106
14107 /**
14108 * Handle change events.
14109 *
14110 * @param {string} value
14111 * @private
14112 */
14113 OO.ui.TextInputWidget.prototype.onChange = function () {
14114 this.setValidityFlag();
14115 this.adjustSize();
14116 };
14117
14118 /**
14119 * Check if the input is {@link #readOnly read-only}.
14120 *
14121 * @return {boolean}
14122 */
14123 OO.ui.TextInputWidget.prototype.isReadOnly = function () {
14124 return this.readOnly;
14125 };
14126
14127 /**
14128 * Set the {@link #readOnly read-only} state of the input.
14129 *
14130 * @param {boolean} state Make input read-only
14131 * @chainable
14132 */
14133 OO.ui.TextInputWidget.prototype.setReadOnly = function ( state ) {
14134 this.readOnly = !!state;
14135 this.$input.prop( 'readOnly', this.readOnly );
14136 return this;
14137 };
14138
14139 /**
14140 * Support function for making #onElementAttach work across browsers.
14141 *
14142 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
14143 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
14144 *
14145 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
14146 * first time that the element gets attached to the documented.
14147 */
14148 OO.ui.TextInputWidget.prototype.installParentChangeDetector = function () {
14149 var mutationObserver, onRemove, topmostNode, fakeParentNode,
14150 MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
14151 widget = this;
14152
14153 if ( MutationObserver ) {
14154 // The new way. If only it wasn't so ugly.
14155
14156 if ( this.$element.closest( 'html' ).length ) {
14157 // Widget is attached already, do nothing. This breaks the functionality of this function when
14158 // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
14159 // would require observation of the whole document, which would hurt performance of other,
14160 // more important code.
14161 return;
14162 }
14163
14164 // Find topmost node in the tree
14165 topmostNode = this.$element[0];
14166 while ( topmostNode.parentNode ) {
14167 topmostNode = topmostNode.parentNode;
14168 }
14169
14170 // We have no way to detect the $element being attached somewhere without observing the entire
14171 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
14172 // parent node of $element, and instead detect when $element is removed from it (and thus
14173 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
14174 // doesn't get attached, we end up back here and create the parent.
14175
14176 mutationObserver = new MutationObserver( function ( mutations ) {
14177 var i, j, removedNodes;
14178 for ( i = 0; i < mutations.length; i++ ) {
14179 removedNodes = mutations[ i ].removedNodes;
14180 for ( j = 0; j < removedNodes.length; j++ ) {
14181 if ( removedNodes[ j ] === topmostNode ) {
14182 setTimeout( onRemove, 0 );
14183 return;
14184 }
14185 }
14186 }
14187 } );
14188
14189 onRemove = function () {
14190 // If the node was attached somewhere else, report it
14191 if ( widget.$element.closest( 'html' ).length ) {
14192 widget.onElementAttach();
14193 }
14194 mutationObserver.disconnect();
14195 widget.installParentChangeDetector();
14196 };
14197
14198 // Create a fake parent and observe it
14199 fakeParentNode = $( '<div>' ).append( topmostNode )[0];
14200 mutationObserver.observe( fakeParentNode, { childList: true } );
14201 } else {
14202 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
14203 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
14204 this.$element.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach.bind( this ) );
14205 }
14206 };
14207
14208 /**
14209 * Automatically adjust the size of the text input.
14210 *
14211 * This only affects #multiline inputs that are {@link #autosize autosized}.
14212 *
14213 * @chainable
14214 */
14215 OO.ui.TextInputWidget.prototype.adjustSize = function () {
14216 var scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError, idealHeight;
14217
14218 if ( this.multiline && this.autosize && this.$input.val() !== this.valCache ) {
14219 this.$clone
14220 .val( this.$input.val() )
14221 .attr( 'rows', this.minRows )
14222 // Set inline height property to 0 to measure scroll height
14223 .css( 'height', 0 );
14224
14225 this.$clone.removeClass( 'oo-ui-element-hidden' );
14226
14227 this.valCache = this.$input.val();
14228
14229 scrollHeight = this.$clone[ 0 ].scrollHeight;
14230
14231 // Remove inline height property to measure natural heights
14232 this.$clone.css( 'height', '' );
14233 innerHeight = this.$clone.innerHeight();
14234 outerHeight = this.$clone.outerHeight();
14235
14236 // Measure max rows height
14237 this.$clone
14238 .attr( 'rows', this.maxRows )
14239 .css( 'height', 'auto' )
14240 .val( '' );
14241 maxInnerHeight = this.$clone.innerHeight();
14242
14243 // Difference between reported innerHeight and scrollHeight with no scrollbars present
14244 // Equals 1 on Blink-based browsers and 0 everywhere else
14245 measurementError = maxInnerHeight - this.$clone[ 0 ].scrollHeight;
14246 idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
14247
14248 this.$clone.addClass( 'oo-ui-element-hidden' );
14249
14250 // Only apply inline height when expansion beyond natural height is needed
14251 if ( idealHeight > innerHeight ) {
14252 // Use the difference between the inner and outer height as a buffer
14253 this.$input.css( 'height', idealHeight + ( outerHeight - innerHeight ) );
14254 } else {
14255 this.$input.css( 'height', '' );
14256 }
14257 }
14258 return this;
14259 };
14260
14261 /**
14262 * @inheritdoc
14263 * @protected
14264 */
14265 OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
14266 var type = [ 'text', 'password', 'search', 'email', 'url' ].indexOf( config.type ) !== -1 ?
14267 config.type :
14268 'text';
14269 return config.multiline ? $( '<textarea>' ) : $( '<input type="' + type + '" />' );
14270 };
14271
14272 /**
14273 * Check if the input supports multiple lines.
14274 *
14275 * @return {boolean}
14276 */
14277 OO.ui.TextInputWidget.prototype.isMultiline = function () {
14278 return !!this.multiline;
14279 };
14280
14281 /**
14282 * Check if the input automatically adjusts its size.
14283 *
14284 * @return {boolean}
14285 */
14286 OO.ui.TextInputWidget.prototype.isAutosizing = function () {
14287 return !!this.autosize;
14288 };
14289
14290 /**
14291 * Select the entire text of the input.
14292 *
14293 * @chainable
14294 */
14295 OO.ui.TextInputWidget.prototype.select = function () {
14296 this.$input.select();
14297 return this;
14298 };
14299
14300 /**
14301 * Set the validation pattern.
14302 *
14303 * The validation pattern is either a regular expression, a function, or the symbolic name of a
14304 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
14305 * value must contain only numbers).
14306 *
14307 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
14308 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
14309 */
14310 OO.ui.TextInputWidget.prototype.setValidation = function ( validate ) {
14311 if ( validate instanceof RegExp || validate instanceof Function ) {
14312 this.validate = validate;
14313 } else {
14314 this.validate = this.constructor.static.validationPatterns[ validate ] || /.*/;
14315 }
14316 };
14317
14318 /**
14319 * Sets the 'invalid' flag appropriately.
14320 *
14321 * @param {boolean} [isValid] Optionally override validation result
14322 */
14323 OO.ui.TextInputWidget.prototype.setValidityFlag = function ( isValid ) {
14324 var widget = this,
14325 setFlag = function ( valid ) {
14326 if ( !valid ) {
14327 widget.$input.attr( 'aria-invalid', 'true' );
14328 } else {
14329 widget.$input.removeAttr( 'aria-invalid' );
14330 }
14331 widget.setFlags( { invalid: !valid } );
14332 };
14333
14334 if ( isValid !== undefined ) {
14335 setFlag( isValid );
14336 } else {
14337 this.isValid().done( setFlag );
14338 }
14339 };
14340
14341 /**
14342 * Check if a value is valid.
14343 *
14344 * This method returns a promise that resolves with a boolean `true` if the current value is
14345 * considered valid according to the supplied {@link #validate validation pattern}.
14346 *
14347 * @return {jQuery.Promise} A promise that resolves to a boolean `true` if the value is valid.
14348 */
14349 OO.ui.TextInputWidget.prototype.isValid = function () {
14350 if ( this.validate instanceof Function ) {
14351 var result = this.validate( this.getValue() );
14352 if ( $.isFunction( result.promise ) ) {
14353 return result.promise();
14354 } else {
14355 return $.Deferred().resolve( !!result ).promise();
14356 }
14357 } else {
14358 return $.Deferred().resolve( !!this.getValue().match( this.validate ) ).promise();
14359 }
14360 };
14361
14362 /**
14363 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
14364 *
14365 * @param {string} labelPosition Label position, 'before' or 'after'
14366 * @chainable
14367 */
14368 OO.ui.TextInputWidget.prototype.setLabelPosition = function ( labelPosition ) {
14369 this.labelPosition = labelPosition;
14370 this.updatePosition();
14371 return this;
14372 };
14373
14374 /**
14375 * Deprecated alias of #setLabelPosition
14376 *
14377 * @deprecated Use setLabelPosition instead.
14378 */
14379 OO.ui.TextInputWidget.prototype.setPosition =
14380 OO.ui.TextInputWidget.prototype.setLabelPosition;
14381
14382 /**
14383 * Update the position of the inline label.
14384 *
14385 * This method is called by #setLabelPosition, and can also be called on its own if
14386 * something causes the label to be mispositioned.
14387 *
14388 *
14389 * @chainable
14390 */
14391 OO.ui.TextInputWidget.prototype.updatePosition = function () {
14392 var after = this.labelPosition === 'after';
14393
14394 this.$element
14395 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label && after )
14396 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label && !after );
14397
14398 if ( this.label ) {
14399 this.positionLabel();
14400 }
14401
14402 return this;
14403 };
14404
14405 /**
14406 * Position the label by setting the correct padding on the input.
14407 *
14408 * @private
14409 * @chainable
14410 */
14411 OO.ui.TextInputWidget.prototype.positionLabel = function () {
14412 // Clear old values
14413 this.$input
14414 // Clear old values if present
14415 .css( {
14416 'padding-right': '',
14417 'padding-left': ''
14418 } );
14419
14420 if ( this.label ) {
14421 this.$element.append( this.$label );
14422 } else {
14423 this.$label.detach();
14424 return;
14425 }
14426
14427 var after = this.labelPosition === 'after',
14428 rtl = this.$element.css( 'direction' ) === 'rtl',
14429 property = after === rtl ? 'padding-left' : 'padding-right';
14430
14431 this.$input.css( property, this.$label.outerWidth( true ) );
14432
14433 return this;
14434 };
14435
14436 /**
14437 * ComboBoxWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
14438 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
14439 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
14440 *
14441 * - by typing a value in the text input field. If the value exactly matches the value of a menu
14442 * option, that option will appear to be selected.
14443 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
14444 * input field.
14445 *
14446 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
14447 *
14448 * @example
14449 * // Example: A ComboBoxWidget.
14450 * var comboBox = new OO.ui.ComboBoxWidget( {
14451 * label: 'ComboBoxWidget',
14452 * input: { value: 'Option One' },
14453 * menu: {
14454 * items: [
14455 * new OO.ui.MenuOptionWidget( {
14456 * data: 'Option 1',
14457 * label: 'Option One'
14458 * } ),
14459 * new OO.ui.MenuOptionWidget( {
14460 * data: 'Option 2',
14461 * label: 'Option Two'
14462 * } ),
14463 * new OO.ui.MenuOptionWidget( {
14464 * data: 'Option 3',
14465 * label: 'Option Three'
14466 * } ),
14467 * new OO.ui.MenuOptionWidget( {
14468 * data: 'Option 4',
14469 * label: 'Option Four'
14470 * } ),
14471 * new OO.ui.MenuOptionWidget( {
14472 * data: 'Option 5',
14473 * label: 'Option Five'
14474 * } )
14475 * ]
14476 * }
14477 * } );
14478 * $( 'body' ).append( comboBox.$element );
14479 *
14480 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
14481 *
14482 * @class
14483 * @extends OO.ui.Widget
14484 * @mixins OO.ui.mixin.TabIndexedElement
14485 *
14486 * @constructor
14487 * @param {Object} [config] Configuration options
14488 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}.
14489 * @cfg {Object} [input] Configuration options to pass to the {@link OO.ui.TextInputWidget text input widget}.
14490 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
14491 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
14492 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
14493 */
14494 OO.ui.ComboBoxWidget = function OoUiComboBoxWidget( config ) {
14495 // Configuration initialization
14496 config = config || {};
14497
14498 // Parent constructor
14499 OO.ui.ComboBoxWidget.parent.call( this, config );
14500
14501 // Properties (must be set before TabIndexedElement constructor call)
14502 this.$indicator = this.$( '<span>' );
14503
14504 // Mixin constructors
14505 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$indicator } ) );
14506
14507 // Properties
14508 this.$overlay = config.$overlay || this.$element;
14509 this.input = new OO.ui.TextInputWidget( $.extend(
14510 {
14511 indicator: 'down',
14512 $indicator: this.$indicator,
14513 disabled: this.isDisabled()
14514 },
14515 config.input
14516 ) );
14517 this.input.$input.eq( 0 ).attr( {
14518 role: 'combobox',
14519 'aria-autocomplete': 'list'
14520 } );
14521 this.menu = new OO.ui.TextInputMenuSelectWidget( this.input, $.extend(
14522 {
14523 widget: this,
14524 input: this.input,
14525 disabled: this.isDisabled()
14526 },
14527 config.menu
14528 ) );
14529
14530 // Events
14531 this.$indicator.on( {
14532 click: this.onClick.bind( this ),
14533 keypress: this.onKeyPress.bind( this )
14534 } );
14535 this.input.connect( this, {
14536 change: 'onInputChange',
14537 enter: 'onInputEnter'
14538 } );
14539 this.menu.connect( this, {
14540 choose: 'onMenuChoose',
14541 add: 'onMenuItemsChange',
14542 remove: 'onMenuItemsChange'
14543 } );
14544
14545 // Initialization
14546 this.$element.addClass( 'oo-ui-comboBoxWidget' ).append( this.input.$element );
14547 this.$overlay.append( this.menu.$element );
14548 this.onMenuItemsChange();
14549 };
14550
14551 /* Setup */
14552
14553 OO.inheritClass( OO.ui.ComboBoxWidget, OO.ui.Widget );
14554 OO.mixinClass( OO.ui.ComboBoxWidget, OO.ui.mixin.TabIndexedElement );
14555
14556 /* Methods */
14557
14558 /**
14559 * Get the combobox's menu.
14560 * @return {OO.ui.TextInputMenuSelectWidget} Menu widget
14561 */
14562 OO.ui.ComboBoxWidget.prototype.getMenu = function () {
14563 return this.menu;
14564 };
14565
14566 /**
14567 * Get the combobox's text input widget.
14568 * @return {OO.ui.TextInputWidget} Text input widget
14569 */
14570 OO.ui.ComboBoxWidget.prototype.getInput = function () {
14571 return this.input;
14572 };
14573
14574 /**
14575 * Handle input change events.
14576 *
14577 * @private
14578 * @param {string} value New value
14579 */
14580 OO.ui.ComboBoxWidget.prototype.onInputChange = function ( value ) {
14581 var match = this.menu.getItemFromData( value );
14582
14583 this.menu.selectItem( match );
14584 if ( this.menu.getHighlightedItem() ) {
14585 this.menu.highlightItem( match );
14586 }
14587
14588 if ( !this.isDisabled() ) {
14589 this.menu.toggle( true );
14590 }
14591 };
14592
14593 /**
14594 * Handle mouse click events.
14595 *
14596 *
14597 * @private
14598 * @param {jQuery.Event} e Mouse click event
14599 */
14600 OO.ui.ComboBoxWidget.prototype.onClick = function ( e ) {
14601 if ( !this.isDisabled() && e.which === 1 ) {
14602 this.menu.toggle();
14603 this.input.$input[ 0 ].focus();
14604 }
14605 return false;
14606 };
14607
14608 /**
14609 * Handle key press events.
14610 *
14611 *
14612 * @private
14613 * @param {jQuery.Event} e Key press event
14614 */
14615 OO.ui.ComboBoxWidget.prototype.onKeyPress = function ( e ) {
14616 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
14617 this.menu.toggle();
14618 this.input.$input[ 0 ].focus();
14619 return false;
14620 }
14621 };
14622
14623 /**
14624 * Handle input enter events.
14625 *
14626 * @private
14627 */
14628 OO.ui.ComboBoxWidget.prototype.onInputEnter = function () {
14629 if ( !this.isDisabled() ) {
14630 this.menu.toggle( false );
14631 }
14632 };
14633
14634 /**
14635 * Handle menu choose events.
14636 *
14637 * @private
14638 * @param {OO.ui.OptionWidget} item Chosen item
14639 */
14640 OO.ui.ComboBoxWidget.prototype.onMenuChoose = function ( item ) {
14641 this.input.setValue( item.getData() );
14642 };
14643
14644 /**
14645 * Handle menu item change events.
14646 *
14647 * @private
14648 */
14649 OO.ui.ComboBoxWidget.prototype.onMenuItemsChange = function () {
14650 var match = this.menu.getItemFromData( this.input.getValue() );
14651 this.menu.selectItem( match );
14652 if ( this.menu.getHighlightedItem() ) {
14653 this.menu.highlightItem( match );
14654 }
14655 this.$element.toggleClass( 'oo-ui-comboBoxWidget-empty', this.menu.isEmpty() );
14656 };
14657
14658 /**
14659 * @inheritdoc
14660 */
14661 OO.ui.ComboBoxWidget.prototype.setDisabled = function ( disabled ) {
14662 // Parent method
14663 OO.ui.ComboBoxWidget.parent.prototype.setDisabled.call( this, disabled );
14664
14665 if ( this.input ) {
14666 this.input.setDisabled( this.isDisabled() );
14667 }
14668 if ( this.menu ) {
14669 this.menu.setDisabled( this.isDisabled() );
14670 }
14671
14672 return this;
14673 };
14674
14675 /**
14676 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
14677 * be configured with a `label` option that is set to a string, a label node, or a function:
14678 *
14679 * - String: a plaintext string
14680 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
14681 * label that includes a link or special styling, such as a gray color or additional graphical elements.
14682 * - Function: a function that will produce a string in the future. Functions are used
14683 * in cases where the value of the label is not currently defined.
14684 *
14685 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
14686 * will come into focus when the label is clicked.
14687 *
14688 * @example
14689 * // Examples of LabelWidgets
14690 * var label1 = new OO.ui.LabelWidget( {
14691 * label: 'plaintext label'
14692 * } );
14693 * var label2 = new OO.ui.LabelWidget( {
14694 * label: $( '<a href="default.html">jQuery label</a>' )
14695 * } );
14696 * // Create a fieldset layout with fields for each example
14697 * var fieldset = new OO.ui.FieldsetLayout();
14698 * fieldset.addItems( [
14699 * new OO.ui.FieldLayout( label1 ),
14700 * new OO.ui.FieldLayout( label2 )
14701 * ] );
14702 * $( 'body' ).append( fieldset.$element );
14703 *
14704 *
14705 * @class
14706 * @extends OO.ui.Widget
14707 * @mixins OO.ui.mixin.LabelElement
14708 *
14709 * @constructor
14710 * @param {Object} [config] Configuration options
14711 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
14712 * Clicking the label will focus the specified input field.
14713 */
14714 OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
14715 // Configuration initialization
14716 config = config || {};
14717
14718 // Parent constructor
14719 OO.ui.LabelWidget.parent.call( this, config );
14720
14721 // Mixin constructors
14722 OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, { $label: this.$element } ) );
14723 OO.ui.mixin.TitledElement.call( this, config );
14724
14725 // Properties
14726 this.input = config.input;
14727
14728 // Events
14729 if ( this.input instanceof OO.ui.InputWidget ) {
14730 this.$element.on( 'click', this.onClick.bind( this ) );
14731 }
14732
14733 // Initialization
14734 this.$element.addClass( 'oo-ui-labelWidget' );
14735 };
14736
14737 /* Setup */
14738
14739 OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
14740 OO.mixinClass( OO.ui.LabelWidget, OO.ui.mixin.LabelElement );
14741 OO.mixinClass( OO.ui.LabelWidget, OO.ui.mixin.TitledElement );
14742
14743 /* Static Properties */
14744
14745 OO.ui.LabelWidget.static.tagName = 'span';
14746
14747 /* Methods */
14748
14749 /**
14750 * Handles label mouse click events.
14751 *
14752 * @private
14753 * @param {jQuery.Event} e Mouse click event
14754 */
14755 OO.ui.LabelWidget.prototype.onClick = function () {
14756 this.input.simulateLabelClick();
14757 return false;
14758 };
14759
14760 /**
14761 * OptionWidgets are special elements that can be selected and configured with data. The
14762 * data is often unique for each option, but it does not have to be. OptionWidgets are used
14763 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
14764 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
14765 *
14766 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
14767 *
14768 * @class
14769 * @extends OO.ui.Widget
14770 * @mixins OO.ui.mixin.LabelElement
14771 * @mixins OO.ui.mixin.FlaggedElement
14772 *
14773 * @constructor
14774 * @param {Object} [config] Configuration options
14775 */
14776 OO.ui.OptionWidget = function OoUiOptionWidget( config ) {
14777 // Configuration initialization
14778 config = config || {};
14779
14780 // Parent constructor
14781 OO.ui.OptionWidget.parent.call( this, config );
14782
14783 // Mixin constructors
14784 OO.ui.mixin.ItemWidget.call( this );
14785 OO.ui.mixin.LabelElement.call( this, config );
14786 OO.ui.mixin.FlaggedElement.call( this, config );
14787
14788 // Properties
14789 this.selected = false;
14790 this.highlighted = false;
14791 this.pressed = false;
14792
14793 // Initialization
14794 this.$element
14795 .data( 'oo-ui-optionWidget', this )
14796 .attr( 'role', 'option' )
14797 .attr( 'aria-selected', 'false' )
14798 .addClass( 'oo-ui-optionWidget' )
14799 .append( this.$label );
14800 };
14801
14802 /* Setup */
14803
14804 OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
14805 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.ItemWidget );
14806 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.LabelElement );
14807 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.FlaggedElement );
14808
14809 /* Static Properties */
14810
14811 OO.ui.OptionWidget.static.selectable = true;
14812
14813 OO.ui.OptionWidget.static.highlightable = true;
14814
14815 OO.ui.OptionWidget.static.pressable = true;
14816
14817 OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
14818
14819 /* Methods */
14820
14821 /**
14822 * Check if the option can be selected.
14823 *
14824 * @return {boolean} Item is selectable
14825 */
14826 OO.ui.OptionWidget.prototype.isSelectable = function () {
14827 return this.constructor.static.selectable && !this.isDisabled();
14828 };
14829
14830 /**
14831 * Check if the option can be highlighted. A highlight indicates that the option
14832 * may be selected when a user presses enter or clicks. Disabled items cannot
14833 * be highlighted.
14834 *
14835 * @return {boolean} Item is highlightable
14836 */
14837 OO.ui.OptionWidget.prototype.isHighlightable = function () {
14838 return this.constructor.static.highlightable && !this.isDisabled();
14839 };
14840
14841 /**
14842 * Check if the option can be pressed. The pressed state occurs when a user mouses
14843 * down on an item, but has not yet let go of the mouse.
14844 *
14845 * @return {boolean} Item is pressable
14846 */
14847 OO.ui.OptionWidget.prototype.isPressable = function () {
14848 return this.constructor.static.pressable && !this.isDisabled();
14849 };
14850
14851 /**
14852 * Check if the option is selected.
14853 *
14854 * @return {boolean} Item is selected
14855 */
14856 OO.ui.OptionWidget.prototype.isSelected = function () {
14857 return this.selected;
14858 };
14859
14860 /**
14861 * Check if the option is highlighted. A highlight indicates that the
14862 * item may be selected when a user presses enter or clicks.
14863 *
14864 * @return {boolean} Item is highlighted
14865 */
14866 OO.ui.OptionWidget.prototype.isHighlighted = function () {
14867 return this.highlighted;
14868 };
14869
14870 /**
14871 * Check if the option is pressed. The pressed state occurs when a user mouses
14872 * down on an item, but has not yet let go of the mouse. The item may appear
14873 * selected, but it will not be selected until the user releases the mouse.
14874 *
14875 * @return {boolean} Item is pressed
14876 */
14877 OO.ui.OptionWidget.prototype.isPressed = function () {
14878 return this.pressed;
14879 };
14880
14881 /**
14882 * Set the option’s selected state. In general, all modifications to the selection
14883 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
14884 * method instead of this method.
14885 *
14886 * @param {boolean} [state=false] Select option
14887 * @chainable
14888 */
14889 OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
14890 if ( this.constructor.static.selectable ) {
14891 this.selected = !!state;
14892 this.$element
14893 .toggleClass( 'oo-ui-optionWidget-selected', state )
14894 .attr( 'aria-selected', state.toString() );
14895 if ( state && this.constructor.static.scrollIntoViewOnSelect ) {
14896 this.scrollElementIntoView();
14897 }
14898 this.updateThemeClasses();
14899 }
14900 return this;
14901 };
14902
14903 /**
14904 * Set the option’s highlighted state. In general, all programmatic
14905 * modifications to the highlight should be handled by the
14906 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
14907 * method instead of this method.
14908 *
14909 * @param {boolean} [state=false] Highlight option
14910 * @chainable
14911 */
14912 OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
14913 if ( this.constructor.static.highlightable ) {
14914 this.highlighted = !!state;
14915 this.$element.toggleClass( 'oo-ui-optionWidget-highlighted', state );
14916 this.updateThemeClasses();
14917 }
14918 return this;
14919 };
14920
14921 /**
14922 * Set the option’s pressed state. In general, all
14923 * programmatic modifications to the pressed state should be handled by the
14924 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
14925 * method instead of this method.
14926 *
14927 * @param {boolean} [state=false] Press option
14928 * @chainable
14929 */
14930 OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
14931 if ( this.constructor.static.pressable ) {
14932 this.pressed = !!state;
14933 this.$element.toggleClass( 'oo-ui-optionWidget-pressed', state );
14934 this.updateThemeClasses();
14935 }
14936 return this;
14937 };
14938
14939 /**
14940 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
14941 * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
14942 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
14943 * options. For more information about options and selects, please see the
14944 * [OOjs UI documentation on MediaWiki][1].
14945 *
14946 * @example
14947 * // Decorated options in a select widget
14948 * var select = new OO.ui.SelectWidget( {
14949 * items: [
14950 * new OO.ui.DecoratedOptionWidget( {
14951 * data: 'a',
14952 * label: 'Option with icon',
14953 * icon: 'help'
14954 * } ),
14955 * new OO.ui.DecoratedOptionWidget( {
14956 * data: 'b',
14957 * label: 'Option with indicator',
14958 * indicator: 'next'
14959 * } )
14960 * ]
14961 * } );
14962 * $( 'body' ).append( select.$element );
14963 *
14964 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
14965 *
14966 * @class
14967 * @extends OO.ui.OptionWidget
14968 * @mixins OO.ui.mixin.IconElement
14969 * @mixins OO.ui.mixin.IndicatorElement
14970 *
14971 * @constructor
14972 * @param {Object} [config] Configuration options
14973 */
14974 OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( config ) {
14975 // Parent constructor
14976 OO.ui.DecoratedOptionWidget.parent.call( this, config );
14977
14978 // Mixin constructors
14979 OO.ui.mixin.IconElement.call( this, config );
14980 OO.ui.mixin.IndicatorElement.call( this, config );
14981
14982 // Initialization
14983 this.$element
14984 .addClass( 'oo-ui-decoratedOptionWidget' )
14985 .prepend( this.$icon )
14986 .append( this.$indicator );
14987 };
14988
14989 /* Setup */
14990
14991 OO.inheritClass( OO.ui.DecoratedOptionWidget, OO.ui.OptionWidget );
14992 OO.mixinClass( OO.ui.DecoratedOptionWidget, OO.ui.mixin.IconElement );
14993 OO.mixinClass( OO.ui.DecoratedOptionWidget, OO.ui.mixin.IndicatorElement );
14994
14995 /**
14996 * ButtonOptionWidget is a special type of {@link OO.ui.mixin.ButtonElement button element} that
14997 * can be selected and configured with data. The class is
14998 * used with OO.ui.ButtonSelectWidget to create a selection of button options. Please see the
14999 * [OOjs UI documentation on MediaWiki] [1] for more information.
15000 *
15001 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_options
15002 *
15003 * @class
15004 * @extends OO.ui.DecoratedOptionWidget
15005 * @mixins OO.ui.mixin.ButtonElement
15006 * @mixins OO.ui.mixin.TabIndexedElement
15007 *
15008 * @constructor
15009 * @param {Object} [config] Configuration options
15010 */
15011 OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( config ) {
15012 // Configuration initialization
15013 config = $.extend( { tabIndex: -1 }, config );
15014
15015 // Parent constructor
15016 OO.ui.ButtonOptionWidget.parent.call( this, config );
15017
15018 // Mixin constructors
15019 OO.ui.mixin.ButtonElement.call( this, config );
15020 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
15021
15022 // Initialization
15023 this.$element.addClass( 'oo-ui-buttonOptionWidget' );
15024 this.$button.append( this.$element.contents() );
15025 this.$element.append( this.$button );
15026 };
15027
15028 /* Setup */
15029
15030 OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.DecoratedOptionWidget );
15031 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.mixin.ButtonElement );
15032 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.mixin.TabIndexedElement );
15033
15034 /* Static Properties */
15035
15036 // Allow button mouse down events to pass through so they can be handled by the parent select widget
15037 OO.ui.ButtonOptionWidget.static.cancelButtonMouseDownEvents = false;
15038
15039 OO.ui.ButtonOptionWidget.static.highlightable = false;
15040
15041 /* Methods */
15042
15043 /**
15044 * @inheritdoc
15045 */
15046 OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) {
15047 OO.ui.ButtonOptionWidget.parent.prototype.setSelected.call( this, state );
15048
15049 if ( this.constructor.static.selectable ) {
15050 this.setActive( state );
15051 }
15052
15053 return this;
15054 };
15055
15056 /**
15057 * RadioOptionWidget is an option widget that looks like a radio button.
15058 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
15059 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
15060 *
15061 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
15062 *
15063 * @class
15064 * @extends OO.ui.OptionWidget
15065 *
15066 * @constructor
15067 * @param {Object} [config] Configuration options
15068 */
15069 OO.ui.RadioOptionWidget = function OoUiRadioOptionWidget( config ) {
15070 // Configuration initialization
15071 config = config || {};
15072
15073 // Properties (must be done before parent constructor which calls #setDisabled)
15074 this.radio = new OO.ui.RadioInputWidget( { value: config.data, tabIndex: -1 } );
15075
15076 // Parent constructor
15077 OO.ui.RadioOptionWidget.parent.call( this, config );
15078
15079 // Events
15080 this.radio.$input.on( 'focus', this.onInputFocus.bind( this ) );
15081
15082 // Initialization
15083 // Remove implicit role, we're handling it ourselves
15084 this.radio.$input.attr( 'role', 'presentation' );
15085 this.$element
15086 .addClass( 'oo-ui-radioOptionWidget' )
15087 .attr( 'role', 'radio' )
15088 .attr( 'aria-checked', 'false' )
15089 .removeAttr( 'aria-selected' )
15090 .prepend( this.radio.$element );
15091 };
15092
15093 /* Setup */
15094
15095 OO.inheritClass( OO.ui.RadioOptionWidget, OO.ui.OptionWidget );
15096
15097 /* Static Properties */
15098
15099 OO.ui.RadioOptionWidget.static.highlightable = false;
15100
15101 OO.ui.RadioOptionWidget.static.scrollIntoViewOnSelect = true;
15102
15103 OO.ui.RadioOptionWidget.static.pressable = false;
15104
15105 OO.ui.RadioOptionWidget.static.tagName = 'label';
15106
15107 /* Methods */
15108
15109 /**
15110 * @param {jQuery.Event} e Focus event
15111 * @private
15112 */
15113 OO.ui.RadioOptionWidget.prototype.onInputFocus = function () {
15114 this.radio.$input.blur();
15115 this.$element.parent().focus();
15116 };
15117
15118 /**
15119 * @inheritdoc
15120 */
15121 OO.ui.RadioOptionWidget.prototype.setSelected = function ( state ) {
15122 OO.ui.RadioOptionWidget.parent.prototype.setSelected.call( this, state );
15123
15124 this.radio.setSelected( state );
15125 this.$element
15126 .attr( 'aria-checked', state.toString() )
15127 .removeAttr( 'aria-selected' );
15128
15129 return this;
15130 };
15131
15132 /**
15133 * @inheritdoc
15134 */
15135 OO.ui.RadioOptionWidget.prototype.setDisabled = function ( disabled ) {
15136 OO.ui.RadioOptionWidget.parent.prototype.setDisabled.call( this, disabled );
15137
15138 this.radio.setDisabled( this.isDisabled() );
15139
15140 return this;
15141 };
15142
15143 /**
15144 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
15145 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
15146 * the [OOjs UI documentation on MediaWiki] [1] for more information.
15147 *
15148 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
15149 *
15150 * @class
15151 * @extends OO.ui.DecoratedOptionWidget
15152 *
15153 * @constructor
15154 * @param {Object} [config] Configuration options
15155 */
15156 OO.ui.MenuOptionWidget = function OoUiMenuOptionWidget( config ) {
15157 // Configuration initialization
15158 config = $.extend( { icon: 'check' }, config );
15159
15160 // Parent constructor
15161 OO.ui.MenuOptionWidget.parent.call( this, config );
15162
15163 // Initialization
15164 this.$element
15165 .attr( 'role', 'menuitem' )
15166 .addClass( 'oo-ui-menuOptionWidget' );
15167 };
15168
15169 /* Setup */
15170
15171 OO.inheritClass( OO.ui.MenuOptionWidget, OO.ui.DecoratedOptionWidget );
15172
15173 /* Static Properties */
15174
15175 OO.ui.MenuOptionWidget.static.scrollIntoViewOnSelect = true;
15176
15177 /**
15178 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
15179 * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
15180 *
15181 * @example
15182 * var myDropdown = new OO.ui.DropdownWidget( {
15183 * menu: {
15184 * items: [
15185 * new OO.ui.MenuSectionOptionWidget( {
15186 * label: 'Dogs'
15187 * } ),
15188 * new OO.ui.MenuOptionWidget( {
15189 * data: 'corgi',
15190 * label: 'Welsh Corgi'
15191 * } ),
15192 * new OO.ui.MenuOptionWidget( {
15193 * data: 'poodle',
15194 * label: 'Standard Poodle'
15195 * } ),
15196 * new OO.ui.MenuSectionOptionWidget( {
15197 * label: 'Cats'
15198 * } ),
15199 * new OO.ui.MenuOptionWidget( {
15200 * data: 'lion',
15201 * label: 'Lion'
15202 * } )
15203 * ]
15204 * }
15205 * } );
15206 * $( 'body' ).append( myDropdown.$element );
15207 *
15208 *
15209 * @class
15210 * @extends OO.ui.DecoratedOptionWidget
15211 *
15212 * @constructor
15213 * @param {Object} [config] Configuration options
15214 */
15215 OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( config ) {
15216 // Parent constructor
15217 OO.ui.MenuSectionOptionWidget.parent.call( this, config );
15218
15219 // Initialization
15220 this.$element.addClass( 'oo-ui-menuSectionOptionWidget' );
15221 };
15222
15223 /* Setup */
15224
15225 OO.inheritClass( OO.ui.MenuSectionOptionWidget, OO.ui.DecoratedOptionWidget );
15226
15227 /* Static Properties */
15228
15229 OO.ui.MenuSectionOptionWidget.static.selectable = false;
15230
15231 OO.ui.MenuSectionOptionWidget.static.highlightable = false;
15232
15233 /**
15234 * OutlineOptionWidget is an item in an {@link OO.ui.OutlineSelectWidget OutlineSelectWidget}.
15235 *
15236 * Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}, which contain
15237 * {@link OO.ui.PageLayout page layouts}. See {@link OO.ui.BookletLayout BookletLayout}
15238 * for an example.
15239 *
15240 * @class
15241 * @extends OO.ui.DecoratedOptionWidget
15242 *
15243 * @constructor
15244 * @param {Object} [config] Configuration options
15245 * @cfg {number} [level] Indentation level
15246 * @cfg {boolean} [movable] Allow modification from {@link OO.ui.OutlineControlsWidget outline controls}.
15247 */
15248 OO.ui.OutlineOptionWidget = function OoUiOutlineOptionWidget( config ) {
15249 // Configuration initialization
15250 config = config || {};
15251
15252 // Parent constructor
15253 OO.ui.OutlineOptionWidget.parent.call( this, config );
15254
15255 // Properties
15256 this.level = 0;
15257 this.movable = !!config.movable;
15258 this.removable = !!config.removable;
15259
15260 // Initialization
15261 this.$element.addClass( 'oo-ui-outlineOptionWidget' );
15262 this.setLevel( config.level );
15263 };
15264
15265 /* Setup */
15266
15267 OO.inheritClass( OO.ui.OutlineOptionWidget, OO.ui.DecoratedOptionWidget );
15268
15269 /* Static Properties */
15270
15271 OO.ui.OutlineOptionWidget.static.highlightable = false;
15272
15273 OO.ui.OutlineOptionWidget.static.scrollIntoViewOnSelect = true;
15274
15275 OO.ui.OutlineOptionWidget.static.levelClass = 'oo-ui-outlineOptionWidget-level-';
15276
15277 OO.ui.OutlineOptionWidget.static.levels = 3;
15278
15279 /* Methods */
15280
15281 /**
15282 * Check if item is movable.
15283 *
15284 * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
15285 *
15286 * @return {boolean} Item is movable
15287 */
15288 OO.ui.OutlineOptionWidget.prototype.isMovable = function () {
15289 return this.movable;
15290 };
15291
15292 /**
15293 * Check if item is removable.
15294 *
15295 * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
15296 *
15297 * @return {boolean} Item is removable
15298 */
15299 OO.ui.OutlineOptionWidget.prototype.isRemovable = function () {
15300 return this.removable;
15301 };
15302
15303 /**
15304 * Get indentation level.
15305 *
15306 * @return {number} Indentation level
15307 */
15308 OO.ui.OutlineOptionWidget.prototype.getLevel = function () {
15309 return this.level;
15310 };
15311
15312 /**
15313 * Set movability.
15314 *
15315 * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
15316 *
15317 * @param {boolean} movable Item is movable
15318 * @chainable
15319 */
15320 OO.ui.OutlineOptionWidget.prototype.setMovable = function ( movable ) {
15321 this.movable = !!movable;
15322 this.updateThemeClasses();
15323 return this;
15324 };
15325
15326 /**
15327 * Set removability.
15328 *
15329 * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
15330 *
15331 * @param {boolean} movable Item is removable
15332 * @chainable
15333 */
15334 OO.ui.OutlineOptionWidget.prototype.setRemovable = function ( removable ) {
15335 this.removable = !!removable;
15336 this.updateThemeClasses();
15337 return this;
15338 };
15339
15340 /**
15341 * Set indentation level.
15342 *
15343 * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
15344 * @chainable
15345 */
15346 OO.ui.OutlineOptionWidget.prototype.setLevel = function ( level ) {
15347 var levels = this.constructor.static.levels,
15348 levelClass = this.constructor.static.levelClass,
15349 i = levels;
15350
15351 this.level = level ? Math.max( 0, Math.min( levels - 1, level ) ) : 0;
15352 while ( i-- ) {
15353 if ( this.level === i ) {
15354 this.$element.addClass( levelClass + i );
15355 } else {
15356 this.$element.removeClass( levelClass + i );
15357 }
15358 }
15359 this.updateThemeClasses();
15360
15361 return this;
15362 };
15363
15364 /**
15365 * TabOptionWidget is an item in a {@link OO.ui.TabSelectWidget TabSelectWidget}.
15366 *
15367 * Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}, which contain
15368 * {@link OO.ui.CardLayout card layouts}. See {@link OO.ui.IndexLayout IndexLayout}
15369 * for an example.
15370 *
15371 * @class
15372 * @extends OO.ui.OptionWidget
15373 *
15374 * @constructor
15375 * @param {Object} [config] Configuration options
15376 */
15377 OO.ui.TabOptionWidget = function OoUiTabOptionWidget( config ) {
15378 // Configuration initialization
15379 config = config || {};
15380
15381 // Parent constructor
15382 OO.ui.TabOptionWidget.parent.call( this, config );
15383
15384 // Initialization
15385 this.$element.addClass( 'oo-ui-tabOptionWidget' );
15386 };
15387
15388 /* Setup */
15389
15390 OO.inheritClass( OO.ui.TabOptionWidget, OO.ui.OptionWidget );
15391
15392 /* Static Properties */
15393
15394 OO.ui.TabOptionWidget.static.highlightable = false;
15395
15396 /**
15397 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
15398 * By default, each popup has an anchor that points toward its origin.
15399 * Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples.
15400 *
15401 * @example
15402 * // A popup widget.
15403 * var popup = new OO.ui.PopupWidget( {
15404 * $content: $( '<p>Hi there!</p>' ),
15405 * padded: true,
15406 * width: 300
15407 * } );
15408 *
15409 * $( 'body' ).append( popup.$element );
15410 * // To display the popup, toggle the visibility to 'true'.
15411 * popup.toggle( true );
15412 *
15413 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups
15414 *
15415 * @class
15416 * @extends OO.ui.Widget
15417 * @mixins OO.ui.mixin.LabelElement
15418 *
15419 * @constructor
15420 * @param {Object} [config] Configuration options
15421 * @cfg {number} [width=320] Width of popup in pixels
15422 * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
15423 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
15424 * @cfg {string} [align='center'] Alignment of the popup: `center`, `force-left`, `force-right`, `backwards` or `forwards`.
15425 * If the popup is forced-left the popup body is leaning towards the left. For force-right alignment, the body of the
15426 * popup is leaning towards the right of the screen.
15427 * Using 'backwards' is a logical direction which will result in the popup leaning towards the beginning of the sentence
15428 * in the given language, which means it will flip to the correct positioning in right-to-left languages.
15429 * Using 'forward' will also result in a logical alignment where the body of the popup leans towards the end of the
15430 * sentence in the given language.
15431 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
15432 * See the [OOjs UI docs on MediaWiki][3] for an example.
15433 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
15434 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
15435 * @cfg {jQuery} [$content] Content to append to the popup's body
15436 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
15437 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
15438 * This config option is only relevant if #autoClose is set to `true`. See the [OOjs UI docs on MediaWiki][2]
15439 * for an example.
15440 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#autocloseExample
15441 * @cfg {boolean} [head] Show a popup header that contains a #label (if specified) and close
15442 * button.
15443 * @cfg {boolean} [padded] Add padding to the popup's body
15444 */
15445 OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
15446 // Configuration initialization
15447 config = config || {};
15448
15449 // Parent constructor
15450 OO.ui.PopupWidget.parent.call( this, config );
15451
15452 // Properties (must be set before ClippableElement constructor call)
15453 this.$body = $( '<div>' );
15454
15455 // Mixin constructors
15456 OO.ui.mixin.LabelElement.call( this, config );
15457 OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$body } ) );
15458
15459 // Properties
15460 this.$popup = $( '<div>' );
15461 this.$head = $( '<div>' );
15462 this.$anchor = $( '<div>' );
15463 // If undefined, will be computed lazily in updateDimensions()
15464 this.$container = config.$container;
15465 this.containerPadding = config.containerPadding !== undefined ? config.containerPadding : 10;
15466 this.autoClose = !!config.autoClose;
15467 this.$autoCloseIgnore = config.$autoCloseIgnore;
15468 this.transitionTimeout = null;
15469 this.anchor = null;
15470 this.width = config.width !== undefined ? config.width : 320;
15471 this.height = config.height !== undefined ? config.height : null;
15472 this.setAlignment( config.align );
15473 this.closeButton = new OO.ui.ButtonWidget( { framed: false, icon: 'close' } );
15474 this.onMouseDownHandler = this.onMouseDown.bind( this );
15475 this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
15476
15477 // Events
15478 this.closeButton.connect( this, { click: 'onCloseButtonClick' } );
15479
15480 // Initialization
15481 this.toggleAnchor( config.anchor === undefined || config.anchor );
15482 this.$body.addClass( 'oo-ui-popupWidget-body' );
15483 this.$anchor.addClass( 'oo-ui-popupWidget-anchor' );
15484 this.$head
15485 .addClass( 'oo-ui-popupWidget-head' )
15486 .append( this.$label, this.closeButton.$element );
15487 if ( !config.head ) {
15488 this.$head.addClass( 'oo-ui-element-hidden' );
15489 }
15490 this.$popup
15491 .addClass( 'oo-ui-popupWidget-popup' )
15492 .append( this.$head, this.$body );
15493 this.$element
15494 .addClass( 'oo-ui-popupWidget' )
15495 .append( this.$popup, this.$anchor );
15496 // Move content, which was added to #$element by OO.ui.Widget, to the body
15497 if ( config.$content instanceof jQuery ) {
15498 this.$body.append( config.$content );
15499 }
15500 if ( config.padded ) {
15501 this.$body.addClass( 'oo-ui-popupWidget-body-padded' );
15502 }
15503
15504 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
15505 // that reference properties not initialized at that time of parent class construction
15506 // TODO: Find a better way to handle post-constructor setup
15507 this.visible = false;
15508 this.$element.addClass( 'oo-ui-element-hidden' );
15509 };
15510
15511 /* Setup */
15512
15513 OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
15514 OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.LabelElement );
15515 OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.ClippableElement );
15516
15517 /* Methods */
15518
15519 /**
15520 * Handles mouse down events.
15521 *
15522 * @private
15523 * @param {MouseEvent} e Mouse down event
15524 */
15525 OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
15526 if (
15527 this.isVisible() &&
15528 !$.contains( this.$element[ 0 ], e.target ) &&
15529 ( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length )
15530 ) {
15531 this.toggle( false );
15532 }
15533 };
15534
15535 /**
15536 * Bind mouse down listener.
15537 *
15538 * @private
15539 */
15540 OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
15541 // Capture clicks outside popup
15542 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
15543 };
15544
15545 /**
15546 * Handles close button click events.
15547 *
15548 * @private
15549 */
15550 OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
15551 if ( this.isVisible() ) {
15552 this.toggle( false );
15553 }
15554 };
15555
15556 /**
15557 * Unbind mouse down listener.
15558 *
15559 * @private
15560 */
15561 OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
15562 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
15563 };
15564
15565 /**
15566 * Handles key down events.
15567 *
15568 * @private
15569 * @param {KeyboardEvent} e Key down event
15570 */
15571 OO.ui.PopupWidget.prototype.onDocumentKeyDown = function ( e ) {
15572 if (
15573 e.which === OO.ui.Keys.ESCAPE &&
15574 this.isVisible()
15575 ) {
15576 this.toggle( false );
15577 e.preventDefault();
15578 e.stopPropagation();
15579 }
15580 };
15581
15582 /**
15583 * Bind key down listener.
15584 *
15585 * @private
15586 */
15587 OO.ui.PopupWidget.prototype.bindKeyDownListener = function () {
15588 this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
15589 };
15590
15591 /**
15592 * Unbind key down listener.
15593 *
15594 * @private
15595 */
15596 OO.ui.PopupWidget.prototype.unbindKeyDownListener = function () {
15597 this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
15598 };
15599
15600 /**
15601 * Show, hide, or toggle the visibility of the anchor.
15602 *
15603 * @param {boolean} [show] Show anchor, omit to toggle
15604 */
15605 OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) {
15606 show = show === undefined ? !this.anchored : !!show;
15607
15608 if ( this.anchored !== show ) {
15609 if ( show ) {
15610 this.$element.addClass( 'oo-ui-popupWidget-anchored' );
15611 } else {
15612 this.$element.removeClass( 'oo-ui-popupWidget-anchored' );
15613 }
15614 this.anchored = show;
15615 }
15616 };
15617
15618 /**
15619 * Check if the anchor is visible.
15620 *
15621 * @return {boolean} Anchor is visible
15622 */
15623 OO.ui.PopupWidget.prototype.hasAnchor = function () {
15624 return this.anchor;
15625 };
15626
15627 /**
15628 * @inheritdoc
15629 */
15630 OO.ui.PopupWidget.prototype.toggle = function ( show ) {
15631 show = show === undefined ? !this.isVisible() : !!show;
15632
15633 var change = show !== this.isVisible();
15634
15635 // Parent method
15636 OO.ui.PopupWidget.parent.prototype.toggle.call( this, show );
15637
15638 if ( change ) {
15639 if ( show ) {
15640 if ( this.autoClose ) {
15641 this.bindMouseDownListener();
15642 this.bindKeyDownListener();
15643 }
15644 this.updateDimensions();
15645 this.toggleClipping( true );
15646 } else {
15647 this.toggleClipping( false );
15648 if ( this.autoClose ) {
15649 this.unbindMouseDownListener();
15650 this.unbindKeyDownListener();
15651 }
15652 }
15653 }
15654
15655 return this;
15656 };
15657
15658 /**
15659 * Set the size of the popup.
15660 *
15661 * Changing the size may also change the popup's position depending on the alignment.
15662 *
15663 * @param {number} width Width in pixels
15664 * @param {number} height Height in pixels
15665 * @param {boolean} [transition=false] Use a smooth transition
15666 * @chainable
15667 */
15668 OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) {
15669 this.width = width;
15670 this.height = height !== undefined ? height : null;
15671 if ( this.isVisible() ) {
15672 this.updateDimensions( transition );
15673 }
15674 };
15675
15676 /**
15677 * Update the size and position.
15678 *
15679 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
15680 * be called automatically.
15681 *
15682 * @param {boolean} [transition=false] Use a smooth transition
15683 * @chainable
15684 */
15685 OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
15686 var popupOffset, originOffset, containerLeft, containerWidth, containerRight,
15687 popupLeft, popupRight, overlapLeft, overlapRight, anchorWidth,
15688 align = this.align,
15689 widget = this;
15690
15691 if ( !this.$container ) {
15692 // Lazy-initialize $container if not specified in constructor
15693 this.$container = $( this.getClosestScrollableElementContainer() );
15694 }
15695
15696 // Set height and width before measuring things, since it might cause our measurements
15697 // to change (e.g. due to scrollbars appearing or disappearing)
15698 this.$popup.css( {
15699 width: this.width,
15700 height: this.height !== null ? this.height : 'auto'
15701 } );
15702
15703 // If we are in RTL, we need to flip the alignment, unless it is center
15704 if ( align === 'forwards' || align === 'backwards' ) {
15705 if ( this.$container.css( 'direction' ) === 'rtl' ) {
15706 align = ( { forwards: 'force-left', backwards: 'force-right' } )[ this.align ];
15707 } else {
15708 align = ( { forwards: 'force-right', backwards: 'force-left' } )[ this.align ];
15709 }
15710
15711 }
15712
15713 // Compute initial popupOffset based on alignment
15714 popupOffset = this.width * ( { 'force-left': -1, center: -0.5, 'force-right': 0 } )[ align ];
15715
15716 // Figure out if this will cause the popup to go beyond the edge of the container
15717 originOffset = this.$element.offset().left;
15718 containerLeft = this.$container.offset().left;
15719 containerWidth = this.$container.innerWidth();
15720 containerRight = containerLeft + containerWidth;
15721 popupLeft = popupOffset - this.containerPadding;
15722 popupRight = popupOffset + this.containerPadding + this.width + this.containerPadding;
15723 overlapLeft = ( originOffset + popupLeft ) - containerLeft;
15724 overlapRight = containerRight - ( originOffset + popupRight );
15725
15726 // Adjust offset to make the popup not go beyond the edge, if needed
15727 if ( overlapRight < 0 ) {
15728 popupOffset += overlapRight;
15729 } else if ( overlapLeft < 0 ) {
15730 popupOffset -= overlapLeft;
15731 }
15732
15733 // Adjust offset to avoid anchor being rendered too close to the edge
15734 // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
15735 // TODO: Find a measurement that works for CSS anchors and image anchors
15736 anchorWidth = this.$anchor[ 0 ].scrollWidth * 2;
15737 if ( popupOffset + this.width < anchorWidth ) {
15738 popupOffset = anchorWidth - this.width;
15739 } else if ( -popupOffset < anchorWidth ) {
15740 popupOffset = -anchorWidth;
15741 }
15742
15743 // Prevent transition from being interrupted
15744 clearTimeout( this.transitionTimeout );
15745 if ( transition ) {
15746 // Enable transition
15747 this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
15748 }
15749
15750 // Position body relative to anchor
15751 this.$popup.css( 'margin-left', popupOffset );
15752
15753 if ( transition ) {
15754 // Prevent transitioning after transition is complete
15755 this.transitionTimeout = setTimeout( function () {
15756 widget.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
15757 }, 200 );
15758 } else {
15759 // Prevent transitioning immediately
15760 this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
15761 }
15762
15763 // Reevaluate clipping state since we've relocated and resized the popup
15764 this.clip();
15765
15766 return this;
15767 };
15768
15769 /**
15770 * Set popup alignment
15771 * @param {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
15772 * `backwards` or `forwards`.
15773 */
15774 OO.ui.PopupWidget.prototype.setAlignment = function ( align ) {
15775 // Validate alignment and transform deprecated values
15776 if ( [ 'left', 'right', 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align ) > -1 ) {
15777 this.align = { left: 'force-right', right: 'force-left' }[ align ] || align;
15778 } else {
15779 this.align = 'center';
15780 }
15781 };
15782
15783 /**
15784 * Get popup alignment
15785 * @return {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
15786 * `backwards` or `forwards`.
15787 */
15788 OO.ui.PopupWidget.prototype.getAlignment = function () {
15789 return this.align;
15790 };
15791
15792 /**
15793 * Progress bars visually display the status of an operation, such as a download,
15794 * and can be either determinate or indeterminate:
15795 *
15796 * - **determinate** process bars show the percent of an operation that is complete.
15797 *
15798 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
15799 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
15800 * not use percentages.
15801 *
15802 * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
15803 *
15804 * @example
15805 * // Examples of determinate and indeterminate progress bars.
15806 * var progressBar1 = new OO.ui.ProgressBarWidget( {
15807 * progress: 33
15808 * } );
15809 * var progressBar2 = new OO.ui.ProgressBarWidget();
15810 *
15811 * // Create a FieldsetLayout to layout progress bars
15812 * var fieldset = new OO.ui.FieldsetLayout;
15813 * fieldset.addItems( [
15814 * new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
15815 * new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
15816 * ] );
15817 * $( 'body' ).append( fieldset.$element );
15818 *
15819 * @class
15820 * @extends OO.ui.Widget
15821 *
15822 * @constructor
15823 * @param {Object} [config] Configuration options
15824 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
15825 * To create a determinate progress bar, specify a number that reflects the initial percent complete.
15826 * By default, the progress bar is indeterminate.
15827 */
15828 OO.ui.ProgressBarWidget = function OoUiProgressBarWidget( config ) {
15829 // Configuration initialization
15830 config = config || {};
15831
15832 // Parent constructor
15833 OO.ui.ProgressBarWidget.parent.call( this, config );
15834
15835 // Properties
15836 this.$bar = $( '<div>' );
15837 this.progress = null;
15838
15839 // Initialization
15840 this.setProgress( config.progress !== undefined ? config.progress : false );
15841 this.$bar.addClass( 'oo-ui-progressBarWidget-bar' );
15842 this.$element
15843 .attr( {
15844 role: 'progressbar',
15845 'aria-valuemin': 0,
15846 'aria-valuemax': 100
15847 } )
15848 .addClass( 'oo-ui-progressBarWidget' )
15849 .append( this.$bar );
15850 };
15851
15852 /* Setup */
15853
15854 OO.inheritClass( OO.ui.ProgressBarWidget, OO.ui.Widget );
15855
15856 /* Static Properties */
15857
15858 OO.ui.ProgressBarWidget.static.tagName = 'div';
15859
15860 /* Methods */
15861
15862 /**
15863 * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
15864 *
15865 * @return {number|boolean} Progress percent
15866 */
15867 OO.ui.ProgressBarWidget.prototype.getProgress = function () {
15868 return this.progress;
15869 };
15870
15871 /**
15872 * Set the percent of the process completed or `false` for an indeterminate process.
15873 *
15874 * @param {number|boolean} progress Progress percent or `false` for indeterminate
15875 */
15876 OO.ui.ProgressBarWidget.prototype.setProgress = function ( progress ) {
15877 this.progress = progress;
15878
15879 if ( progress !== false ) {
15880 this.$bar.css( 'width', this.progress + '%' );
15881 this.$element.attr( 'aria-valuenow', this.progress );
15882 } else {
15883 this.$bar.css( 'width', '' );
15884 this.$element.removeAttr( 'aria-valuenow' );
15885 }
15886 this.$element.toggleClass( 'oo-ui-progressBarWidget-indeterminate', !progress );
15887 };
15888
15889 /**
15890 * SearchWidgets combine a {@link OO.ui.TextInputWidget text input field}, where users can type a search query,
15891 * and a {@link OO.ui.TextInputMenuSelectWidget menu} of search results, which is displayed beneath the query
15892 * field. Unlike {@link OO.ui.mixin.LookupElement lookup menus}, search result menus are always visible to the user.
15893 * Users can choose an item from the menu or type a query into the text field to search for a matching result item.
15894 * In general, search widgets are used inside a separate {@link OO.ui.Dialog dialog} window.
15895 *
15896 * Each time the query is changed, the search result menu is cleared and repopulated. Please see
15897 * the [OOjs UI demos][1] for an example.
15898 *
15899 * [1]: https://tools.wmflabs.org/oojs-ui/oojs-ui/demos/#dialogs-mediawiki-vector-ltr
15900 *
15901 * @class
15902 * @extends OO.ui.Widget
15903 *
15904 * @constructor
15905 * @param {Object} [config] Configuration options
15906 * @cfg {string|jQuery} [placeholder] Placeholder text for query input
15907 * @cfg {string} [value] Initial query value
15908 */
15909 OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
15910 // Configuration initialization
15911 config = config || {};
15912
15913 // Parent constructor
15914 OO.ui.SearchWidget.parent.call( this, config );
15915
15916 // Properties
15917 this.query = new OO.ui.TextInputWidget( {
15918 icon: 'search',
15919 placeholder: config.placeholder,
15920 value: config.value
15921 } );
15922 this.results = new OO.ui.SelectWidget();
15923 this.$query = $( '<div>' );
15924 this.$results = $( '<div>' );
15925
15926 // Events
15927 this.query.connect( this, {
15928 change: 'onQueryChange',
15929 enter: 'onQueryEnter'
15930 } );
15931 this.query.$input.on( 'keydown', this.onQueryKeydown.bind( this ) );
15932
15933 // Initialization
15934 this.$query
15935 .addClass( 'oo-ui-searchWidget-query' )
15936 .append( this.query.$element );
15937 this.$results
15938 .addClass( 'oo-ui-searchWidget-results' )
15939 .append( this.results.$element );
15940 this.$element
15941 .addClass( 'oo-ui-searchWidget' )
15942 .append( this.$results, this.$query );
15943 };
15944
15945 /* Setup */
15946
15947 OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget );
15948
15949 /* Methods */
15950
15951 /**
15952 * Handle query key down events.
15953 *
15954 * @private
15955 * @param {jQuery.Event} e Key down event
15956 */
15957 OO.ui.SearchWidget.prototype.onQueryKeydown = function ( e ) {
15958 var highlightedItem, nextItem,
15959 dir = e.which === OO.ui.Keys.DOWN ? 1 : ( e.which === OO.ui.Keys.UP ? -1 : 0 );
15960
15961 if ( dir ) {
15962 highlightedItem = this.results.getHighlightedItem();
15963 if ( !highlightedItem ) {
15964 highlightedItem = this.results.getSelectedItem();
15965 }
15966 nextItem = this.results.getRelativeSelectableItem( highlightedItem, dir );
15967 this.results.highlightItem( nextItem );
15968 nextItem.scrollElementIntoView();
15969 }
15970 };
15971
15972 /**
15973 * Handle select widget select events.
15974 *
15975 * Clears existing results. Subclasses should repopulate items according to new query.
15976 *
15977 * @private
15978 * @param {string} value New value
15979 */
15980 OO.ui.SearchWidget.prototype.onQueryChange = function () {
15981 // Reset
15982 this.results.clearItems();
15983 };
15984
15985 /**
15986 * Handle select widget enter key events.
15987 *
15988 * Chooses highlighted item.
15989 *
15990 * @private
15991 * @param {string} value New value
15992 */
15993 OO.ui.SearchWidget.prototype.onQueryEnter = function () {
15994 // Reset
15995 this.results.chooseItem( this.results.getHighlightedItem() );
15996 };
15997
15998 /**
15999 * Get the query input.
16000 *
16001 * @return {OO.ui.TextInputWidget} Query input
16002 */
16003 OO.ui.SearchWidget.prototype.getQuery = function () {
16004 return this.query;
16005 };
16006
16007 /**
16008 * Get the search results menu.
16009 *
16010 * @return {OO.ui.SelectWidget} Menu of search results
16011 */
16012 OO.ui.SearchWidget.prototype.getResults = function () {
16013 return this.results;
16014 };
16015
16016 /**
16017 * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
16018 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
16019 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
16020 * menu selects}.
16021 *
16022 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
16023 * information, please see the [OOjs UI documentation on MediaWiki][1].
16024 *
16025 * @example
16026 * // Example of a select widget with three options
16027 * var select = new OO.ui.SelectWidget( {
16028 * items: [
16029 * new OO.ui.OptionWidget( {
16030 * data: 'a',
16031 * label: 'Option One',
16032 * } ),
16033 * new OO.ui.OptionWidget( {
16034 * data: 'b',
16035 * label: 'Option Two',
16036 * } ),
16037 * new OO.ui.OptionWidget( {
16038 * data: 'c',
16039 * label: 'Option Three',
16040 * } )
16041 * ]
16042 * } );
16043 * $( 'body' ).append( select.$element );
16044 *
16045 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
16046 *
16047 * @abstract
16048 * @class
16049 * @extends OO.ui.Widget
16050 * @mixins OO.ui.mixin.GroupElement
16051 *
16052 * @constructor
16053 * @param {Object} [config] Configuration options
16054 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
16055 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
16056 * the [OOjs UI documentation on MediaWiki] [2] for examples.
16057 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
16058 */
16059 OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
16060 // Configuration initialization
16061 config = config || {};
16062
16063 // Parent constructor
16064 OO.ui.SelectWidget.parent.call( this, config );
16065
16066 // Mixin constructors
16067 OO.ui.mixin.GroupWidget.call( this, $.extend( {}, config, { $group: this.$element } ) );
16068
16069 // Properties
16070 this.pressed = false;
16071 this.selecting = null;
16072 this.onMouseUpHandler = this.onMouseUp.bind( this );
16073 this.onMouseMoveHandler = this.onMouseMove.bind( this );
16074 this.onKeyDownHandler = this.onKeyDown.bind( this );
16075 this.onKeyPressHandler = this.onKeyPress.bind( this );
16076 this.keyPressBuffer = '';
16077 this.keyPressBufferTimer = null;
16078
16079 // Events
16080 this.connect( this, {
16081 toggle: 'onToggle'
16082 } );
16083 this.$element.on( {
16084 mousedown: this.onMouseDown.bind( this ),
16085 mouseover: this.onMouseOver.bind( this ),
16086 mouseleave: this.onMouseLeave.bind( this )
16087 } );
16088
16089 // Initialization
16090 this.$element
16091 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
16092 .attr( 'role', 'listbox' );
16093 if ( Array.isArray( config.items ) ) {
16094 this.addItems( config.items );
16095 }
16096 };
16097
16098 /* Setup */
16099
16100 OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
16101
16102 // Need to mixin base class as well
16103 OO.mixinClass( OO.ui.SelectWidget, OO.ui.mixin.GroupElement );
16104 OO.mixinClass( OO.ui.SelectWidget, OO.ui.mixin.GroupWidget );
16105
16106 /* Static */
16107 OO.ui.SelectWidget.static.passAllFilter = function () {
16108 return true;
16109 };
16110
16111 /* Events */
16112
16113 /**
16114 * @event highlight
16115 *
16116 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
16117 *
16118 * @param {OO.ui.OptionWidget|null} item Highlighted item
16119 */
16120
16121 /**
16122 * @event press
16123 *
16124 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
16125 * pressed state of an option.
16126 *
16127 * @param {OO.ui.OptionWidget|null} item Pressed item
16128 */
16129
16130 /**
16131 * @event select
16132 *
16133 * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
16134 *
16135 * @param {OO.ui.OptionWidget|null} item Selected item
16136 */
16137
16138 /**
16139 * @event choose
16140 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
16141 * @param {OO.ui.OptionWidget} item Chosen item
16142 */
16143
16144 /**
16145 * @event add
16146 *
16147 * An `add` event is emitted when options are added to the select with the #addItems method.
16148 *
16149 * @param {OO.ui.OptionWidget[]} items Added items
16150 * @param {number} index Index of insertion point
16151 */
16152
16153 /**
16154 * @event remove
16155 *
16156 * A `remove` event is emitted when options are removed from the select with the #clearItems
16157 * or #removeItems methods.
16158 *
16159 * @param {OO.ui.OptionWidget[]} items Removed items
16160 */
16161
16162 /* Methods */
16163
16164 /**
16165 * Handle mouse down events.
16166 *
16167 * @private
16168 * @param {jQuery.Event} e Mouse down event
16169 */
16170 OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
16171 var item;
16172
16173 if ( !this.isDisabled() && e.which === 1 ) {
16174 this.togglePressed( true );
16175 item = this.getTargetItem( e );
16176 if ( item && item.isSelectable() ) {
16177 this.pressItem( item );
16178 this.selecting = item;
16179 this.getElementDocument().addEventListener(
16180 'mouseup',
16181 this.onMouseUpHandler,
16182 true
16183 );
16184 this.getElementDocument().addEventListener(
16185 'mousemove',
16186 this.onMouseMoveHandler,
16187 true
16188 );
16189 }
16190 }
16191 return false;
16192 };
16193
16194 /**
16195 * Handle mouse up events.
16196 *
16197 * @private
16198 * @param {jQuery.Event} e Mouse up event
16199 */
16200 OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
16201 var item;
16202
16203 this.togglePressed( false );
16204 if ( !this.selecting ) {
16205 item = this.getTargetItem( e );
16206 if ( item && item.isSelectable() ) {
16207 this.selecting = item;
16208 }
16209 }
16210 if ( !this.isDisabled() && e.which === 1 && this.selecting ) {
16211 this.pressItem( null );
16212 this.chooseItem( this.selecting );
16213 this.selecting = null;
16214 }
16215
16216 this.getElementDocument().removeEventListener(
16217 'mouseup',
16218 this.onMouseUpHandler,
16219 true
16220 );
16221 this.getElementDocument().removeEventListener(
16222 'mousemove',
16223 this.onMouseMoveHandler,
16224 true
16225 );
16226
16227 return false;
16228 };
16229
16230 /**
16231 * Handle mouse move events.
16232 *
16233 * @private
16234 * @param {jQuery.Event} e Mouse move event
16235 */
16236 OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
16237 var item;
16238
16239 if ( !this.isDisabled() && this.pressed ) {
16240 item = this.getTargetItem( e );
16241 if ( item && item !== this.selecting && item.isSelectable() ) {
16242 this.pressItem( item );
16243 this.selecting = item;
16244 }
16245 }
16246 return false;
16247 };
16248
16249 /**
16250 * Handle mouse over events.
16251 *
16252 * @private
16253 * @param {jQuery.Event} e Mouse over event
16254 */
16255 OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
16256 var item;
16257
16258 if ( !this.isDisabled() ) {
16259 item = this.getTargetItem( e );
16260 this.highlightItem( item && item.isHighlightable() ? item : null );
16261 }
16262 return false;
16263 };
16264
16265 /**
16266 * Handle mouse leave events.
16267 *
16268 * @private
16269 * @param {jQuery.Event} e Mouse over event
16270 */
16271 OO.ui.SelectWidget.prototype.onMouseLeave = function () {
16272 if ( !this.isDisabled() ) {
16273 this.highlightItem( null );
16274 }
16275 return false;
16276 };
16277
16278 /**
16279 * Handle key down events.
16280 *
16281 * @protected
16282 * @param {jQuery.Event} e Key down event
16283 */
16284 OO.ui.SelectWidget.prototype.onKeyDown = function ( e ) {
16285 var nextItem,
16286 handled = false,
16287 currentItem = this.getHighlightedItem() || this.getSelectedItem();
16288
16289 if ( !this.isDisabled() && this.isVisible() ) {
16290 switch ( e.keyCode ) {
16291 case OO.ui.Keys.ENTER:
16292 if ( currentItem && currentItem.constructor.static.highlightable ) {
16293 // Was only highlighted, now let's select it. No-op if already selected.
16294 this.chooseItem( currentItem );
16295 handled = true;
16296 }
16297 break;
16298 case OO.ui.Keys.UP:
16299 case OO.ui.Keys.LEFT:
16300 this.clearKeyPressBuffer();
16301 nextItem = this.getRelativeSelectableItem( currentItem, -1 );
16302 handled = true;
16303 break;
16304 case OO.ui.Keys.DOWN:
16305 case OO.ui.Keys.RIGHT:
16306 this.clearKeyPressBuffer();
16307 nextItem = this.getRelativeSelectableItem( currentItem, 1 );
16308 handled = true;
16309 break;
16310 case OO.ui.Keys.ESCAPE:
16311 case OO.ui.Keys.TAB:
16312 if ( currentItem && currentItem.constructor.static.highlightable ) {
16313 currentItem.setHighlighted( false );
16314 }
16315 this.unbindKeyDownListener();
16316 this.unbindKeyPressListener();
16317 // Don't prevent tabbing away / defocusing
16318 handled = false;
16319 break;
16320 }
16321
16322 if ( nextItem ) {
16323 if ( nextItem.constructor.static.highlightable ) {
16324 this.highlightItem( nextItem );
16325 } else {
16326 this.chooseItem( nextItem );
16327 }
16328 nextItem.scrollElementIntoView();
16329 }
16330
16331 if ( handled ) {
16332 // Can't just return false, because e is not always a jQuery event
16333 e.preventDefault();
16334 e.stopPropagation();
16335 }
16336 }
16337 };
16338
16339 /**
16340 * Bind key down listener.
16341 *
16342 * @protected
16343 */
16344 OO.ui.SelectWidget.prototype.bindKeyDownListener = function () {
16345 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
16346 };
16347
16348 /**
16349 * Unbind key down listener.
16350 *
16351 * @protected
16352 */
16353 OO.ui.SelectWidget.prototype.unbindKeyDownListener = function () {
16354 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
16355 };
16356
16357 /**
16358 * Clear the key-press buffer
16359 *
16360 * @protected
16361 */
16362 OO.ui.SelectWidget.prototype.clearKeyPressBuffer = function () {
16363 if ( this.keyPressBufferTimer ) {
16364 clearTimeout( this.keyPressBufferTimer );
16365 this.keyPressBufferTimer = null;
16366 }
16367 this.keyPressBuffer = '';
16368 };
16369
16370 /**
16371 * Handle key press events.
16372 *
16373 * @protected
16374 * @param {jQuery.Event} e Key press event
16375 */
16376 OO.ui.SelectWidget.prototype.onKeyPress = function ( e ) {
16377 var c, filter, item;
16378
16379 if ( !e.charCode ) {
16380 if ( e.keyCode === OO.ui.Keys.BACKSPACE && this.keyPressBuffer !== '' ) {
16381 this.keyPressBuffer = this.keyPressBuffer.substr( 0, this.keyPressBuffer.length - 1 );
16382 return false;
16383 }
16384 return;
16385 }
16386 if ( String.fromCodePoint ) {
16387 c = String.fromCodePoint( e.charCode );
16388 } else {
16389 c = String.fromCharCode( e.charCode );
16390 }
16391
16392 if ( this.keyPressBufferTimer ) {
16393 clearTimeout( this.keyPressBufferTimer );
16394 }
16395 this.keyPressBufferTimer = setTimeout( this.clearKeyPressBuffer.bind( this ), 1500 );
16396
16397 item = this.getHighlightedItem() || this.getSelectedItem();
16398
16399 if ( this.keyPressBuffer === c ) {
16400 // Common (if weird) special case: typing "xxxx" will cycle through all
16401 // the items beginning with "x".
16402 if ( item ) {
16403 item = this.getRelativeSelectableItem( item, 1 );
16404 }
16405 } else {
16406 this.keyPressBuffer += c;
16407 }
16408
16409 filter = this.getItemMatcher( this.keyPressBuffer );
16410 if ( !item || !filter( item ) ) {
16411 item = this.getRelativeSelectableItem( item, 1, filter );
16412 }
16413 if ( item ) {
16414 if ( item.constructor.static.highlightable ) {
16415 this.highlightItem( item );
16416 } else {
16417 this.chooseItem( item );
16418 }
16419 item.scrollElementIntoView();
16420 }
16421
16422 return false;
16423 };
16424
16425 /**
16426 * Get a matcher for the specific string
16427 *
16428 * @protected
16429 * @param {string} s String to match against items
16430 * @return {Function} function ( OO.ui.OptionItem ) => boolean
16431 */
16432 OO.ui.SelectWidget.prototype.getItemMatcher = function ( s ) {
16433 var re;
16434
16435 if ( s.normalize ) {
16436 s = s.normalize();
16437 }
16438 re = new RegExp( '^\\s*' + s.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' ), 'i' );
16439 return function ( item ) {
16440 var l = item.getLabel();
16441 if ( typeof l !== 'string' ) {
16442 l = item.$label.text();
16443 }
16444 if ( l.normalize ) {
16445 l = l.normalize();
16446 }
16447 return re.test( l );
16448 };
16449 };
16450
16451 /**
16452 * Bind key press listener.
16453 *
16454 * @protected
16455 */
16456 OO.ui.SelectWidget.prototype.bindKeyPressListener = function () {
16457 this.getElementWindow().addEventListener( 'keypress', this.onKeyPressHandler, true );
16458 };
16459
16460 /**
16461 * Unbind key down listener.
16462 *
16463 * If you override this, be sure to call this.clearKeyPressBuffer() from your
16464 * implementation.
16465 *
16466 * @protected
16467 */
16468 OO.ui.SelectWidget.prototype.unbindKeyPressListener = function () {
16469 this.getElementWindow().removeEventListener( 'keypress', this.onKeyPressHandler, true );
16470 this.clearKeyPressBuffer();
16471 };
16472
16473 /**
16474 * Visibility change handler
16475 *
16476 * @protected
16477 * @param {boolean} visible
16478 */
16479 OO.ui.SelectWidget.prototype.onToggle = function ( visible ) {
16480 if ( !visible ) {
16481 this.clearKeyPressBuffer();
16482 }
16483 };
16484
16485 /**
16486 * Get the closest item to a jQuery.Event.
16487 *
16488 * @private
16489 * @param {jQuery.Event} e
16490 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
16491 */
16492 OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) {
16493 return $( e.target ).closest( '.oo-ui-optionWidget' ).data( 'oo-ui-optionWidget' ) || null;
16494 };
16495
16496 /**
16497 * Get selected item.
16498 *
16499 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
16500 */
16501 OO.ui.SelectWidget.prototype.getSelectedItem = function () {
16502 var i, len;
16503
16504 for ( i = 0, len = this.items.length; i < len; i++ ) {
16505 if ( this.items[ i ].isSelected() ) {
16506 return this.items[ i ];
16507 }
16508 }
16509 return null;
16510 };
16511
16512 /**
16513 * Get highlighted item.
16514 *
16515 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
16516 */
16517 OO.ui.SelectWidget.prototype.getHighlightedItem = function () {
16518 var i, len;
16519
16520 for ( i = 0, len = this.items.length; i < len; i++ ) {
16521 if ( this.items[ i ].isHighlighted() ) {
16522 return this.items[ i ];
16523 }
16524 }
16525 return null;
16526 };
16527
16528 /**
16529 * Toggle pressed state.
16530 *
16531 * Press is a state that occurs when a user mouses down on an item, but
16532 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
16533 * until the user releases the mouse.
16534 *
16535 * @param {boolean} pressed An option is being pressed
16536 */
16537 OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) {
16538 if ( pressed === undefined ) {
16539 pressed = !this.pressed;
16540 }
16541 if ( pressed !== this.pressed ) {
16542 this.$element
16543 .toggleClass( 'oo-ui-selectWidget-pressed', pressed )
16544 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed );
16545 this.pressed = pressed;
16546 }
16547 };
16548
16549 /**
16550 * Highlight an option. If the `item` param is omitted, no options will be highlighted
16551 * and any existing highlight will be removed. The highlight is mutually exclusive.
16552 *
16553 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
16554 * @fires highlight
16555 * @chainable
16556 */
16557 OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
16558 var i, len, highlighted,
16559 changed = false;
16560
16561 for ( i = 0, len = this.items.length; i < len; i++ ) {
16562 highlighted = this.items[ i ] === item;
16563 if ( this.items[ i ].isHighlighted() !== highlighted ) {
16564 this.items[ i ].setHighlighted( highlighted );
16565 changed = true;
16566 }
16567 }
16568 if ( changed ) {
16569 this.emit( 'highlight', item );
16570 }
16571
16572 return this;
16573 };
16574
16575 /**
16576 * Programmatically select an option by its data. If the `data` parameter is omitted,
16577 * or if the item does not exist, all options will be deselected.
16578 *
16579 * @param {Object|string} [data] Value of the item to select, omit to deselect all
16580 * @fires select
16581 * @chainable
16582 */
16583 OO.ui.SelectWidget.prototype.selectItemByData = function ( data ) {
16584 var itemFromData = this.getItemFromData( data );
16585 if ( data === undefined || !itemFromData ) {
16586 return this.selectItem();
16587 }
16588 return this.selectItem( itemFromData );
16589 };
16590
16591 /**
16592 * Programmatically select an option by its reference. If the `item` parameter is omitted,
16593 * all options will be deselected.
16594 *
16595 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
16596 * @fires select
16597 * @chainable
16598 */
16599 OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
16600 var i, len, selected,
16601 changed = false;
16602
16603 for ( i = 0, len = this.items.length; i < len; i++ ) {
16604 selected = this.items[ i ] === item;
16605 if ( this.items[ i ].isSelected() !== selected ) {
16606 this.items[ i ].setSelected( selected );
16607 changed = true;
16608 }
16609 }
16610 if ( changed ) {
16611 this.emit( 'select', item );
16612 }
16613
16614 return this;
16615 };
16616
16617 /**
16618 * Press an item.
16619 *
16620 * Press is a state that occurs when a user mouses down on an item, but has not
16621 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
16622 * releases the mouse.
16623 *
16624 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
16625 * @fires press
16626 * @chainable
16627 */
16628 OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
16629 var i, len, pressed,
16630 changed = false;
16631
16632 for ( i = 0, len = this.items.length; i < len; i++ ) {
16633 pressed = this.items[ i ] === item;
16634 if ( this.items[ i ].isPressed() !== pressed ) {
16635 this.items[ i ].setPressed( pressed );
16636 changed = true;
16637 }
16638 }
16639 if ( changed ) {
16640 this.emit( 'press', item );
16641 }
16642
16643 return this;
16644 };
16645
16646 /**
16647 * Choose an item.
16648 *
16649 * Note that ‘choose’ should never be modified programmatically. A user can choose
16650 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
16651 * use the #selectItem method.
16652 *
16653 * This method is identical to #selectItem, but may vary in subclasses that take additional action
16654 * when users choose an item with the keyboard or mouse.
16655 *
16656 * @param {OO.ui.OptionWidget} item Item to choose
16657 * @fires choose
16658 * @chainable
16659 */
16660 OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
16661 this.selectItem( item );
16662 this.emit( 'choose', item );
16663
16664 return this;
16665 };
16666
16667 /**
16668 * Get an option by its position relative to the specified item (or to the start of the option array,
16669 * if item is `null`). The direction in which to search through the option array is specified with a
16670 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
16671 * `null` if there are no options in the array.
16672 *
16673 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
16674 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
16675 * @param {Function} filter Only consider items for which this function returns
16676 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
16677 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
16678 */
16679 OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction, filter ) {
16680 var currentIndex, nextIndex, i,
16681 increase = direction > 0 ? 1 : -1,
16682 len = this.items.length;
16683
16684 if ( !$.isFunction( filter ) ) {
16685 filter = OO.ui.SelectWidget.static.passAllFilter;
16686 }
16687
16688 if ( item instanceof OO.ui.OptionWidget ) {
16689 currentIndex = $.inArray( item, this.items );
16690 nextIndex = ( currentIndex + increase + len ) % len;
16691 } else {
16692 // If no item is selected and moving forward, start at the beginning.
16693 // If moving backward, start at the end.
16694 nextIndex = direction > 0 ? 0 : len - 1;
16695 }
16696
16697 for ( i = 0; i < len; i++ ) {
16698 item = this.items[ nextIndex ];
16699 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() && filter( item ) ) {
16700 return item;
16701 }
16702 nextIndex = ( nextIndex + increase + len ) % len;
16703 }
16704 return null;
16705 };
16706
16707 /**
16708 * Get the next selectable item or `null` if there are no selectable items.
16709 * Disabled options and menu-section markers and breaks are not selectable.
16710 *
16711 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
16712 */
16713 OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
16714 var i, len, item;
16715
16716 for ( i = 0, len = this.items.length; i < len; i++ ) {
16717 item = this.items[ i ];
16718 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
16719 return item;
16720 }
16721 }
16722
16723 return null;
16724 };
16725
16726 /**
16727 * Add an array of options to the select. Optionally, an index number can be used to
16728 * specify an insertion point.
16729 *
16730 * @param {OO.ui.OptionWidget[]} items Items to add
16731 * @param {number} [index] Index to insert items after
16732 * @fires add
16733 * @chainable
16734 */
16735 OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
16736 // Mixin method
16737 OO.ui.mixin.GroupWidget.prototype.addItems.call( this, items, index );
16738
16739 // Always provide an index, even if it was omitted
16740 this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index );
16741
16742 return this;
16743 };
16744
16745 /**
16746 * Remove the specified array of options from the select. Options will be detached
16747 * from the DOM, not removed, so they can be reused later. To remove all options from
16748 * the select, you may wish to use the #clearItems method instead.
16749 *
16750 * @param {OO.ui.OptionWidget[]} items Items to remove
16751 * @fires remove
16752 * @chainable
16753 */
16754 OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
16755 var i, len, item;
16756
16757 // Deselect items being removed
16758 for ( i = 0, len = items.length; i < len; i++ ) {
16759 item = items[ i ];
16760 if ( item.isSelected() ) {
16761 this.selectItem( null );
16762 }
16763 }
16764
16765 // Mixin method
16766 OO.ui.mixin.GroupWidget.prototype.removeItems.call( this, items );
16767
16768 this.emit( 'remove', items );
16769
16770 return this;
16771 };
16772
16773 /**
16774 * Clear all options from the select. Options will be detached from the DOM, not removed,
16775 * so that they can be reused later. To remove a subset of options from the select, use
16776 * the #removeItems method.
16777 *
16778 * @fires remove
16779 * @chainable
16780 */
16781 OO.ui.SelectWidget.prototype.clearItems = function () {
16782 var items = this.items.slice();
16783
16784 // Mixin method
16785 OO.ui.mixin.GroupWidget.prototype.clearItems.call( this );
16786
16787 // Clear selection
16788 this.selectItem( null );
16789
16790 this.emit( 'remove', items );
16791
16792 return this;
16793 };
16794
16795 /**
16796 * ButtonSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains
16797 * button options and is used together with
16798 * OO.ui.ButtonOptionWidget. The ButtonSelectWidget provides an interface for
16799 * highlighting, choosing, and selecting mutually exclusive options. Please see
16800 * the [OOjs UI documentation on MediaWiki] [1] for more information.
16801 *
16802 * @example
16803 * // Example: A ButtonSelectWidget that contains three ButtonOptionWidgets
16804 * var option1 = new OO.ui.ButtonOptionWidget( {
16805 * data: 1,
16806 * label: 'Option 1',
16807 * title: 'Button option 1'
16808 * } );
16809 *
16810 * var option2 = new OO.ui.ButtonOptionWidget( {
16811 * data: 2,
16812 * label: 'Option 2',
16813 * title: 'Button option 2'
16814 * } );
16815 *
16816 * var option3 = new OO.ui.ButtonOptionWidget( {
16817 * data: 3,
16818 * label: 'Option 3',
16819 * title: 'Button option 3'
16820 * } );
16821 *
16822 * var buttonSelect=new OO.ui.ButtonSelectWidget( {
16823 * items: [ option1, option2, option3 ]
16824 * } );
16825 * $( 'body' ).append( buttonSelect.$element );
16826 *
16827 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
16828 *
16829 * @class
16830 * @extends OO.ui.SelectWidget
16831 * @mixins OO.ui.mixin.TabIndexedElement
16832 *
16833 * @constructor
16834 * @param {Object} [config] Configuration options
16835 */
16836 OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
16837 // Parent constructor
16838 OO.ui.ButtonSelectWidget.parent.call( this, config );
16839
16840 // Mixin constructors
16841 OO.ui.mixin.TabIndexedElement.call( this, config );
16842
16843 // Events
16844 this.$element.on( {
16845 focus: this.bindKeyDownListener.bind( this ),
16846 blur: this.unbindKeyDownListener.bind( this )
16847 } );
16848
16849 // Initialization
16850 this.$element.addClass( 'oo-ui-buttonSelectWidget' );
16851 };
16852
16853 /* Setup */
16854
16855 OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
16856 OO.mixinClass( OO.ui.ButtonSelectWidget, OO.ui.mixin.TabIndexedElement );
16857
16858 /**
16859 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
16860 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
16861 * an interface for adding, removing and selecting options.
16862 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
16863 *
16864 * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
16865 * OO.ui.RadioSelectInputWidget instead.
16866 *
16867 * @example
16868 * // A RadioSelectWidget with RadioOptions.
16869 * var option1 = new OO.ui.RadioOptionWidget( {
16870 * data: 'a',
16871 * label: 'Selected radio option'
16872 * } );
16873 *
16874 * var option2 = new OO.ui.RadioOptionWidget( {
16875 * data: 'b',
16876 * label: 'Unselected radio option'
16877 * } );
16878 *
16879 * var radioSelect=new OO.ui.RadioSelectWidget( {
16880 * items: [ option1, option2 ]
16881 * } );
16882 *
16883 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
16884 * radioSelect.selectItem( option1 );
16885 *
16886 * $( 'body' ).append( radioSelect.$element );
16887 *
16888 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
16889
16890 *
16891 * @class
16892 * @extends OO.ui.SelectWidget
16893 * @mixins OO.ui.mixin.TabIndexedElement
16894 *
16895 * @constructor
16896 * @param {Object} [config] Configuration options
16897 */
16898 OO.ui.RadioSelectWidget = function OoUiRadioSelectWidget( config ) {
16899 // Parent constructor
16900 OO.ui.RadioSelectWidget.parent.call( this, config );
16901
16902 // Mixin constructors
16903 OO.ui.mixin.TabIndexedElement.call( this, config );
16904
16905 // Events
16906 this.$element.on( {
16907 focus: this.bindKeyDownListener.bind( this ),
16908 blur: this.unbindKeyDownListener.bind( this )
16909 } );
16910
16911 // Initialization
16912 this.$element
16913 .addClass( 'oo-ui-radioSelectWidget' )
16914 .attr( 'role', 'radiogroup' );
16915 };
16916
16917 /* Setup */
16918
16919 OO.inheritClass( OO.ui.RadioSelectWidget, OO.ui.SelectWidget );
16920 OO.mixinClass( OO.ui.RadioSelectWidget, OO.ui.mixin.TabIndexedElement );
16921
16922 /**
16923 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
16924 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
16925 * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxWidget ComboBoxWidget},
16926 * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
16927 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
16928 * and customized to be opened, closed, and displayed as needed.
16929 *
16930 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
16931 * mouse outside the menu.
16932 *
16933 * Menus also have support for keyboard interaction:
16934 *
16935 * - Enter/Return key: choose and select a menu option
16936 * - Up-arrow key: highlight the previous menu option
16937 * - Down-arrow key: highlight the next menu option
16938 * - Esc key: hide the menu
16939 *
16940 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
16941 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
16942 *
16943 * @class
16944 * @extends OO.ui.SelectWidget
16945 * @mixins OO.ui.mixin.ClippableElement
16946 *
16947 * @constructor
16948 * @param {Object} [config] Configuration options
16949 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
16950 * the text the user types. This config is used by {@link OO.ui.ComboBoxWidget ComboBoxWidget}
16951 * and {@link OO.ui.mixin.LookupElement LookupElement}
16952 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks the mouse
16953 * anywhere on the page outside of this widget, the menu is hidden. For example, if there is a button
16954 * that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
16955 * that button, unless the button (or its parent widget) is passed in here.
16956 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
16957 */
16958 OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) {
16959 // Configuration initialization
16960 config = config || {};
16961
16962 // Parent constructor
16963 OO.ui.MenuSelectWidget.parent.call( this, config );
16964
16965 // Mixin constructors
16966 OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
16967
16968 // Properties
16969 this.newItems = null;
16970 this.autoHide = config.autoHide === undefined || !!config.autoHide;
16971 this.$input = config.input ? config.input.$input : null;
16972 this.$widget = config.widget ? config.widget.$element : null;
16973 this.onDocumentMouseDownHandler = this.onDocumentMouseDown.bind( this );
16974
16975 // Initialization
16976 this.$element
16977 .addClass( 'oo-ui-menuSelectWidget' )
16978 .attr( 'role', 'menu' );
16979
16980 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
16981 // that reference properties not initialized at that time of parent class construction
16982 // TODO: Find a better way to handle post-constructor setup
16983 this.visible = false;
16984 this.$element.addClass( 'oo-ui-element-hidden' );
16985 };
16986
16987 /* Setup */
16988
16989 OO.inheritClass( OO.ui.MenuSelectWidget, OO.ui.SelectWidget );
16990 OO.mixinClass( OO.ui.MenuSelectWidget, OO.ui.mixin.ClippableElement );
16991
16992 /* Methods */
16993
16994 /**
16995 * Handles document mouse down events.
16996 *
16997 * @protected
16998 * @param {jQuery.Event} e Key down event
16999 */
17000 OO.ui.MenuSelectWidget.prototype.onDocumentMouseDown = function ( e ) {
17001 if (
17002 !OO.ui.contains( this.$element[ 0 ], e.target, true ) &&
17003 ( !this.$widget || !OO.ui.contains( this.$widget[ 0 ], e.target, true ) )
17004 ) {
17005 this.toggle( false );
17006 }
17007 };
17008
17009 /**
17010 * @inheritdoc
17011 */
17012 OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) {
17013 var currentItem = this.getHighlightedItem() || this.getSelectedItem();
17014
17015 if ( !this.isDisabled() && this.isVisible() ) {
17016 switch ( e.keyCode ) {
17017 case OO.ui.Keys.LEFT:
17018 case OO.ui.Keys.RIGHT:
17019 // Do nothing if a text field is associated, arrow keys will be handled natively
17020 if ( !this.$input ) {
17021 OO.ui.MenuSelectWidget.parent.prototype.onKeyDown.call( this, e );
17022 }
17023 break;
17024 case OO.ui.Keys.ESCAPE:
17025 case OO.ui.Keys.TAB:
17026 if ( currentItem ) {
17027 currentItem.setHighlighted( false );
17028 }
17029 this.toggle( false );
17030 // Don't prevent tabbing away, prevent defocusing
17031 if ( e.keyCode === OO.ui.Keys.ESCAPE ) {
17032 e.preventDefault();
17033 e.stopPropagation();
17034 }
17035 break;
17036 default:
17037 OO.ui.MenuSelectWidget.parent.prototype.onKeyDown.call( this, e );
17038 return;
17039 }
17040 }
17041 };
17042
17043 /**
17044 * @inheritdoc
17045 */
17046 OO.ui.MenuSelectWidget.prototype.bindKeyDownListener = function () {
17047 if ( this.$input ) {
17048 this.$input.on( 'keydown', this.onKeyDownHandler );
17049 } else {
17050 OO.ui.MenuSelectWidget.parent.prototype.bindKeyDownListener.call( this );
17051 }
17052 };
17053
17054 /**
17055 * @inheritdoc
17056 */
17057 OO.ui.MenuSelectWidget.prototype.unbindKeyDownListener = function () {
17058 if ( this.$input ) {
17059 this.$input.off( 'keydown', this.onKeyDownHandler );
17060 } else {
17061 OO.ui.MenuSelectWidget.parent.prototype.unbindKeyDownListener.call( this );
17062 }
17063 };
17064
17065 /**
17066 * @inheritdoc
17067 */
17068 OO.ui.MenuSelectWidget.prototype.bindKeyPressListener = function () {
17069 if ( !this.$input ) {
17070 OO.ui.MenuSelectWidget.parent.prototype.bindKeyPressListener.call( this );
17071 }
17072 };
17073
17074 /**
17075 * @inheritdoc
17076 */
17077 OO.ui.MenuSelectWidget.prototype.unbindKeyPressListener = function () {
17078 if ( this.$input ) {
17079 this.clearKeyPressBuffer();
17080 } else {
17081 OO.ui.MenuSelectWidget.parent.prototype.unbindKeyPressListener.call( this );
17082 }
17083 };
17084
17085 /**
17086 * Choose an item.
17087 *
17088 * When a user chooses an item, the menu is closed.
17089 *
17090 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
17091 * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
17092 * @param {OO.ui.OptionWidget} item Item to choose
17093 * @chainable
17094 */
17095 OO.ui.MenuSelectWidget.prototype.chooseItem = function ( item ) {
17096 OO.ui.MenuSelectWidget.parent.prototype.chooseItem.call( this, item );
17097 this.toggle( false );
17098 return this;
17099 };
17100
17101 /**
17102 * @inheritdoc
17103 */
17104 OO.ui.MenuSelectWidget.prototype.addItems = function ( items, index ) {
17105 var i, len, item;
17106
17107 // Parent method
17108 OO.ui.MenuSelectWidget.parent.prototype.addItems.call( this, items, index );
17109
17110 // Auto-initialize
17111 if ( !this.newItems ) {
17112 this.newItems = [];
17113 }
17114
17115 for ( i = 0, len = items.length; i < len; i++ ) {
17116 item = items[ i ];
17117 if ( this.isVisible() ) {
17118 // Defer fitting label until item has been attached
17119 item.fitLabel();
17120 } else {
17121 this.newItems.push( item );
17122 }
17123 }
17124
17125 // Reevaluate clipping
17126 this.clip();
17127
17128 return this;
17129 };
17130
17131 /**
17132 * @inheritdoc
17133 */
17134 OO.ui.MenuSelectWidget.prototype.removeItems = function ( items ) {
17135 // Parent method
17136 OO.ui.MenuSelectWidget.parent.prototype.removeItems.call( this, items );
17137
17138 // Reevaluate clipping
17139 this.clip();
17140
17141 return this;
17142 };
17143
17144 /**
17145 * @inheritdoc
17146 */
17147 OO.ui.MenuSelectWidget.prototype.clearItems = function () {
17148 // Parent method
17149 OO.ui.MenuSelectWidget.parent.prototype.clearItems.call( this );
17150
17151 // Reevaluate clipping
17152 this.clip();
17153
17154 return this;
17155 };
17156
17157 /**
17158 * @inheritdoc
17159 */
17160 OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
17161 visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
17162
17163 var i, len,
17164 change = visible !== this.isVisible();
17165
17166 // Parent method
17167 OO.ui.MenuSelectWidget.parent.prototype.toggle.call( this, visible );
17168
17169 if ( change ) {
17170 if ( visible ) {
17171 this.bindKeyDownListener();
17172 this.bindKeyPressListener();
17173
17174 if ( this.newItems && this.newItems.length ) {
17175 for ( i = 0, len = this.newItems.length; i < len; i++ ) {
17176 this.newItems[ i ].fitLabel();
17177 }
17178 this.newItems = null;
17179 }
17180 this.toggleClipping( true );
17181
17182 // Auto-hide
17183 if ( this.autoHide ) {
17184 this.getElementDocument().addEventListener(
17185 'mousedown', this.onDocumentMouseDownHandler, true
17186 );
17187 }
17188 } else {
17189 this.unbindKeyDownListener();
17190 this.unbindKeyPressListener();
17191 this.getElementDocument().removeEventListener(
17192 'mousedown', this.onDocumentMouseDownHandler, true
17193 );
17194 this.toggleClipping( false );
17195 }
17196 }
17197
17198 return this;
17199 };
17200
17201 /**
17202 * TextInputMenuSelectWidget is a menu that is specially designed to be positioned beneath
17203 * a {@link OO.ui.TextInputWidget text input} field. The menu's position is automatically
17204 * calculated and maintained when the menu is toggled or the window is resized.
17205 * See OO.ui.ComboBoxWidget for an example of a widget that uses this class.
17206 *
17207 * @class
17208 * @extends OO.ui.MenuSelectWidget
17209 *
17210 * @constructor
17211 * @param {OO.ui.TextInputWidget} inputWidget Text input widget to provide menu for
17212 * @param {Object} [config] Configuration options
17213 * @cfg {jQuery} [$container=input.$element] Element to render menu under
17214 */
17215 OO.ui.TextInputMenuSelectWidget = function OoUiTextInputMenuSelectWidget( inputWidget, config ) {
17216 // Allow passing positional parameters inside the config object
17217 if ( OO.isPlainObject( inputWidget ) && config === undefined ) {
17218 config = inputWidget;
17219 inputWidget = config.inputWidget;
17220 }
17221
17222 // Configuration initialization
17223 config = config || {};
17224
17225 // Parent constructor
17226 OO.ui.TextInputMenuSelectWidget.parent.call( this, config );
17227
17228 // Properties
17229 this.inputWidget = inputWidget;
17230 this.$container = config.$container || this.inputWidget.$element;
17231 this.onWindowResizeHandler = this.onWindowResize.bind( this );
17232
17233 // Initialization
17234 this.$element.addClass( 'oo-ui-textInputMenuSelectWidget' );
17235 };
17236
17237 /* Setup */
17238
17239 OO.inheritClass( OO.ui.TextInputMenuSelectWidget, OO.ui.MenuSelectWidget );
17240
17241 /* Methods */
17242
17243 /**
17244 * Handle window resize event.
17245 *
17246 * @private
17247 * @param {jQuery.Event} e Window resize event
17248 */
17249 OO.ui.TextInputMenuSelectWidget.prototype.onWindowResize = function () {
17250 this.position();
17251 };
17252
17253 /**
17254 * @inheritdoc
17255 */
17256 OO.ui.TextInputMenuSelectWidget.prototype.toggle = function ( visible ) {
17257 visible = visible === undefined ? !this.isVisible() : !!visible;
17258
17259 var change = visible !== this.isVisible();
17260
17261 if ( change && visible ) {
17262 // Make sure the width is set before the parent method runs.
17263 // After this we have to call this.position(); again to actually
17264 // position ourselves correctly.
17265 this.position();
17266 }
17267
17268 // Parent method
17269 OO.ui.TextInputMenuSelectWidget.parent.prototype.toggle.call( this, visible );
17270
17271 if ( change ) {
17272 if ( this.isVisible() ) {
17273 this.position();
17274 $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
17275 } else {
17276 $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
17277 }
17278 }
17279
17280 return this;
17281 };
17282
17283 /**
17284 * Position the menu.
17285 *
17286 * @private
17287 * @chainable
17288 */
17289 OO.ui.TextInputMenuSelectWidget.prototype.position = function () {
17290 var $container = this.$container,
17291 pos = OO.ui.Element.static.getRelativePosition( $container, this.$element.offsetParent() );
17292
17293 // Position under input
17294 pos.top += $container.height();
17295 this.$element.css( pos );
17296
17297 // Set width
17298 this.setIdealSize( $container.width() );
17299 // We updated the position, so re-evaluate the clipping state
17300 this.clip();
17301
17302 return this;
17303 };
17304
17305 /**
17306 * OutlineSelectWidget is a structured list that contains {@link OO.ui.OutlineOptionWidget outline options}
17307 * A set of controls can be provided with an {@link OO.ui.OutlineControlsWidget outline controls} widget.
17308 *
17309 * **Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.**
17310 *
17311 * @class
17312 * @extends OO.ui.SelectWidget
17313 * @mixins OO.ui.mixin.TabIndexedElement
17314 *
17315 * @constructor
17316 * @param {Object} [config] Configuration options
17317 */
17318 OO.ui.OutlineSelectWidget = function OoUiOutlineSelectWidget( config ) {
17319 // Parent constructor
17320 OO.ui.OutlineSelectWidget.parent.call( this, config );
17321
17322 // Mixin constructors
17323 OO.ui.mixin.TabIndexedElement.call( this, config );
17324
17325 // Events
17326 this.$element.on( {
17327 focus: this.bindKeyDownListener.bind( this ),
17328 blur: this.unbindKeyDownListener.bind( this )
17329 } );
17330
17331 // Initialization
17332 this.$element.addClass( 'oo-ui-outlineSelectWidget' );
17333 };
17334
17335 /* Setup */
17336
17337 OO.inheritClass( OO.ui.OutlineSelectWidget, OO.ui.SelectWidget );
17338 OO.mixinClass( OO.ui.OutlineSelectWidget, OO.ui.mixin.TabIndexedElement );
17339
17340 /**
17341 * TabSelectWidget is a list that contains {@link OO.ui.TabOptionWidget tab options}
17342 *
17343 * **Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}.**
17344 *
17345 * @class
17346 * @extends OO.ui.SelectWidget
17347 * @mixins OO.ui.mixin.TabIndexedElement
17348 *
17349 * @constructor
17350 * @param {Object} [config] Configuration options
17351 */
17352 OO.ui.TabSelectWidget = function OoUiTabSelectWidget( config ) {
17353 // Parent constructor
17354 OO.ui.TabSelectWidget.parent.call( this, config );
17355
17356 // Mixin constructors
17357 OO.ui.mixin.TabIndexedElement.call( this, config );
17358
17359 // Events
17360 this.$element.on( {
17361 focus: this.bindKeyDownListener.bind( this ),
17362 blur: this.unbindKeyDownListener.bind( this )
17363 } );
17364
17365 // Initialization
17366 this.$element.addClass( 'oo-ui-tabSelectWidget' );
17367 };
17368
17369 /* Setup */
17370
17371 OO.inheritClass( OO.ui.TabSelectWidget, OO.ui.SelectWidget );
17372 OO.mixinClass( OO.ui.TabSelectWidget, OO.ui.mixin.TabIndexedElement );
17373
17374 /**
17375 * NumberInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
17376 * can be entered manually) and two {@link OO.ui.ButtonWidget button widgets}
17377 * (to adjust the value in increments) to allow the user to enter a number.
17378 *
17379 * @example
17380 * // Example: A NumberInputWidget.
17381 * var numberInput = new OO.ui.NumberInputWidget( {
17382 * label: 'NumberInputWidget',
17383 * input: { value: 5, min: 1, max: 10 }
17384 * } );
17385 * $( 'body' ).append( numberInput.$element );
17386 *
17387 * @class
17388 * @extends OO.ui.Widget
17389 *
17390 * @constructor
17391 * @param {Object} [config] Configuration options
17392 * @cfg {Object} [input] Configuration options to pass to the {@link OO.ui.TextInputWidget text input widget}.
17393 * @cfg {Object} [minusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget decrementing button widget}.
17394 * @cfg {Object} [plusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget incrementing button widget}.
17395 * @cfg {boolean} [isInteger=false] Whether the field accepts only integer values.
17396 * @cfg {number} [min=-Infinity] Minimum allowed value
17397 * @cfg {number} [max=Infinity] Maximum allowed value
17398 * @cfg {number} [step=1] Delta when using the buttons or up/down arrow keys
17399 * @cfg {number|null} [pageStep] Delta when using the page-up/page-down keys. Defaults to 10 times #step.
17400 */
17401 OO.ui.NumberInputWidget = function OoUiNumberInputWidget( config ) {
17402 // Configuration initialization
17403 config = $.extend( {
17404 isInteger: false,
17405 min: -Infinity,
17406 max: Infinity,
17407 step: 1,
17408 pageStep: null
17409 }, config );
17410
17411 // Parent constructor
17412 OO.ui.NumberInputWidget.parent.call( this, config );
17413
17414 // Properties
17415 this.input = new OO.ui.TextInputWidget( $.extend(
17416 {
17417 disabled: this.isDisabled()
17418 },
17419 config.input
17420 ) );
17421 this.minusButton = new OO.ui.ButtonWidget( $.extend(
17422 {
17423 disabled: this.isDisabled(),
17424 tabIndex: -1
17425 },
17426 config.minusButton,
17427 {
17428 classes: [ 'oo-ui-numberInputWidget-minusButton' ],
17429 label: '−'
17430 }
17431 ) );
17432 this.plusButton = new OO.ui.ButtonWidget( $.extend(
17433 {
17434 disabled: this.isDisabled(),
17435 tabIndex: -1
17436 },
17437 config.plusButton,
17438 {
17439 classes: [ 'oo-ui-numberInputWidget-plusButton' ],
17440 label: '+'
17441 }
17442 ) );
17443
17444 // Events
17445 this.input.connect( this, {
17446 change: this.emit.bind( this, 'change' ),
17447 enter: this.emit.bind( this, 'enter' )
17448 } );
17449 this.input.$input.on( {
17450 keydown: this.onKeyDown.bind( this ),
17451 'wheel mousewheel DOMMouseScroll': this.onWheel.bind( this )
17452 } );
17453 this.plusButton.connect( this, {
17454 click: [ 'onButtonClick', +1 ]
17455 } );
17456 this.minusButton.connect( this, {
17457 click: [ 'onButtonClick', -1 ]
17458 } );
17459
17460 // Initialization
17461 this.setIsInteger( !!config.isInteger );
17462 this.setRange( config.min, config.max );
17463 this.setStep( config.step, config.pageStep );
17464
17465 this.$field = $( '<div>' ).addClass( 'oo-ui-numberInputWidget-field' )
17466 .append(
17467 this.minusButton.$element,
17468 this.input.$element,
17469 this.plusButton.$element
17470 );
17471 this.$element.addClass( 'oo-ui-numberInputWidget' ).append( this.$field );
17472 this.input.setValidation( this.validateNumber.bind( this ) );
17473 };
17474
17475 /* Setup */
17476
17477 OO.inheritClass( OO.ui.NumberInputWidget, OO.ui.Widget );
17478
17479 /* Events */
17480
17481 /**
17482 * A `change` event is emitted when the value of the input changes.
17483 *
17484 * @event change
17485 */
17486
17487 /**
17488 * An `enter` event is emitted when the user presses 'enter' inside the text box.
17489 *
17490 * @event enter
17491 */
17492
17493 /* Methods */
17494
17495 /**
17496 * Set whether only integers are allowed
17497 * @param {boolean} flag
17498 */
17499 OO.ui.NumberInputWidget.prototype.setIsInteger = function ( flag ) {
17500 this.isInteger = !!flag;
17501 this.input.setValidityFlag();
17502 };
17503
17504 /**
17505 * Get whether only integers are allowed
17506 * @return {boolean} Flag value
17507 */
17508 OO.ui.NumberInputWidget.prototype.getIsInteger = function () {
17509 return this.isInteger;
17510 };
17511
17512 /**
17513 * Set the range of allowed values
17514 * @param {number} min Minimum allowed value
17515 * @param {number} max Maximum allowed value
17516 */
17517 OO.ui.NumberInputWidget.prototype.setRange = function ( min, max ) {
17518 if ( min > max ) {
17519 throw new Error( 'Minimum (' + min + ') must not be greater than maximum (' + max + ')' );
17520 }
17521 this.min = min;
17522 this.max = max;
17523 this.input.setValidityFlag();
17524 };
17525
17526 /**
17527 * Get the current range
17528 * @return {number[]} Minimum and maximum values
17529 */
17530 OO.ui.NumberInputWidget.prototype.getRange = function () {
17531 return [ this.min, this.max ];
17532 };
17533
17534 /**
17535 * Set the stepping deltas
17536 * @param {number} step Normal step
17537 * @param {number|null} pageStep Page step. If null, 10 * step will be used.
17538 */
17539 OO.ui.NumberInputWidget.prototype.setStep = function ( step, pageStep ) {
17540 if ( step <= 0 ) {
17541 throw new Error( 'Step value must be positive' );
17542 }
17543 if ( pageStep === null ) {
17544 pageStep = step * 10;
17545 } else if ( pageStep <= 0 ) {
17546 throw new Error( 'Page step value must be positive' );
17547 }
17548 this.step = step;
17549 this.pageStep = pageStep;
17550 };
17551
17552 /**
17553 * Get the current stepping values
17554 * @return {number[]} Step and page step
17555 */
17556 OO.ui.NumberInputWidget.prototype.getStep = function () {
17557 return [ this.step, this.pageStep ];
17558 };
17559
17560 /**
17561 * Get the current value of the widget
17562 * @return {string}
17563 */
17564 OO.ui.NumberInputWidget.prototype.getValue = function () {
17565 return this.input.getValue();
17566 };
17567
17568 /**
17569 * Get the current value of the widget as a number
17570 * @return {number} May be NaN, or an invalid number
17571 */
17572 OO.ui.NumberInputWidget.prototype.getNumericValue = function () {
17573 return +this.input.getValue();
17574 };
17575
17576 /**
17577 * Set the value of the widget
17578 * @param {string} value Invalid values are allowed
17579 */
17580 OO.ui.NumberInputWidget.prototype.setValue = function ( value ) {
17581 this.input.setValue( value );
17582 };
17583
17584 /**
17585 * Adjust the value of the widget
17586 * @param {number} delta Adjustment amount
17587 */
17588 OO.ui.NumberInputWidget.prototype.adjustValue = function ( delta ) {
17589 var n, v = this.getNumericValue();
17590
17591 delta = +delta;
17592 if ( isNaN( delta ) || !isFinite( delta ) ) {
17593 throw new Error( 'Delta must be a finite number' );
17594 }
17595
17596 if ( isNaN( v ) ) {
17597 n = 0;
17598 } else {
17599 n = v + delta;
17600 n = Math.max( Math.min( n, this.max ), this.min );
17601 if ( this.isInteger ) {
17602 n = Math.round( n );
17603 }
17604 }
17605
17606 if ( n !== v ) {
17607 this.setValue( n );
17608 }
17609 };
17610
17611 /**
17612 * Validate input
17613 * @private
17614 * @param {string} value Field value
17615 * @return {boolean}
17616 */
17617 OO.ui.NumberInputWidget.prototype.validateNumber = function ( value ) {
17618 var n = +value;
17619 if ( isNaN( n ) || !isFinite( n ) ) {
17620 return false;
17621 }
17622
17623 /*jshint bitwise: false */
17624 if ( this.isInteger && ( n | 0 ) !== n ) {
17625 return false;
17626 }
17627 /*jshint bitwise: true */
17628
17629 if ( n < this.min || n > this.max ) {
17630 return false;
17631 }
17632
17633 return true;
17634 };
17635
17636 /**
17637 * Handle mouse click events.
17638 *
17639 * @private
17640 * @param {number} dir +1 or -1
17641 */
17642 OO.ui.NumberInputWidget.prototype.onButtonClick = function ( dir ) {
17643 this.adjustValue( dir * this.step );
17644 };
17645
17646 /**
17647 * Handle mouse wheel events.
17648 *
17649 * @private
17650 * @param {jQuery.Event} event
17651 */
17652 OO.ui.NumberInputWidget.prototype.onWheel = function ( event ) {
17653 var delta = 0;
17654
17655 // Standard 'wheel' event
17656 if ( event.originalEvent.deltaMode !== undefined ) {
17657 this.sawWheelEvent = true;
17658 }
17659 if ( event.originalEvent.deltaY ) {
17660 delta = -event.originalEvent.deltaY;
17661 } else if ( event.originalEvent.deltaX ) {
17662 delta = event.originalEvent.deltaX;
17663 }
17664
17665 // Non-standard events
17666 if ( !this.sawWheelEvent ) {
17667 if ( event.originalEvent.wheelDeltaX ) {
17668 delta = -event.originalEvent.wheelDeltaX;
17669 } else if ( event.originalEvent.wheelDeltaY ) {
17670 delta = event.originalEvent.wheelDeltaY;
17671 } else if ( event.originalEvent.wheelDelta ) {
17672 delta = event.originalEvent.wheelDelta;
17673 } else if ( event.originalEvent.detail ) {
17674 delta = -event.originalEvent.detail;
17675 }
17676 }
17677
17678 if ( delta ) {
17679 delta = delta < 0 ? -1 : 1;
17680 this.adjustValue( delta * this.step );
17681 }
17682
17683 return false;
17684 };
17685
17686 /**
17687 * Handle key down events.
17688 *
17689 *
17690 * @private
17691 * @param {jQuery.Event} e Key down event
17692 */
17693 OO.ui.NumberInputWidget.prototype.onKeyDown = function ( e ) {
17694 if ( !this.isDisabled() ) {
17695 switch ( e.which ) {
17696 case OO.ui.Keys.UP:
17697 this.adjustValue( this.step );
17698 return false;
17699 case OO.ui.Keys.DOWN:
17700 this.adjustValue( -this.step );
17701 return false;
17702 case OO.ui.Keys.PAGEUP:
17703 this.adjustValue( this.pageStep );
17704 return false;
17705 case OO.ui.Keys.PAGEDOWN:
17706 this.adjustValue( -this.pageStep );
17707 return false;
17708 }
17709 }
17710 };
17711
17712 /**
17713 * @inheritdoc
17714 */
17715 OO.ui.NumberInputWidget.prototype.setDisabled = function ( disabled ) {
17716 // Parent method
17717 OO.ui.NumberInputWidget.parent.prototype.setDisabled.call( this, disabled );
17718
17719 if ( this.input ) {
17720 this.input.setDisabled( this.isDisabled() );
17721 }
17722 if ( this.minusButton ) {
17723 this.minusButton.setDisabled( this.isDisabled() );
17724 }
17725 if ( this.plusButton ) {
17726 this.plusButton.setDisabled( this.isDisabled() );
17727 }
17728
17729 return this;
17730 };
17731
17732 /**
17733 * ToggleSwitches are switches that slide on and off. Their state is represented by a Boolean
17734 * value (`true` for ‘on’, and `false` otherwise, the default). The ‘off’ state is represented
17735 * visually by a slider in the leftmost position.
17736 *
17737 * @example
17738 * // Toggle switches in the 'off' and 'on' position.
17739 * var toggleSwitch1 = new OO.ui.ToggleSwitchWidget();
17740 * var toggleSwitch2 = new OO.ui.ToggleSwitchWidget( {
17741 * value: true
17742 * } );
17743 *
17744 * // Create a FieldsetLayout to layout and label switches
17745 * var fieldset = new OO.ui.FieldsetLayout( {
17746 * label: 'Toggle switches'
17747 * } );
17748 * fieldset.addItems( [
17749 * new OO.ui.FieldLayout( toggleSwitch1, { label: 'Off', align: 'top' } ),
17750 * new OO.ui.FieldLayout( toggleSwitch2, { label: 'On', align: 'top' } )
17751 * ] );
17752 * $( 'body' ).append( fieldset.$element );
17753 *
17754 * @class
17755 * @extends OO.ui.ToggleWidget
17756 * @mixins OO.ui.mixin.TabIndexedElement
17757 *
17758 * @constructor
17759 * @param {Object} [config] Configuration options
17760 * @cfg {boolean} [value=false] The toggle switch’s initial on/off state.
17761 * By default, the toggle switch is in the 'off' position.
17762 */
17763 OO.ui.ToggleSwitchWidget = function OoUiToggleSwitchWidget( config ) {
17764 // Parent constructor
17765 OO.ui.ToggleSwitchWidget.parent.call( this, config );
17766
17767 // Mixin constructors
17768 OO.ui.mixin.TabIndexedElement.call( this, config );
17769
17770 // Properties
17771 this.dragging = false;
17772 this.dragStart = null;
17773 this.sliding = false;
17774 this.$glow = $( '<span>' );
17775 this.$grip = $( '<span>' );
17776
17777 // Events
17778 this.$element.on( {
17779 click: this.onClick.bind( this ),
17780 keypress: this.onKeyPress.bind( this )
17781 } );
17782
17783 // Initialization
17784 this.$glow.addClass( 'oo-ui-toggleSwitchWidget-glow' );
17785 this.$grip.addClass( 'oo-ui-toggleSwitchWidget-grip' );
17786 this.$element
17787 .addClass( 'oo-ui-toggleSwitchWidget' )
17788 .attr( 'role', 'checkbox' )
17789 .append( this.$glow, this.$grip );
17790 };
17791
17792 /* Setup */
17793
17794 OO.inheritClass( OO.ui.ToggleSwitchWidget, OO.ui.ToggleWidget );
17795 OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.mixin.TabIndexedElement );
17796
17797 /* Methods */
17798
17799 /**
17800 * Handle mouse click events.
17801 *
17802 * @private
17803 * @param {jQuery.Event} e Mouse click event
17804 */
17805 OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
17806 if ( !this.isDisabled() && e.which === 1 ) {
17807 this.setValue( !this.value );
17808 }
17809 return false;
17810 };
17811
17812 /**
17813 * Handle key press events.
17814 *
17815 * @private
17816 * @param {jQuery.Event} e Key press event
17817 */
17818 OO.ui.ToggleSwitchWidget.prototype.onKeyPress = function ( e ) {
17819 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
17820 this.setValue( !this.value );
17821 return false;
17822 }
17823 };
17824
17825 /*!
17826 * Deprecated aliases for classes in the `OO.ui.mixin` namespace.
17827 */
17828
17829 /**
17830 * @inheritdoc OO.ui.mixin.ButtonElement
17831 * @deprecated Use {@link OO.ui.mixin.ButtonElement} instead.
17832 */
17833 OO.ui.ButtonElement = OO.ui.mixin.ButtonElement;
17834
17835 /**
17836 * @inheritdoc OO.ui.mixin.ClippableElement
17837 * @deprecated Use {@link OO.ui.mixin.ClippableElement} instead.
17838 */
17839 OO.ui.ClippableElement = OO.ui.mixin.ClippableElement;
17840
17841 /**
17842 * @inheritdoc OO.ui.mixin.DraggableElement
17843 * @deprecated Use {@link OO.ui.mixin.DraggableElement} instead.
17844 */
17845 OO.ui.DraggableElement = OO.ui.mixin.DraggableElement;
17846
17847 /**
17848 * @inheritdoc OO.ui.mixin.DraggableGroupElement
17849 * @deprecated Use {@link OO.ui.mixin.DraggableGroupElement} instead.
17850 */
17851 OO.ui.DraggableGroupElement = OO.ui.mixin.DraggableGroupElement;
17852
17853 /**
17854 * @inheritdoc OO.ui.mixin.FlaggedElement
17855 * @deprecated Use {@link OO.ui.mixin.FlaggedElement} instead.
17856 */
17857 OO.ui.FlaggedElement = OO.ui.mixin.FlaggedElement;
17858
17859 /**
17860 * @inheritdoc OO.ui.mixin.GroupElement
17861 * @deprecated Use {@link OO.ui.mixin.GroupElement} instead.
17862 */
17863 OO.ui.GroupElement = OO.ui.mixin.GroupElement;
17864
17865 /**
17866 * @inheritdoc OO.ui.mixin.GroupWidget
17867 * @deprecated Use {@link OO.ui.mixin.GroupWidget} instead.
17868 */
17869 OO.ui.GroupWidget = OO.ui.mixin.GroupWidget;
17870
17871 /**
17872 * @inheritdoc OO.ui.mixin.IconElement
17873 * @deprecated Use {@link OO.ui.mixin.IconElement} instead.
17874 */
17875 OO.ui.IconElement = OO.ui.mixin.IconElement;
17876
17877 /**
17878 * @inheritdoc OO.ui.mixin.IndicatorElement
17879 * @deprecated Use {@link OO.ui.mixin.IndicatorElement} instead.
17880 */
17881 OO.ui.IndicatorElement = OO.ui.mixin.IndicatorElement;
17882
17883 /**
17884 * @inheritdoc OO.ui.mixin.ItemWidget
17885 * @deprecated Use {@link OO.ui.mixin.ItemWidget} instead.
17886 */
17887 OO.ui.ItemWidget = OO.ui.mixin.ItemWidget;
17888
17889 /**
17890 * @inheritdoc OO.ui.mixin.LabelElement
17891 * @deprecated Use {@link OO.ui.mixin.LabelElement} instead.
17892 */
17893 OO.ui.LabelElement = OO.ui.mixin.LabelElement;
17894
17895 /**
17896 * @inheritdoc OO.ui.mixin.LookupElement
17897 * @deprecated Use {@link OO.ui.mixin.LookupElement} instead.
17898 */
17899 OO.ui.LookupElement = OO.ui.mixin.LookupElement;
17900
17901 /**
17902 * @inheritdoc OO.ui.mixin.PendingElement
17903 * @deprecated Use {@link OO.ui.mixin.PendingElement} instead.
17904 */
17905 OO.ui.PendingElement = OO.ui.mixin.PendingElement;
17906
17907 /**
17908 * @inheritdoc OO.ui.mixin.PopupElement
17909 * @deprecated Use {@link OO.ui.mixin.PopupElement} instead.
17910 */
17911 OO.ui.PopupElement = OO.ui.mixin.PopupElement;
17912
17913 /**
17914 * @inheritdoc OO.ui.mixin.TabIndexedElement
17915 * @deprecated Use {@link OO.ui.mixin.TabIndexedElement} instead.
17916 */
17917 OO.ui.TabIndexedElement = OO.ui.mixin.TabIndexedElement;
17918
17919 /**
17920 * @inheritdoc OO.ui.mixin.TitledElement
17921 * @deprecated Use {@link OO.ui.mixin.TitledElement} instead.
17922 */
17923 OO.ui.TitledElement = OO.ui.mixin.TitledElement;
17924
17925 }( OO ) );