Merge "Increase $wgSVGMaxSize to 5120 pixels wide (previously 2048)."
[lhc/web/wiklou.git] / resources / lib / oojs-ui / oojs-ui.js
1 /*!
2 * OOjs UI v0.12.1
3 * https://www.mediawiki.org/wiki/OOjs_UI
4 *
5 * Copyright 2011–2015 OOjs UI Team and other contributors.
6 * Released under the MIT license
7 * http://oojs.mit-license.org
8 *
9 * Date: 2015-07-22T19:37:07Z
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 OO.ui.Element.static.getDir( this.$content ) || 'ltr';
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.$document = $( this.getElementDocument() );
2375
2376 // Events
2377 this.$element.on( 'mousedown', this.onMouseDown.bind( this ) );
2378
2379 // Initialization
2380 this.$head.addClass( 'oo-ui-window-head' );
2381 this.$body.addClass( 'oo-ui-window-body' );
2382 this.$foot.addClass( 'oo-ui-window-foot' );
2383 this.$content.append( this.$head, this.$body, this.$foot );
2384
2385 return this;
2386 };
2387
2388 /**
2389 * Open the window.
2390 *
2391 * This method is a wrapper around a call to the window manager’s {@link OO.ui.WindowManager#openWindow openWindow}
2392 * method, which returns a promise resolved when the window is done opening.
2393 *
2394 * To customize the window each time it opens, use #getSetupProcess or #getReadyProcess.
2395 *
2396 * @param {Object} [data] Window opening data
2397 * @return {jQuery.Promise} Promise resolved with a value when the window is opened, or rejected
2398 * if the window fails to open. When the promise is resolved successfully, the first argument of the
2399 * value is a new promise, which is resolved when the window begins closing.
2400 * @throws {Error} An error is thrown if the window is not attached to a window manager
2401 */
2402 OO.ui.Window.prototype.open = function ( data ) {
2403 if ( !this.manager ) {
2404 throw new Error( 'Cannot open window, must be attached to a manager' );
2405 }
2406
2407 return this.manager.openWindow( this, data );
2408 };
2409
2410 /**
2411 * Close the window.
2412 *
2413 * This method is a wrapper around a call to the window
2414 * manager’s {@link OO.ui.WindowManager#closeWindow closeWindow} method,
2415 * which returns a closing promise resolved when the window is done closing.
2416 *
2417 * The window's #getHoldProcess and #getTeardownProcess methods are called during the closing
2418 * phase of the window’s lifecycle and can be used to specify closing behavior each time
2419 * the window closes.
2420 *
2421 * @param {Object} [data] Window closing data
2422 * @return {jQuery.Promise} Promise resolved when window is closed
2423 * @throws {Error} An error is thrown if the window is not attached to a window manager
2424 */
2425 OO.ui.Window.prototype.close = function ( data ) {
2426 if ( !this.manager ) {
2427 throw new Error( 'Cannot close window, must be attached to a manager' );
2428 }
2429
2430 return this.manager.closeWindow( this, data );
2431 };
2432
2433 /**
2434 * Setup window.
2435 *
2436 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2437 * by other systems.
2438 *
2439 * @param {Object} [data] Window opening data
2440 * @return {jQuery.Promise} Promise resolved when window is setup
2441 */
2442 OO.ui.Window.prototype.setup = function ( data ) {
2443 var win = this,
2444 deferred = $.Deferred();
2445
2446 this.toggle( true );
2447
2448 this.getSetupProcess( data ).execute().done( function () {
2449 // Force redraw by asking the browser to measure the elements' widths
2450 win.$element.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2451 win.$content.addClass( 'oo-ui-window-content-setup' ).width();
2452 deferred.resolve();
2453 } );
2454
2455 return deferred.promise();
2456 };
2457
2458 /**
2459 * Ready window.
2460 *
2461 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2462 * by other systems.
2463 *
2464 * @param {Object} [data] Window opening data
2465 * @return {jQuery.Promise} Promise resolved when window is ready
2466 */
2467 OO.ui.Window.prototype.ready = function ( data ) {
2468 var win = this,
2469 deferred = $.Deferred();
2470
2471 this.$content.focus();
2472 this.getReadyProcess( data ).execute().done( function () {
2473 // Force redraw by asking the browser to measure the elements' widths
2474 win.$element.addClass( 'oo-ui-window-ready' ).width();
2475 win.$content.addClass( 'oo-ui-window-content-ready' ).width();
2476 deferred.resolve();
2477 } );
2478
2479 return deferred.promise();
2480 };
2481
2482 /**
2483 * Hold window.
2484 *
2485 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2486 * by other systems.
2487 *
2488 * @param {Object} [data] Window closing data
2489 * @return {jQuery.Promise} Promise resolved when window is held
2490 */
2491 OO.ui.Window.prototype.hold = function ( data ) {
2492 var win = this,
2493 deferred = $.Deferred();
2494
2495 this.getHoldProcess( data ).execute().done( function () {
2496 // Get the focused element within the window's content
2497 var $focus = win.$content.find( OO.ui.Element.static.getDocument( win.$content ).activeElement );
2498
2499 // Blur the focused element
2500 if ( $focus.length ) {
2501 $focus[ 0 ].blur();
2502 }
2503
2504 // Force redraw by asking the browser to measure the elements' widths
2505 win.$element.removeClass( 'oo-ui-window-ready' ).width();
2506 win.$content.removeClass( 'oo-ui-window-content-ready' ).width();
2507 deferred.resolve();
2508 } );
2509
2510 return deferred.promise();
2511 };
2512
2513 /**
2514 * Teardown window.
2515 *
2516 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2517 * by other systems.
2518 *
2519 * @param {Object} [data] Window closing data
2520 * @return {jQuery.Promise} Promise resolved when window is torn down
2521 */
2522 OO.ui.Window.prototype.teardown = function ( data ) {
2523 var win = this;
2524
2525 return this.getTeardownProcess( data ).execute()
2526 .done( function () {
2527 // Force redraw by asking the browser to measure the elements' widths
2528 win.$element.removeClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2529 win.$content.removeClass( 'oo-ui-window-content-setup' ).width();
2530 win.toggle( false );
2531 } );
2532 };
2533
2534 /**
2535 * The Dialog class serves as the base class for the other types of dialogs.
2536 * Unless extended to include controls, the rendered dialog box is a simple window
2537 * that users can close by hitting the ‘Esc’ key. Dialog windows are used with OO.ui.WindowManager,
2538 * which opens, closes, and controls the presentation of the window. See the
2539 * [OOjs UI documentation on MediaWiki] [1] for more information.
2540 *
2541 * @example
2542 * // A simple dialog window.
2543 * function MyDialog( config ) {
2544 * MyDialog.parent.call( this, config );
2545 * }
2546 * OO.inheritClass( MyDialog, OO.ui.Dialog );
2547 * MyDialog.prototype.initialize = function () {
2548 * MyDialog.parent.prototype.initialize.call( this );
2549 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
2550 * this.content.$element.append( '<p>A simple dialog window. Press \'Esc\' to close.</p>' );
2551 * this.$body.append( this.content.$element );
2552 * };
2553 * MyDialog.prototype.getBodyHeight = function () {
2554 * return this.content.$element.outerHeight( true );
2555 * };
2556 * var myDialog = new MyDialog( {
2557 * size: 'medium'
2558 * } );
2559 * // Create and append a window manager, which opens and closes the window.
2560 * var windowManager = new OO.ui.WindowManager();
2561 * $( 'body' ).append( windowManager.$element );
2562 * windowManager.addWindows( [ myDialog ] );
2563 * // Open the window!
2564 * windowManager.openWindow( myDialog );
2565 *
2566 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Dialogs
2567 *
2568 * @abstract
2569 * @class
2570 * @extends OO.ui.Window
2571 * @mixins OO.ui.mixin.PendingElement
2572 *
2573 * @constructor
2574 * @param {Object} [config] Configuration options
2575 */
2576 OO.ui.Dialog = function OoUiDialog( config ) {
2577 // Parent constructor
2578 OO.ui.Dialog.parent.call( this, config );
2579
2580 // Mixin constructors
2581 OO.ui.mixin.PendingElement.call( this );
2582
2583 // Properties
2584 this.actions = new OO.ui.ActionSet();
2585 this.attachedActions = [];
2586 this.currentAction = null;
2587 this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
2588
2589 // Events
2590 this.actions.connect( this, {
2591 click: 'onActionClick',
2592 resize: 'onActionResize',
2593 change: 'onActionsChange'
2594 } );
2595
2596 // Initialization
2597 this.$element
2598 .addClass( 'oo-ui-dialog' )
2599 .attr( 'role', 'dialog' );
2600 };
2601
2602 /* Setup */
2603
2604 OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
2605 OO.mixinClass( OO.ui.Dialog, OO.ui.mixin.PendingElement );
2606
2607 /* Static Properties */
2608
2609 /**
2610 * Symbolic name of dialog.
2611 *
2612 * The dialog class must have a symbolic name in order to be registered with OO.Factory.
2613 * Please see the [OOjs UI documentation on MediaWiki] [3] for more information.
2614 *
2615 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
2616 *
2617 * @abstract
2618 * @static
2619 * @inheritable
2620 * @property {string}
2621 */
2622 OO.ui.Dialog.static.name = '';
2623
2624 /**
2625 * The dialog title.
2626 *
2627 * The title can be specified as a plaintext string, a {@link OO.ui.mixin.LabelElement Label} node, or a function
2628 * that will produce a Label node or string. The title can also be specified with data passed to the
2629 * constructor (see #getSetupProcess). In this case, the static value will be overriden.
2630 *
2631 * @abstract
2632 * @static
2633 * @inheritable
2634 * @property {jQuery|string|Function}
2635 */
2636 OO.ui.Dialog.static.title = '';
2637
2638 /**
2639 * An array of configured {@link OO.ui.ActionWidget action widgets}.
2640 *
2641 * Actions can also be specified with data passed to the constructor (see #getSetupProcess). In this case, the static
2642 * value will be overriden.
2643 *
2644 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
2645 *
2646 * @static
2647 * @inheritable
2648 * @property {Object[]}
2649 */
2650 OO.ui.Dialog.static.actions = [];
2651
2652 /**
2653 * Close the dialog when the 'Esc' key is pressed.
2654 *
2655 * @static
2656 * @abstract
2657 * @inheritable
2658 * @property {boolean}
2659 */
2660 OO.ui.Dialog.static.escapable = true;
2661
2662 /* Methods */
2663
2664 /**
2665 * Handle frame document key down events.
2666 *
2667 * @private
2668 * @param {jQuery.Event} e Key down event
2669 */
2670 OO.ui.Dialog.prototype.onDocumentKeyDown = function ( e ) {
2671 if ( e.which === OO.ui.Keys.ESCAPE ) {
2672 this.close();
2673 e.preventDefault();
2674 e.stopPropagation();
2675 }
2676 };
2677
2678 /**
2679 * Handle action resized events.
2680 *
2681 * @private
2682 * @param {OO.ui.ActionWidget} action Action that was resized
2683 */
2684 OO.ui.Dialog.prototype.onActionResize = function () {
2685 // Override in subclass
2686 };
2687
2688 /**
2689 * Handle action click events.
2690 *
2691 * @private
2692 * @param {OO.ui.ActionWidget} action Action that was clicked
2693 */
2694 OO.ui.Dialog.prototype.onActionClick = function ( action ) {
2695 if ( !this.isPending() ) {
2696 this.executeAction( action.getAction() );
2697 }
2698 };
2699
2700 /**
2701 * Handle actions change event.
2702 *
2703 * @private
2704 */
2705 OO.ui.Dialog.prototype.onActionsChange = function () {
2706 this.detachActions();
2707 if ( !this.isClosing() ) {
2708 this.attachActions();
2709 }
2710 };
2711
2712 /**
2713 * Get the set of actions used by the dialog.
2714 *
2715 * @return {OO.ui.ActionSet}
2716 */
2717 OO.ui.Dialog.prototype.getActions = function () {
2718 return this.actions;
2719 };
2720
2721 /**
2722 * Get a process for taking action.
2723 *
2724 * When you override this method, you can create a new OO.ui.Process and return it, or add additional
2725 * accept steps to the process the parent method provides using the {@link OO.ui.Process#first 'first'}
2726 * and {@link OO.ui.Process#next 'next'} methods of OO.ui.Process.
2727 *
2728 * @abstract
2729 * @param {string} [action] Symbolic name of action
2730 * @return {OO.ui.Process} Action process
2731 */
2732 OO.ui.Dialog.prototype.getActionProcess = function ( action ) {
2733 return new OO.ui.Process()
2734 .next( function () {
2735 if ( !action ) {
2736 // An empty action always closes the dialog without data, which should always be
2737 // safe and make no changes
2738 this.close();
2739 }
2740 }, this );
2741 };
2742
2743 /**
2744 * @inheritdoc
2745 *
2746 * @param {Object} [data] Dialog opening data
2747 * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use
2748 * the {@link #static-title static title}
2749 * @param {Object[]} [data.actions] List of configuration options for each
2750 * {@link OO.ui.ActionWidget action widget}, omit to use {@link #static-actions static actions}.
2751 */
2752 OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
2753 data = data || {};
2754
2755 // Parent method
2756 return OO.ui.Dialog.parent.prototype.getSetupProcess.call( this, data )
2757 .next( function () {
2758 var config = this.constructor.static,
2759 actions = data.actions !== undefined ? data.actions : config.actions;
2760
2761 this.title.setLabel(
2762 data.title !== undefined ? data.title : this.constructor.static.title
2763 );
2764 this.actions.add( this.getActionWidgets( actions ) );
2765
2766 if ( this.constructor.static.escapable ) {
2767 this.$document.on( 'keydown', this.onDocumentKeyDownHandler );
2768 }
2769 }, this );
2770 };
2771
2772 /**
2773 * @inheritdoc
2774 */
2775 OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
2776 // Parent method
2777 return OO.ui.Dialog.parent.prototype.getTeardownProcess.call( this, data )
2778 .first( function () {
2779 if ( this.constructor.static.escapable ) {
2780 this.$document.off( 'keydown', this.onDocumentKeyDownHandler );
2781 }
2782
2783 this.actions.clear();
2784 this.currentAction = null;
2785 }, this );
2786 };
2787
2788 /**
2789 * @inheritdoc
2790 */
2791 OO.ui.Dialog.prototype.initialize = function () {
2792 // Parent method
2793 OO.ui.Dialog.parent.prototype.initialize.call( this );
2794
2795 var titleId = OO.ui.generateElementId();
2796
2797 // Properties
2798 this.title = new OO.ui.LabelWidget( {
2799 id: titleId
2800 } );
2801
2802 // Initialization
2803 this.$content.addClass( 'oo-ui-dialog-content' );
2804 this.$element.attr( 'aria-labelledby', titleId );
2805 this.setPendingElement( this.$head );
2806 };
2807
2808 /**
2809 * Get action widgets from a list of configs
2810 *
2811 * @param {Object[]} actions Action widget configs
2812 * @return {OO.ui.ActionWidget[]} Action widgets
2813 */
2814 OO.ui.Dialog.prototype.getActionWidgets = function ( actions ) {
2815 var i, len, widgets = [];
2816 for ( i = 0, len = actions.length; i < len; i++ ) {
2817 widgets.push(
2818 new OO.ui.ActionWidget( actions[ i ] )
2819 );
2820 }
2821 return widgets;
2822 };
2823
2824 /**
2825 * Attach action actions.
2826 *
2827 * @protected
2828 */
2829 OO.ui.Dialog.prototype.attachActions = function () {
2830 // Remember the list of potentially attached actions
2831 this.attachedActions = this.actions.get();
2832 };
2833
2834 /**
2835 * Detach action actions.
2836 *
2837 * @protected
2838 * @chainable
2839 */
2840 OO.ui.Dialog.prototype.detachActions = function () {
2841 var i, len;
2842
2843 // Detach all actions that may have been previously attached
2844 for ( i = 0, len = this.attachedActions.length; i < len; i++ ) {
2845 this.attachedActions[ i ].$element.detach();
2846 }
2847 this.attachedActions = [];
2848 };
2849
2850 /**
2851 * Execute an action.
2852 *
2853 * @param {string} action Symbolic name of action to execute
2854 * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
2855 */
2856 OO.ui.Dialog.prototype.executeAction = function ( action ) {
2857 this.pushPending();
2858 this.currentAction = action;
2859 return this.getActionProcess( action ).execute()
2860 .always( this.popPending.bind( this ) );
2861 };
2862
2863 /**
2864 * Window managers are used to open and close {@link OO.ui.Window windows} and control their presentation.
2865 * Managed windows are mutually exclusive. If a new window is opened while a current window is opening
2866 * or is opened, the current window will be closed and any ongoing {@link OO.ui.Process process} will be cancelled. Windows
2867 * themselves are persistent and—rather than being torn down when closed—can be repopulated with the
2868 * pertinent data and reused.
2869 *
2870 * Over the lifecycle of a window, the window manager makes available three promises: `opening`,
2871 * `opened`, and `closing`, which represent the primary stages of the cycle:
2872 *
2873 * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s
2874 * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window.
2875 *
2876 * - an `opening` event is emitted with an `opening` promise
2877 * - the #getSetupDelay method is called and the returned value is used to time a pause in execution before
2878 * the window’s {@link OO.ui.Window#getSetupProcess getSetupProcess} method is called on the
2879 * window and its result executed
2880 * - a `setup` progress notification is emitted from the `opening` promise
2881 * - the #getReadyDelay method is called the returned value is used to time a pause in execution before
2882 * the window’s {@link OO.ui.Window#getReadyProcess getReadyProcess} method is called on the
2883 * window and its result executed
2884 * - a `ready` progress notification is emitted from the `opening` promise
2885 * - the `opening` promise is resolved with an `opened` promise
2886 *
2887 * **Opened**: the window is now open.
2888 *
2889 * **Closing**: the closing stage begins when the window manager's #closeWindow or the
2890 * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins
2891 * to close the window.
2892 *
2893 * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted
2894 * - the #getHoldDelay method is called and the returned value is used to time a pause in execution before
2895 * the window's {@link OO.ui.Window#getHoldProcess getHoldProces} method is called on the
2896 * window and its result executed
2897 * - a `hold` progress notification is emitted from the `closing` promise
2898 * - the #getTeardownDelay() method is called and the returned value is used to time a pause in execution before
2899 * the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method is called on the
2900 * window and its result executed
2901 * - a `teardown` progress notification is emitted from the `closing` promise
2902 * - the `closing` promise is resolved. The window is now closed
2903 *
2904 * See the [OOjs UI documentation on MediaWiki][1] for more information.
2905 *
2906 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
2907 *
2908 * @class
2909 * @extends OO.ui.Element
2910 * @mixins OO.EventEmitter
2911 *
2912 * @constructor
2913 * @param {Object} [config] Configuration options
2914 * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
2915 * Note that window classes that are instantiated with a factory must have
2916 * a {@link OO.ui.Dialog#static-name static name} property that specifies a symbolic name.
2917 * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
2918 */
2919 OO.ui.WindowManager = function OoUiWindowManager( config ) {
2920 // Configuration initialization
2921 config = config || {};
2922
2923 // Parent constructor
2924 OO.ui.WindowManager.parent.call( this, config );
2925
2926 // Mixin constructors
2927 OO.EventEmitter.call( this );
2928
2929 // Properties
2930 this.factory = config.factory;
2931 this.modal = config.modal === undefined || !!config.modal;
2932 this.windows = {};
2933 this.opening = null;
2934 this.opened = null;
2935 this.closing = null;
2936 this.preparingToOpen = null;
2937 this.preparingToClose = null;
2938 this.currentWindow = null;
2939 this.globalEvents = false;
2940 this.$ariaHidden = null;
2941 this.onWindowResizeTimeout = null;
2942 this.onWindowResizeHandler = this.onWindowResize.bind( this );
2943 this.afterWindowResizeHandler = this.afterWindowResize.bind( this );
2944
2945 // Initialization
2946 this.$element
2947 .addClass( 'oo-ui-windowManager' )
2948 .toggleClass( 'oo-ui-windowManager-modal', this.modal );
2949 };
2950
2951 /* Setup */
2952
2953 OO.inheritClass( OO.ui.WindowManager, OO.ui.Element );
2954 OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter );
2955
2956 /* Events */
2957
2958 /**
2959 * An 'opening' event is emitted when the window begins to be opened.
2960 *
2961 * @event opening
2962 * @param {OO.ui.Window} win Window that's being opened
2963 * @param {jQuery.Promise} opening An `opening` promise resolved with a value when the window is opened successfully.
2964 * When the `opening` promise is resolved, the first argument of the value is an 'opened' promise, the second argument
2965 * is the opening data. The `opening` promise emits `setup` and `ready` notifications when those processes are complete.
2966 * @param {Object} data Window opening data
2967 */
2968
2969 /**
2970 * A 'closing' event is emitted when the window begins to be closed.
2971 *
2972 * @event closing
2973 * @param {OO.ui.Window} win Window that's being closed
2974 * @param {jQuery.Promise} closing A `closing` promise is resolved with a value when the window
2975 * is closed successfully. The promise emits `hold` and `teardown` notifications when those
2976 * processes are complete. When the `closing` promise is resolved, the first argument of its value
2977 * is the closing data.
2978 * @param {Object} data Window closing data
2979 */
2980
2981 /**
2982 * A 'resize' event is emitted when a window is resized.
2983 *
2984 * @event resize
2985 * @param {OO.ui.Window} win Window that was resized
2986 */
2987
2988 /* Static Properties */
2989
2990 /**
2991 * Map of the symbolic name of each window size and its CSS properties.
2992 *
2993 * @static
2994 * @inheritable
2995 * @property {Object}
2996 */
2997 OO.ui.WindowManager.static.sizes = {
2998 small: {
2999 width: 300
3000 },
3001 medium: {
3002 width: 500
3003 },
3004 large: {
3005 width: 700
3006 },
3007 larger: {
3008 width: 900
3009 },
3010 full: {
3011 // These can be non-numeric because they are never used in calculations
3012 width: '100%',
3013 height: '100%'
3014 }
3015 };
3016
3017 /**
3018 * Symbolic name of the default window size.
3019 *
3020 * The default size is used if the window's requested size is not recognized.
3021 *
3022 * @static
3023 * @inheritable
3024 * @property {string}
3025 */
3026 OO.ui.WindowManager.static.defaultSize = 'medium';
3027
3028 /* Methods */
3029
3030 /**
3031 * Handle window resize events.
3032 *
3033 * @private
3034 * @param {jQuery.Event} e Window resize event
3035 */
3036 OO.ui.WindowManager.prototype.onWindowResize = function () {
3037 clearTimeout( this.onWindowResizeTimeout );
3038 this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 );
3039 };
3040
3041 /**
3042 * Handle window resize events.
3043 *
3044 * @private
3045 * @param {jQuery.Event} e Window resize event
3046 */
3047 OO.ui.WindowManager.prototype.afterWindowResize = function () {
3048 if ( this.currentWindow ) {
3049 this.updateWindowSize( this.currentWindow );
3050 }
3051 };
3052
3053 /**
3054 * Check if window is opening.
3055 *
3056 * @return {boolean} Window is opening
3057 */
3058 OO.ui.WindowManager.prototype.isOpening = function ( win ) {
3059 return win === this.currentWindow && !!this.opening && this.opening.state() === 'pending';
3060 };
3061
3062 /**
3063 * Check if window is closing.
3064 *
3065 * @return {boolean} Window is closing
3066 */
3067 OO.ui.WindowManager.prototype.isClosing = function ( win ) {
3068 return win === this.currentWindow && !!this.closing && this.closing.state() === 'pending';
3069 };
3070
3071 /**
3072 * Check if window is opened.
3073 *
3074 * @return {boolean} Window is opened
3075 */
3076 OO.ui.WindowManager.prototype.isOpened = function ( win ) {
3077 return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending';
3078 };
3079
3080 /**
3081 * Check if a window is being managed.
3082 *
3083 * @param {OO.ui.Window} win Window to check
3084 * @return {boolean} Window is being managed
3085 */
3086 OO.ui.WindowManager.prototype.hasWindow = function ( win ) {
3087 var name;
3088
3089 for ( name in this.windows ) {
3090 if ( this.windows[ name ] === win ) {
3091 return true;
3092 }
3093 }
3094
3095 return false;
3096 };
3097
3098 /**
3099 * Get the number of milliseconds to wait after opening begins before executing the ‘setup’ process.
3100 *
3101 * @param {OO.ui.Window} win Window being opened
3102 * @param {Object} [data] Window opening data
3103 * @return {number} Milliseconds to wait
3104 */
3105 OO.ui.WindowManager.prototype.getSetupDelay = function () {
3106 return 0;
3107 };
3108
3109 /**
3110 * Get the number of milliseconds to wait after setup has finished before executing the ‘ready’ process.
3111 *
3112 * @param {OO.ui.Window} win Window being opened
3113 * @param {Object} [data] Window opening data
3114 * @return {number} Milliseconds to wait
3115 */
3116 OO.ui.WindowManager.prototype.getReadyDelay = function () {
3117 return 0;
3118 };
3119
3120 /**
3121 * Get the number of milliseconds to wait after closing has begun before executing the 'hold' process.
3122 *
3123 * @param {OO.ui.Window} win Window being closed
3124 * @param {Object} [data] Window closing data
3125 * @return {number} Milliseconds to wait
3126 */
3127 OO.ui.WindowManager.prototype.getHoldDelay = function () {
3128 return 0;
3129 };
3130
3131 /**
3132 * Get the number of milliseconds to wait after the ‘hold’ process has finished before
3133 * executing the ‘teardown’ process.
3134 *
3135 * @param {OO.ui.Window} win Window being closed
3136 * @param {Object} [data] Window closing data
3137 * @return {number} Milliseconds to wait
3138 */
3139 OO.ui.WindowManager.prototype.getTeardownDelay = function () {
3140 return this.modal ? 250 : 0;
3141 };
3142
3143 /**
3144 * Get a window by its symbolic name.
3145 *
3146 * If the window is not yet instantiated and its symbolic name is recognized by a factory, it will be
3147 * instantiated and added to the window manager automatically. Please see the [OOjs UI documentation on MediaWiki][3]
3148 * for more information about using factories.
3149 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
3150 *
3151 * @param {string} name Symbolic name of the window
3152 * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
3153 * @throws {Error} An error is thrown if the symbolic name is not recognized by the factory.
3154 * @throws {Error} An error is thrown if the named window is not recognized as a managed window.
3155 */
3156 OO.ui.WindowManager.prototype.getWindow = function ( name ) {
3157 var deferred = $.Deferred(),
3158 win = this.windows[ name ];
3159
3160 if ( !( win instanceof OO.ui.Window ) ) {
3161 if ( this.factory ) {
3162 if ( !this.factory.lookup( name ) ) {
3163 deferred.reject( new OO.ui.Error(
3164 'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
3165 ) );
3166 } else {
3167 win = this.factory.create( name );
3168 this.addWindows( [ win ] );
3169 deferred.resolve( win );
3170 }
3171 } else {
3172 deferred.reject( new OO.ui.Error(
3173 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
3174 ) );
3175 }
3176 } else {
3177 deferred.resolve( win );
3178 }
3179
3180 return deferred.promise();
3181 };
3182
3183 /**
3184 * Get current window.
3185 *
3186 * @return {OO.ui.Window|null} Currently opening/opened/closing window
3187 */
3188 OO.ui.WindowManager.prototype.getCurrentWindow = function () {
3189 return this.currentWindow;
3190 };
3191
3192 /**
3193 * Open a window.
3194 *
3195 * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
3196 * @param {Object} [data] Window opening data
3197 * @return {jQuery.Promise} An `opening` promise resolved when the window is done opening.
3198 * See {@link #event-opening 'opening' event} for more information about `opening` promises.
3199 * @fires opening
3200 */
3201 OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
3202 var manager = this,
3203 opening = $.Deferred();
3204
3205 // Argument handling
3206 if ( typeof win === 'string' ) {
3207 return this.getWindow( win ).then( function ( win ) {
3208 return manager.openWindow( win, data );
3209 } );
3210 }
3211
3212 // Error handling
3213 if ( !this.hasWindow( win ) ) {
3214 opening.reject( new OO.ui.Error(
3215 'Cannot open window: window is not attached to manager'
3216 ) );
3217 } else if ( this.preparingToOpen || this.opening || this.opened ) {
3218 opening.reject( new OO.ui.Error(
3219 'Cannot open window: another window is opening or open'
3220 ) );
3221 }
3222
3223 // Window opening
3224 if ( opening.state() !== 'rejected' ) {
3225 // If a window is currently closing, wait for it to complete
3226 this.preparingToOpen = $.when( this.closing );
3227 // Ensure handlers get called after preparingToOpen is set
3228 this.preparingToOpen.done( function () {
3229 if ( manager.modal ) {
3230 manager.toggleGlobalEvents( true );
3231 manager.toggleAriaIsolation( true );
3232 }
3233 manager.currentWindow = win;
3234 manager.opening = opening;
3235 manager.preparingToOpen = null;
3236 manager.emit( 'opening', win, opening, data );
3237 setTimeout( function () {
3238 win.setup( data ).then( function () {
3239 manager.updateWindowSize( win );
3240 manager.opening.notify( { state: 'setup' } );
3241 setTimeout( function () {
3242 win.ready( data ).then( function () {
3243 manager.opening.notify( { state: 'ready' } );
3244 manager.opening = null;
3245 manager.opened = $.Deferred();
3246 opening.resolve( manager.opened.promise(), data );
3247 } );
3248 }, manager.getReadyDelay() );
3249 } );
3250 }, manager.getSetupDelay() );
3251 } );
3252 }
3253
3254 return opening.promise();
3255 };
3256
3257 /**
3258 * Close a window.
3259 *
3260 * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
3261 * @param {Object} [data] Window closing data
3262 * @return {jQuery.Promise} A `closing` promise resolved when the window is done closing.
3263 * See {@link #event-closing 'closing' event} for more information about closing promises.
3264 * @throws {Error} An error is thrown if the window is not managed by the window manager.
3265 * @fires closing
3266 */
3267 OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
3268 var manager = this,
3269 closing = $.Deferred(),
3270 opened;
3271
3272 // Argument handling
3273 if ( typeof win === 'string' ) {
3274 win = this.windows[ win ];
3275 } else if ( !this.hasWindow( win ) ) {
3276 win = null;
3277 }
3278
3279 // Error handling
3280 if ( !win ) {
3281 closing.reject( new OO.ui.Error(
3282 'Cannot close window: window is not attached to manager'
3283 ) );
3284 } else if ( win !== this.currentWindow ) {
3285 closing.reject( new OO.ui.Error(
3286 'Cannot close window: window already closed with different data'
3287 ) );
3288 } else if ( this.preparingToClose || this.closing ) {
3289 closing.reject( new OO.ui.Error(
3290 'Cannot close window: window already closing with different data'
3291 ) );
3292 }
3293
3294 // Window closing
3295 if ( closing.state() !== 'rejected' ) {
3296 // If the window is currently opening, close it when it's done
3297 this.preparingToClose = $.when( this.opening );
3298 // Ensure handlers get called after preparingToClose is set
3299 this.preparingToClose.done( function () {
3300 manager.closing = closing;
3301 manager.preparingToClose = null;
3302 manager.emit( 'closing', win, closing, data );
3303 opened = manager.opened;
3304 manager.opened = null;
3305 opened.resolve( closing.promise(), data );
3306 setTimeout( function () {
3307 win.hold( data ).then( function () {
3308 closing.notify( { state: 'hold' } );
3309 setTimeout( function () {
3310 win.teardown( data ).then( function () {
3311 closing.notify( { state: 'teardown' } );
3312 if ( manager.modal ) {
3313 manager.toggleGlobalEvents( false );
3314 manager.toggleAriaIsolation( false );
3315 }
3316 manager.closing = null;
3317 manager.currentWindow = null;
3318 closing.resolve( data );
3319 } );
3320 }, manager.getTeardownDelay() );
3321 } );
3322 }, manager.getHoldDelay() );
3323 } );
3324 }
3325
3326 return closing.promise();
3327 };
3328
3329 /**
3330 * Add windows to the window manager.
3331 *
3332 * Windows can be added by reference, symbolic name, or explicitly defined symbolic names.
3333 * See the [OOjs ui documentation on MediaWiki] [2] for examples.
3334 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
3335 *
3336 * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows An array of window objects specified
3337 * by reference, symbolic name, or explicitly defined symbolic names.
3338 * @throws {Error} An error is thrown if a window is added by symbolic name, but has neither an
3339 * explicit nor a statically configured symbolic name.
3340 */
3341 OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
3342 var i, len, win, name, list;
3343
3344 if ( Array.isArray( windows ) ) {
3345 // Convert to map of windows by looking up symbolic names from static configuration
3346 list = {};
3347 for ( i = 0, len = windows.length; i < len; i++ ) {
3348 name = windows[ i ].constructor.static.name;
3349 if ( typeof name !== 'string' ) {
3350 throw new Error( 'Cannot add window' );
3351 }
3352 list[ name ] = windows[ i ];
3353 }
3354 } else if ( OO.isPlainObject( windows ) ) {
3355 list = windows;
3356 }
3357
3358 // Add windows
3359 for ( name in list ) {
3360 win = list[ name ];
3361 this.windows[ name ] = win.toggle( false );
3362 this.$element.append( win.$element );
3363 win.setManager( this );
3364 }
3365 };
3366
3367 /**
3368 * Remove the specified windows from the windows manager.
3369 *
3370 * Windows will be closed before they are removed. If you wish to remove all windows, you may wish to use
3371 * the #clearWindows method instead. If you no longer need the window manager and want to ensure that it no
3372 * longer listens to events, use the #destroy method.
3373 *
3374 * @param {string[]} names Symbolic names of windows to remove
3375 * @return {jQuery.Promise} Promise resolved when window is closed and removed
3376 * @throws {Error} An error is thrown if the named windows are not managed by the window manager.
3377 */
3378 OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
3379 var i, len, win, name, cleanupWindow,
3380 manager = this,
3381 promises = [],
3382 cleanup = function ( name, win ) {
3383 delete manager.windows[ name ];
3384 win.$element.detach();
3385 };
3386
3387 for ( i = 0, len = names.length; i < len; i++ ) {
3388 name = names[ i ];
3389 win = this.windows[ name ];
3390 if ( !win ) {
3391 throw new Error( 'Cannot remove window' );
3392 }
3393 cleanupWindow = cleanup.bind( null, name, win );
3394 promises.push( this.closeWindow( name ).then( cleanupWindow, cleanupWindow ) );
3395 }
3396
3397 return $.when.apply( $, promises );
3398 };
3399
3400 /**
3401 * Remove all windows from the window manager.
3402 *
3403 * Windows will be closed before they are removed. Note that the window manager, though not in use, will still
3404 * listen to events. If the window manager will not be used again, you may wish to use the #destroy method instead.
3405 * To remove just a subset of windows, use the #removeWindows method.
3406 *
3407 * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
3408 */
3409 OO.ui.WindowManager.prototype.clearWindows = function () {
3410 return this.removeWindows( Object.keys( this.windows ) );
3411 };
3412
3413 /**
3414 * Set dialog size. In general, this method should not be called directly.
3415 *
3416 * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
3417 *
3418 * @chainable
3419 */
3420 OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) {
3421 // Bypass for non-current, and thus invisible, windows
3422 if ( win !== this.currentWindow ) {
3423 return;
3424 }
3425
3426 var viewport = OO.ui.Element.static.getDimensions( win.getElementWindow() ),
3427 sizes = this.constructor.static.sizes,
3428 size = win.getSize();
3429
3430 if ( !sizes[ size ] ) {
3431 size = this.constructor.static.defaultSize;
3432 }
3433 if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[ size ].width ) {
3434 size = 'full';
3435 }
3436
3437 this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', size === 'full' );
3438 this.$element.toggleClass( 'oo-ui-windowManager-floating', size !== 'full' );
3439 win.setDimensions( sizes[ size ] );
3440
3441 this.emit( 'resize', win );
3442
3443 return this;
3444 };
3445
3446 /**
3447 * Bind or unbind global events for scrolling.
3448 *
3449 * @private
3450 * @param {boolean} [on] Bind global events
3451 * @chainable
3452 */
3453 OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) {
3454 on = on === undefined ? !!this.globalEvents : !!on;
3455
3456 var scrollWidth, bodyMargin,
3457 $body = $( this.getElementDocument().body ),
3458 // We could have multiple window managers open so only modify
3459 // the body css at the bottom of the stack
3460 stackDepth = $body.data( 'windowManagerGlobalEvents' ) || 0 ;
3461
3462 if ( on ) {
3463 if ( !this.globalEvents ) {
3464 $( this.getElementWindow() ).on( {
3465 // Start listening for top-level window dimension changes
3466 'orientationchange resize': this.onWindowResizeHandler
3467 } );
3468 if ( stackDepth === 0 ) {
3469 scrollWidth = window.innerWidth - document.documentElement.clientWidth;
3470 bodyMargin = parseFloat( $body.css( 'margin-right' ) ) || 0;
3471 $body.css( {
3472 overflow: 'hidden',
3473 'margin-right': bodyMargin + scrollWidth
3474 } );
3475 }
3476 stackDepth++;
3477 this.globalEvents = true;
3478 }
3479 } else if ( this.globalEvents ) {
3480 $( this.getElementWindow() ).off( {
3481 // Stop listening for top-level window dimension changes
3482 'orientationchange resize': this.onWindowResizeHandler
3483 } );
3484 stackDepth--;
3485 if ( stackDepth === 0 ) {
3486 $body.css( {
3487 overflow: '',
3488 'margin-right': ''
3489 } );
3490 }
3491 this.globalEvents = false;
3492 }
3493 $body.data( 'windowManagerGlobalEvents', stackDepth );
3494
3495 return this;
3496 };
3497
3498 /**
3499 * Toggle screen reader visibility of content other than the window manager.
3500 *
3501 * @private
3502 * @param {boolean} [isolate] Make only the window manager visible to screen readers
3503 * @chainable
3504 */
3505 OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) {
3506 isolate = isolate === undefined ? !this.$ariaHidden : !!isolate;
3507
3508 if ( isolate ) {
3509 if ( !this.$ariaHidden ) {
3510 // Hide everything other than the window manager from screen readers
3511 this.$ariaHidden = $( 'body' )
3512 .children()
3513 .not( this.$element.parentsUntil( 'body' ).last() )
3514 .attr( 'aria-hidden', '' );
3515 }
3516 } else if ( this.$ariaHidden ) {
3517 // Restore screen reader visibility
3518 this.$ariaHidden.removeAttr( 'aria-hidden' );
3519 this.$ariaHidden = null;
3520 }
3521
3522 return this;
3523 };
3524
3525 /**
3526 * Destroy the window manager.
3527 *
3528 * Destroying the window manager ensures that it will no longer listen to events. If you would like to
3529 * continue using the window manager, but wish to remove all windows from it, use the #clearWindows method
3530 * instead.
3531 */
3532 OO.ui.WindowManager.prototype.destroy = function () {
3533 this.toggleGlobalEvents( false );
3534 this.toggleAriaIsolation( false );
3535 this.clearWindows();
3536 this.$element.remove();
3537 };
3538
3539 /**
3540 * Errors contain a required message (either a string or jQuery selection) that is used to describe what went wrong
3541 * in a {@link OO.ui.Process process}. The error's #recoverable and #warning configurations are used to customize the
3542 * appearance and functionality of the error interface.
3543 *
3544 * The basic error interface contains a formatted error message as well as two buttons: 'Dismiss' and 'Try again' (i.e., the error
3545 * is 'recoverable' by default). If the error is not recoverable, the 'Try again' button will not be rendered and the widget
3546 * that initiated the failed process will be disabled.
3547 *
3548 * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button, which will try the
3549 * process again.
3550 *
3551 * For an example of error interfaces, please see the [OOjs UI documentation on MediaWiki][1].
3552 *
3553 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Processes_and_errors
3554 *
3555 * @class
3556 *
3557 * @constructor
3558 * @param {string|jQuery} message Description of error
3559 * @param {Object} [config] Configuration options
3560 * @cfg {boolean} [recoverable=true] Error is recoverable.
3561 * By default, errors are recoverable, and users can try the process again.
3562 * @cfg {boolean} [warning=false] Error is a warning.
3563 * If the error is a warning, the error interface will include a
3564 * 'Dismiss' and a 'Continue' button. It is the responsibility of the developer to ensure that the warning
3565 * is not triggered a second time if the user chooses to continue.
3566 */
3567 OO.ui.Error = function OoUiError( message, config ) {
3568 // Allow passing positional parameters inside the config object
3569 if ( OO.isPlainObject( message ) && config === undefined ) {
3570 config = message;
3571 message = config.message;
3572 }
3573
3574 // Configuration initialization
3575 config = config || {};
3576
3577 // Properties
3578 this.message = message instanceof jQuery ? message : String( message );
3579 this.recoverable = config.recoverable === undefined || !!config.recoverable;
3580 this.warning = !!config.warning;
3581 };
3582
3583 /* Setup */
3584
3585 OO.initClass( OO.ui.Error );
3586
3587 /* Methods */
3588
3589 /**
3590 * Check if the error is recoverable.
3591 *
3592 * If the error is recoverable, users are able to try the process again.
3593 *
3594 * @return {boolean} Error is recoverable
3595 */
3596 OO.ui.Error.prototype.isRecoverable = function () {
3597 return this.recoverable;
3598 };
3599
3600 /**
3601 * Check if the error is a warning.
3602 *
3603 * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button.
3604 *
3605 * @return {boolean} Error is warning
3606 */
3607 OO.ui.Error.prototype.isWarning = function () {
3608 return this.warning;
3609 };
3610
3611 /**
3612 * Get error message as DOM nodes.
3613 *
3614 * @return {jQuery} Error message in DOM nodes
3615 */
3616 OO.ui.Error.prototype.getMessage = function () {
3617 return this.message instanceof jQuery ?
3618 this.message.clone() :
3619 $( '<div>' ).text( this.message ).contents();
3620 };
3621
3622 /**
3623 * Get the error message text.
3624 *
3625 * @return {string} Error message
3626 */
3627 OO.ui.Error.prototype.getMessageText = function () {
3628 return this.message instanceof jQuery ? this.message.text() : this.message;
3629 };
3630
3631 /**
3632 * Wraps an HTML snippet for use with configuration values which default
3633 * to strings. This bypasses the default html-escaping done to string
3634 * values.
3635 *
3636 * @class
3637 *
3638 * @constructor
3639 * @param {string} [content] HTML content
3640 */
3641 OO.ui.HtmlSnippet = function OoUiHtmlSnippet( content ) {
3642 // Properties
3643 this.content = content;
3644 };
3645
3646 /* Setup */
3647
3648 OO.initClass( OO.ui.HtmlSnippet );
3649
3650 /* Methods */
3651
3652 /**
3653 * Render into HTML.
3654 *
3655 * @return {string} Unchanged HTML snippet.
3656 */
3657 OO.ui.HtmlSnippet.prototype.toString = function () {
3658 return this.content;
3659 };
3660
3661 /**
3662 * A Process is a list of steps that are called in sequence. The step can be a number, a jQuery promise,
3663 * or a function:
3664 *
3665 * - **number**: the process will wait for the specified number of milliseconds before proceeding.
3666 * - **promise**: the process will continue to the next step when the promise is successfully resolved
3667 * or stop if the promise is rejected.
3668 * - **function**: the process will execute the function. The process will stop if the function returns
3669 * either a boolean `false` or a promise that is rejected; if the function returns a number, the process
3670 * will wait for that number of milliseconds before proceeding.
3671 *
3672 * If the process fails, an {@link OO.ui.Error error} is generated. Depending on how the error is
3673 * configured, users can dismiss the error and try the process again, or not. If a process is stopped,
3674 * its remaining steps will not be performed.
3675 *
3676 * @class
3677 *
3678 * @constructor
3679 * @param {number|jQuery.Promise|Function} step Number of miliseconds to wait before proceeding, promise
3680 * that must be resolved before proceeding, or a function to execute. See #createStep for more information. see #createStep for more information
3681 * @param {Object} [context=null] Execution context of the function. The context is ignored if the step is
3682 * a number or promise.
3683 * @return {Object} Step object, with `callback` and `context` properties
3684 */
3685 OO.ui.Process = function ( step, context ) {
3686 // Properties
3687 this.steps = [];
3688
3689 // Initialization
3690 if ( step !== undefined ) {
3691 this.next( step, context );
3692 }
3693 };
3694
3695 /* Setup */
3696
3697 OO.initClass( OO.ui.Process );
3698
3699 /* Methods */
3700
3701 /**
3702 * Start the process.
3703 *
3704 * @return {jQuery.Promise} Promise that is resolved when all steps have successfully completed.
3705 * If any of the steps return a promise that is rejected or a boolean false, this promise is rejected
3706 * and any remaining steps are not performed.
3707 */
3708 OO.ui.Process.prototype.execute = function () {
3709 var i, len, promise;
3710
3711 /**
3712 * Continue execution.
3713 *
3714 * @ignore
3715 * @param {Array} step A function and the context it should be called in
3716 * @return {Function} Function that continues the process
3717 */
3718 function proceed( step ) {
3719 return function () {
3720 // Execute step in the correct context
3721 var deferred,
3722 result = step.callback.call( step.context );
3723
3724 if ( result === false ) {
3725 // Use rejected promise for boolean false results
3726 return $.Deferred().reject( [] ).promise();
3727 }
3728 if ( typeof result === 'number' ) {
3729 if ( result < 0 ) {
3730 throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
3731 }
3732 // Use a delayed promise for numbers, expecting them to be in milliseconds
3733 deferred = $.Deferred();
3734 setTimeout( deferred.resolve, result );
3735 return deferred.promise();
3736 }
3737 if ( result instanceof OO.ui.Error ) {
3738 // Use rejected promise for error
3739 return $.Deferred().reject( [ result ] ).promise();
3740 }
3741 if ( Array.isArray( result ) && result.length && result[ 0 ] instanceof OO.ui.Error ) {
3742 // Use rejected promise for list of errors
3743 return $.Deferred().reject( result ).promise();
3744 }
3745 // Duck-type the object to see if it can produce a promise
3746 if ( result && $.isFunction( result.promise ) ) {
3747 // Use a promise generated from the result
3748 return result.promise();
3749 }
3750 // Use resolved promise for other results
3751 return $.Deferred().resolve().promise();
3752 };
3753 }
3754
3755 if ( this.steps.length ) {
3756 // Generate a chain reaction of promises
3757 promise = proceed( this.steps[ 0 ] )();
3758 for ( i = 1, len = this.steps.length; i < len; i++ ) {
3759 promise = promise.then( proceed( this.steps[ i ] ) );
3760 }
3761 } else {
3762 promise = $.Deferred().resolve().promise();
3763 }
3764
3765 return promise;
3766 };
3767
3768 /**
3769 * Create a process step.
3770 *
3771 * @private
3772 * @param {number|jQuery.Promise|Function} step
3773 *
3774 * - Number of milliseconds to wait before proceeding
3775 * - Promise that must be resolved before proceeding
3776 * - Function to execute
3777 * - If the function returns a boolean false the process will stop
3778 * - If the function returns a promise, the process will continue to the next
3779 * step when the promise is resolved or stop if the promise is rejected
3780 * - If the function returns a number, the process will wait for that number of
3781 * milliseconds before proceeding
3782 * @param {Object} [context=null] Execution context of the function. The context is
3783 * ignored if the step is a number or promise.
3784 * @return {Object} Step object, with `callback` and `context` properties
3785 */
3786 OO.ui.Process.prototype.createStep = function ( step, context ) {
3787 if ( typeof step === 'number' || $.isFunction( step.promise ) ) {
3788 return {
3789 callback: function () {
3790 return step;
3791 },
3792 context: null
3793 };
3794 }
3795 if ( $.isFunction( step ) ) {
3796 return {
3797 callback: step,
3798 context: context
3799 };
3800 }
3801 throw new Error( 'Cannot create process step: number, promise or function expected' );
3802 };
3803
3804 /**
3805 * Add step to the beginning of the process.
3806 *
3807 * @inheritdoc #createStep
3808 * @return {OO.ui.Process} this
3809 * @chainable
3810 */
3811 OO.ui.Process.prototype.first = function ( step, context ) {
3812 this.steps.unshift( this.createStep( step, context ) );
3813 return this;
3814 };
3815
3816 /**
3817 * Add step to the end of the process.
3818 *
3819 * @inheritdoc #createStep
3820 * @return {OO.ui.Process} this
3821 * @chainable
3822 */
3823 OO.ui.Process.prototype.next = function ( step, context ) {
3824 this.steps.push( this.createStep( step, context ) );
3825 return this;
3826 };
3827
3828 /**
3829 * A ToolFactory creates tools on demand. All tools ({@link OO.ui.Tool Tools}, {@link OO.ui.PopupTool PopupTools},
3830 * and {@link OO.ui.ToolGroupTool ToolGroupTools}) must be registered with a tool factory. Tools are
3831 * registered by their symbolic name. See {@link OO.ui.Toolbar toolbars} for an example.
3832 *
3833 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
3834 *
3835 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
3836 *
3837 * @class
3838 * @extends OO.Factory
3839 * @constructor
3840 */
3841 OO.ui.ToolFactory = function OoUiToolFactory() {
3842 // Parent constructor
3843 OO.ui.ToolFactory.parent.call( this );
3844 };
3845
3846 /* Setup */
3847
3848 OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
3849
3850 /* Methods */
3851
3852 /**
3853 * Get tools from the factory
3854 *
3855 * @param {Array} include Included tools
3856 * @param {Array} exclude Excluded tools
3857 * @param {Array} promote Promoted tools
3858 * @param {Array} demote Demoted tools
3859 * @return {string[]} List of tools
3860 */
3861 OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
3862 var i, len, included, promoted, demoted,
3863 auto = [],
3864 used = {};
3865
3866 // Collect included and not excluded tools
3867 included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
3868
3869 // Promotion
3870 promoted = this.extract( promote, used );
3871 demoted = this.extract( demote, used );
3872
3873 // Auto
3874 for ( i = 0, len = included.length; i < len; i++ ) {
3875 if ( !used[ included[ i ] ] ) {
3876 auto.push( included[ i ] );
3877 }
3878 }
3879
3880 return promoted.concat( auto ).concat( demoted );
3881 };
3882
3883 /**
3884 * Get a flat list of names from a list of names or groups.
3885 *
3886 * Tools can be specified in the following ways:
3887 *
3888 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
3889 * - All tools in a group: `{ group: 'group-name' }`
3890 * - All tools: `'*'`
3891 *
3892 * @private
3893 * @param {Array|string} collection List of tools
3894 * @param {Object} [used] Object with names that should be skipped as properties; extracted
3895 * names will be added as properties
3896 * @return {string[]} List of extracted names
3897 */
3898 OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
3899 var i, len, item, name, tool,
3900 names = [];
3901
3902 if ( collection === '*' ) {
3903 for ( name in this.registry ) {
3904 tool = this.registry[ name ];
3905 if (
3906 // Only add tools by group name when auto-add is enabled
3907 tool.static.autoAddToCatchall &&
3908 // Exclude already used tools
3909 ( !used || !used[ name ] )
3910 ) {
3911 names.push( name );
3912 if ( used ) {
3913 used[ name ] = true;
3914 }
3915 }
3916 }
3917 } else if ( Array.isArray( collection ) ) {
3918 for ( i = 0, len = collection.length; i < len; i++ ) {
3919 item = collection[ i ];
3920 // Allow plain strings as shorthand for named tools
3921 if ( typeof item === 'string' ) {
3922 item = { name: item };
3923 }
3924 if ( OO.isPlainObject( item ) ) {
3925 if ( item.group ) {
3926 for ( name in this.registry ) {
3927 tool = this.registry[ name ];
3928 if (
3929 // Include tools with matching group
3930 tool.static.group === item.group &&
3931 // Only add tools by group name when auto-add is enabled
3932 tool.static.autoAddToGroup &&
3933 // Exclude already used tools
3934 ( !used || !used[ name ] )
3935 ) {
3936 names.push( name );
3937 if ( used ) {
3938 used[ name ] = true;
3939 }
3940 }
3941 }
3942 // Include tools with matching name and exclude already used tools
3943 } else if ( item.name && ( !used || !used[ item.name ] ) ) {
3944 names.push( item.name );
3945 if ( used ) {
3946 used[ item.name ] = true;
3947 }
3948 }
3949 }
3950 }
3951 }
3952 return names;
3953 };
3954
3955 /**
3956 * ToolGroupFactories create {@link OO.ui.ToolGroup toolgroups} on demand. The toolgroup classes must
3957 * specify a symbolic name and be registered with the factory. The following classes are registered by
3958 * default:
3959 *
3960 * - {@link OO.ui.BarToolGroup BarToolGroups} (‘bar’)
3961 * - {@link OO.ui.MenuToolGroup MenuToolGroups} (‘menu’)
3962 * - {@link OO.ui.ListToolGroup ListToolGroups} (‘list’)
3963 *
3964 * See {@link OO.ui.Toolbar toolbars} for an example.
3965 *
3966 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
3967 *
3968 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
3969 * @class
3970 * @extends OO.Factory
3971 * @constructor
3972 */
3973 OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() {
3974 // Parent constructor
3975 OO.Factory.call( this );
3976
3977 var i, l,
3978 defaultClasses = this.constructor.static.getDefaultClasses();
3979
3980 // Register default toolgroups
3981 for ( i = 0, l = defaultClasses.length; i < l; i++ ) {
3982 this.register( defaultClasses[ i ] );
3983 }
3984 };
3985
3986 /* Setup */
3987
3988 OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory );
3989
3990 /* Static Methods */
3991
3992 /**
3993 * Get a default set of classes to be registered on construction.
3994 *
3995 * @return {Function[]} Default classes
3996 */
3997 OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
3998 return [
3999 OO.ui.BarToolGroup,
4000 OO.ui.ListToolGroup,
4001 OO.ui.MenuToolGroup
4002 ];
4003 };
4004
4005 /**
4006 * Theme logic.
4007 *
4008 * @abstract
4009 * @class
4010 *
4011 * @constructor
4012 * @param {Object} [config] Configuration options
4013 */
4014 OO.ui.Theme = function OoUiTheme( config ) {
4015 // Configuration initialization
4016 config = config || {};
4017 };
4018
4019 /* Setup */
4020
4021 OO.initClass( OO.ui.Theme );
4022
4023 /* Methods */
4024
4025 /**
4026 * Get a list of classes to be applied to a widget.
4027 *
4028 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
4029 * otherwise state transitions will not work properly.
4030 *
4031 * @param {OO.ui.Element} element Element for which to get classes
4032 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
4033 */
4034 OO.ui.Theme.prototype.getElementClasses = function ( /* element */ ) {
4035 return { on: [], off: [] };
4036 };
4037
4038 /**
4039 * Update CSS classes provided by the theme.
4040 *
4041 * For elements with theme logic hooks, this should be called any time there's a state change.
4042 *
4043 * @param {OO.ui.Element} element Element for which to update classes
4044 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
4045 */
4046 OO.ui.Theme.prototype.updateElementClasses = function ( element ) {
4047 var classes = this.getElementClasses( element );
4048
4049 element.$element
4050 .removeClass( classes.off.join( ' ' ) )
4051 .addClass( classes.on.join( ' ' ) );
4052 };
4053
4054 /**
4055 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
4056 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
4057 * order in which users will navigate through the focusable elements via the "tab" key.
4058 *
4059 * @example
4060 * // TabIndexedElement is mixed into the ButtonWidget class
4061 * // to provide a tabIndex property.
4062 * var button1 = new OO.ui.ButtonWidget( {
4063 * label: 'fourth',
4064 * tabIndex: 4
4065 * } );
4066 * var button2 = new OO.ui.ButtonWidget( {
4067 * label: 'second',
4068 * tabIndex: 2
4069 * } );
4070 * var button3 = new OO.ui.ButtonWidget( {
4071 * label: 'third',
4072 * tabIndex: 3
4073 * } );
4074 * var button4 = new OO.ui.ButtonWidget( {
4075 * label: 'first',
4076 * tabIndex: 1
4077 * } );
4078 * $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
4079 *
4080 * @abstract
4081 * @class
4082 *
4083 * @constructor
4084 * @param {Object} [config] Configuration options
4085 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
4086 * the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
4087 * functionality will be applied to it instead.
4088 * @cfg {number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
4089 * order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
4090 * to remove the element from the tab-navigation flow.
4091 */
4092 OO.ui.mixin.TabIndexedElement = function OoUiMixinTabIndexedElement( config ) {
4093 // Configuration initialization
4094 config = $.extend( { tabIndex: 0 }, config );
4095
4096 // Properties
4097 this.$tabIndexed = null;
4098 this.tabIndex = null;
4099
4100 // Events
4101 this.connect( this, { disable: 'onTabIndexedElementDisable' } );
4102
4103 // Initialization
4104 this.setTabIndex( config.tabIndex );
4105 this.setTabIndexedElement( config.$tabIndexed || this.$element );
4106 };
4107
4108 /* Setup */
4109
4110 OO.initClass( OO.ui.mixin.TabIndexedElement );
4111
4112 /* Methods */
4113
4114 /**
4115 * Set the element that should use the tabindex functionality.
4116 *
4117 * This method is used to retarget a tabindex mixin so that its functionality applies
4118 * to the specified element. If an element is currently using the functionality, the mixin’s
4119 * effect on that element is removed before the new element is set up.
4120 *
4121 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
4122 * @chainable
4123 */
4124 OO.ui.mixin.TabIndexedElement.prototype.setTabIndexedElement = function ( $tabIndexed ) {
4125 var tabIndex = this.tabIndex;
4126 // Remove attributes from old $tabIndexed
4127 this.setTabIndex( null );
4128 // Force update of new $tabIndexed
4129 this.$tabIndexed = $tabIndexed;
4130 this.tabIndex = tabIndex;
4131 return this.updateTabIndex();
4132 };
4133
4134 /**
4135 * Set the value of the tabindex.
4136 *
4137 * @param {number|null} tabIndex Tabindex value, or `null` for no tabindex
4138 * @chainable
4139 */
4140 OO.ui.mixin.TabIndexedElement.prototype.setTabIndex = function ( tabIndex ) {
4141 tabIndex = typeof tabIndex === 'number' ? tabIndex : null;
4142
4143 if ( this.tabIndex !== tabIndex ) {
4144 this.tabIndex = tabIndex;
4145 this.updateTabIndex();
4146 }
4147
4148 return this;
4149 };
4150
4151 /**
4152 * Update the `tabindex` attribute, in case of changes to tab index or
4153 * disabled state.
4154 *
4155 * @private
4156 * @chainable
4157 */
4158 OO.ui.mixin.TabIndexedElement.prototype.updateTabIndex = function () {
4159 if ( this.$tabIndexed ) {
4160 if ( this.tabIndex !== null ) {
4161 // Do not index over disabled elements
4162 this.$tabIndexed.attr( {
4163 tabindex: this.isDisabled() ? -1 : this.tabIndex,
4164 // ChromeVox and NVDA do not seem to inherit this from parent elements
4165 'aria-disabled': this.isDisabled().toString()
4166 } );
4167 } else {
4168 this.$tabIndexed.removeAttr( 'tabindex aria-disabled' );
4169 }
4170 }
4171 return this;
4172 };
4173
4174 /**
4175 * Handle disable events.
4176 *
4177 * @private
4178 * @param {boolean} disabled Element is disabled
4179 */
4180 OO.ui.mixin.TabIndexedElement.prototype.onTabIndexedElementDisable = function () {
4181 this.updateTabIndex();
4182 };
4183
4184 /**
4185 * Get the value of the tabindex.
4186 *
4187 * @return {number|null} Tabindex value
4188 */
4189 OO.ui.mixin.TabIndexedElement.prototype.getTabIndex = function () {
4190 return this.tabIndex;
4191 };
4192
4193 /**
4194 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
4195 * interface element that can be configured with access keys for accessibility.
4196 * See the [OOjs UI documentation on MediaWiki] [1] for examples.
4197 *
4198 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
4199 * @abstract
4200 * @class
4201 *
4202 * @constructor
4203 * @param {Object} [config] Configuration options
4204 * @cfg {jQuery} [$button] The button element created by the class.
4205 * If this configuration is omitted, the button element will use a generated `<a>`.
4206 * @cfg {boolean} [framed=true] Render the button with a frame
4207 * @cfg {string} [accessKey] Button's access key
4208 */
4209 OO.ui.mixin.ButtonElement = function OoUiMixinButtonElement( config ) {
4210 // Configuration initialization
4211 config = config || {};
4212
4213 // Properties
4214 this.$button = null;
4215 this.framed = null;
4216 this.accessKey = null;
4217 this.active = false;
4218 this.onMouseUpHandler = this.onMouseUp.bind( this );
4219 this.onMouseDownHandler = this.onMouseDown.bind( this );
4220 this.onKeyDownHandler = this.onKeyDown.bind( this );
4221 this.onKeyUpHandler = this.onKeyUp.bind( this );
4222 this.onClickHandler = this.onClick.bind( this );
4223 this.onKeyPressHandler = this.onKeyPress.bind( this );
4224
4225 // Initialization
4226 this.$element.addClass( 'oo-ui-buttonElement' );
4227 this.toggleFramed( config.framed === undefined || config.framed );
4228 this.setAccessKey( config.accessKey );
4229 this.setButtonElement( config.$button || $( '<a>' ) );
4230 };
4231
4232 /* Setup */
4233
4234 OO.initClass( OO.ui.mixin.ButtonElement );
4235
4236 /* Static Properties */
4237
4238 /**
4239 * Cancel mouse down events.
4240 *
4241 * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
4242 * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
4243 * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
4244 * parent widget.
4245 *
4246 * @static
4247 * @inheritable
4248 * @property {boolean}
4249 */
4250 OO.ui.mixin.ButtonElement.static.cancelButtonMouseDownEvents = true;
4251
4252 /* Events */
4253
4254 /**
4255 * A 'click' event is emitted when the button element is clicked.
4256 *
4257 * @event click
4258 */
4259
4260 /* Methods */
4261
4262 /**
4263 * Set the button element.
4264 *
4265 * This method is used to retarget a button mixin so that its functionality applies to
4266 * the specified button element instead of the one created by the class. If a button element
4267 * is already set, the method will remove the mixin’s effect on that element.
4268 *
4269 * @param {jQuery} $button Element to use as button
4270 */
4271 OO.ui.mixin.ButtonElement.prototype.setButtonElement = function ( $button ) {
4272 if ( this.$button ) {
4273 this.$button
4274 .removeClass( 'oo-ui-buttonElement-button' )
4275 .removeAttr( 'role accesskey' )
4276 .off( {
4277 mousedown: this.onMouseDownHandler,
4278 keydown: this.onKeyDownHandler,
4279 click: this.onClickHandler,
4280 keypress: this.onKeyPressHandler
4281 } );
4282 }
4283
4284 this.$button = $button
4285 .addClass( 'oo-ui-buttonElement-button' )
4286 .attr( { role: 'button', accesskey: this.accessKey } )
4287 .on( {
4288 mousedown: this.onMouseDownHandler,
4289 keydown: this.onKeyDownHandler,
4290 click: this.onClickHandler,
4291 keypress: this.onKeyPressHandler
4292 } );
4293 };
4294
4295 /**
4296 * Handles mouse down events.
4297 *
4298 * @protected
4299 * @param {jQuery.Event} e Mouse down event
4300 */
4301 OO.ui.mixin.ButtonElement.prototype.onMouseDown = function ( e ) {
4302 if ( this.isDisabled() || e.which !== 1 ) {
4303 return;
4304 }
4305 this.$element.addClass( 'oo-ui-buttonElement-pressed' );
4306 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
4307 // reliably remove the pressed class
4308 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
4309 // Prevent change of focus unless specifically configured otherwise
4310 if ( this.constructor.static.cancelButtonMouseDownEvents ) {
4311 return false;
4312 }
4313 };
4314
4315 /**
4316 * Handles mouse up events.
4317 *
4318 * @protected
4319 * @param {jQuery.Event} e Mouse up event
4320 */
4321 OO.ui.mixin.ButtonElement.prototype.onMouseUp = function ( e ) {
4322 if ( this.isDisabled() || e.which !== 1 ) {
4323 return;
4324 }
4325 this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
4326 // Stop listening for mouseup, since we only needed this once
4327 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
4328 };
4329
4330 /**
4331 * Handles mouse click events.
4332 *
4333 * @protected
4334 * @param {jQuery.Event} e Mouse click event
4335 * @fires click
4336 */
4337 OO.ui.mixin.ButtonElement.prototype.onClick = function ( e ) {
4338 if ( !this.isDisabled() && e.which === 1 ) {
4339 if ( this.emit( 'click' ) ) {
4340 return false;
4341 }
4342 }
4343 };
4344
4345 /**
4346 * Handles key down events.
4347 *
4348 * @protected
4349 * @param {jQuery.Event} e Key down event
4350 */
4351 OO.ui.mixin.ButtonElement.prototype.onKeyDown = function ( e ) {
4352 if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
4353 return;
4354 }
4355 this.$element.addClass( 'oo-ui-buttonElement-pressed' );
4356 // Run the keyup handler no matter where the key is when the button is let go, so we can
4357 // reliably remove the pressed class
4358 this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler, true );
4359 };
4360
4361 /**
4362 * Handles key up events.
4363 *
4364 * @protected
4365 * @param {jQuery.Event} e Key up event
4366 */
4367 OO.ui.mixin.ButtonElement.prototype.onKeyUp = function ( e ) {
4368 if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
4369 return;
4370 }
4371 this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
4372 // Stop listening for keyup, since we only needed this once
4373 this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler, true );
4374 };
4375
4376 /**
4377 * Handles key press events.
4378 *
4379 * @protected
4380 * @param {jQuery.Event} e Key press event
4381 * @fires click
4382 */
4383 OO.ui.mixin.ButtonElement.prototype.onKeyPress = function ( e ) {
4384 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
4385 if ( this.emit( 'click' ) ) {
4386 return false;
4387 }
4388 }
4389 };
4390
4391 /**
4392 * Check if button has a frame.
4393 *
4394 * @return {boolean} Button is framed
4395 */
4396 OO.ui.mixin.ButtonElement.prototype.isFramed = function () {
4397 return this.framed;
4398 };
4399
4400 /**
4401 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
4402 *
4403 * @param {boolean} [framed] Make button framed, omit to toggle
4404 * @chainable
4405 */
4406 OO.ui.mixin.ButtonElement.prototype.toggleFramed = function ( framed ) {
4407 framed = framed === undefined ? !this.framed : !!framed;
4408 if ( framed !== this.framed ) {
4409 this.framed = framed;
4410 this.$element
4411 .toggleClass( 'oo-ui-buttonElement-frameless', !framed )
4412 .toggleClass( 'oo-ui-buttonElement-framed', framed );
4413 this.updateThemeClasses();
4414 }
4415
4416 return this;
4417 };
4418
4419 /**
4420 * Set the button's access key.
4421 *
4422 * @param {string} accessKey Button's access key, use empty string to remove
4423 * @chainable
4424 */
4425 OO.ui.mixin.ButtonElement.prototype.setAccessKey = function ( accessKey ) {
4426 accessKey = typeof accessKey === 'string' && accessKey.length ? accessKey : null;
4427
4428 if ( this.accessKey !== accessKey ) {
4429 if ( this.$button ) {
4430 if ( accessKey !== null ) {
4431 this.$button.attr( 'accesskey', accessKey );
4432 } else {
4433 this.$button.removeAttr( 'accesskey' );
4434 }
4435 }
4436 this.accessKey = accessKey;
4437 }
4438
4439 return this;
4440 };
4441
4442 /**
4443 * Set the button to its 'active' state.
4444 *
4445 * The active state occurs when a {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} or
4446 * a {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} is pressed. This method does nothing
4447 * for other button types.
4448 *
4449 * @param {boolean} [value] Make button active
4450 * @chainable
4451 */
4452 OO.ui.mixin.ButtonElement.prototype.setActive = function ( value ) {
4453 this.$element.toggleClass( 'oo-ui-buttonElement-active', !!value );
4454 return this;
4455 };
4456
4457 /**
4458 * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
4459 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
4460 * items from the group is done through the interface the class provides.
4461 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
4462 *
4463 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
4464 *
4465 * @abstract
4466 * @class
4467 *
4468 * @constructor
4469 * @param {Object} [config] Configuration options
4470 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
4471 * is omitted, the group element will use a generated `<div>`.
4472 */
4473 OO.ui.mixin.GroupElement = function OoUiMixinGroupElement( config ) {
4474 // Configuration initialization
4475 config = config || {};
4476
4477 // Properties
4478 this.$group = null;
4479 this.items = [];
4480 this.aggregateItemEvents = {};
4481
4482 // Initialization
4483 this.setGroupElement( config.$group || $( '<div>' ) );
4484 };
4485
4486 /* Methods */
4487
4488 /**
4489 * Set the group element.
4490 *
4491 * If an element is already set, items will be moved to the new element.
4492 *
4493 * @param {jQuery} $group Element to use as group
4494 */
4495 OO.ui.mixin.GroupElement.prototype.setGroupElement = function ( $group ) {
4496 var i, len;
4497
4498 this.$group = $group;
4499 for ( i = 0, len = this.items.length; i < len; i++ ) {
4500 this.$group.append( this.items[ i ].$element );
4501 }
4502 };
4503
4504 /**
4505 * Check if a group contains no items.
4506 *
4507 * @return {boolean} Group is empty
4508 */
4509 OO.ui.mixin.GroupElement.prototype.isEmpty = function () {
4510 return !this.items.length;
4511 };
4512
4513 /**
4514 * Get all items in the group.
4515 *
4516 * The method returns an array of item references (e.g., [button1, button2, button3]) and is useful
4517 * when synchronizing groups of items, or whenever the references are required (e.g., when removing items
4518 * from a group).
4519 *
4520 * @return {OO.ui.Element[]} An array of items.
4521 */
4522 OO.ui.mixin.GroupElement.prototype.getItems = function () {
4523 return this.items.slice( 0 );
4524 };
4525
4526 /**
4527 * Get an item by its data.
4528 *
4529 * Only the first item with matching data will be returned. To return all matching items,
4530 * use the #getItemsFromData method.
4531 *
4532 * @param {Object} data Item data to search for
4533 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
4534 */
4535 OO.ui.mixin.GroupElement.prototype.getItemFromData = function ( data ) {
4536 var i, len, item,
4537 hash = OO.getHash( data );
4538
4539 for ( i = 0, len = this.items.length; i < len; i++ ) {
4540 item = this.items[ i ];
4541 if ( hash === OO.getHash( item.getData() ) ) {
4542 return item;
4543 }
4544 }
4545
4546 return null;
4547 };
4548
4549 /**
4550 * Get items by their data.
4551 *
4552 * All items with matching data will be returned. To return only the first match, use the #getItemFromData method instead.
4553 *
4554 * @param {Object} data Item data to search for
4555 * @return {OO.ui.Element[]} Items with equivalent data
4556 */
4557 OO.ui.mixin.GroupElement.prototype.getItemsFromData = function ( data ) {
4558 var i, len, item,
4559 hash = OO.getHash( data ),
4560 items = [];
4561
4562 for ( i = 0, len = this.items.length; i < len; i++ ) {
4563 item = this.items[ i ];
4564 if ( hash === OO.getHash( item.getData() ) ) {
4565 items.push( item );
4566 }
4567 }
4568
4569 return items;
4570 };
4571
4572 /**
4573 * Aggregate the events emitted by the group.
4574 *
4575 * When events are aggregated, the group will listen to all contained items for the event,
4576 * and then emit the event under a new name. The new event will contain an additional leading
4577 * parameter containing the item that emitted the original event. Other arguments emitted from
4578 * the original event are passed through.
4579 *
4580 * @param {Object.<string,string|null>} events An object keyed by the name of the event that should be
4581 * aggregated (e.g., ‘click’) and the value of the new name to use (e.g., ‘groupClick’).
4582 * A `null` value will remove aggregated events.
4583
4584 * @throws {Error} An error is thrown if aggregation already exists.
4585 */
4586 OO.ui.mixin.GroupElement.prototype.aggregate = function ( events ) {
4587 var i, len, item, add, remove, itemEvent, groupEvent;
4588
4589 for ( itemEvent in events ) {
4590 groupEvent = events[ itemEvent ];
4591
4592 // Remove existing aggregated event
4593 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4594 // Don't allow duplicate aggregations
4595 if ( groupEvent ) {
4596 throw new Error( 'Duplicate item event aggregation for ' + itemEvent );
4597 }
4598 // Remove event aggregation from existing items
4599 for ( i = 0, len = this.items.length; i < len; i++ ) {
4600 item = this.items[ i ];
4601 if ( item.connect && item.disconnect ) {
4602 remove = {};
4603 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[itemEvent], item ];
4604 item.disconnect( this, remove );
4605 }
4606 }
4607 // Prevent future items from aggregating event
4608 delete this.aggregateItemEvents[ itemEvent ];
4609 }
4610
4611 // Add new aggregate event
4612 if ( groupEvent ) {
4613 // Make future items aggregate event
4614 this.aggregateItemEvents[ itemEvent ] = groupEvent;
4615 // Add event aggregation to existing items
4616 for ( i = 0, len = this.items.length; i < len; i++ ) {
4617 item = this.items[ i ];
4618 if ( item.connect && item.disconnect ) {
4619 add = {};
4620 add[ itemEvent ] = [ 'emit', groupEvent, item ];
4621 item.connect( this, add );
4622 }
4623 }
4624 }
4625 }
4626 };
4627
4628 /**
4629 * Add items to the group.
4630 *
4631 * Items will be added to the end of the group array unless the optional `index` parameter specifies
4632 * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
4633 *
4634 * @param {OO.ui.Element[]} items An array of items to add to the group
4635 * @param {number} [index] Index of the insertion point
4636 * @chainable
4637 */
4638 OO.ui.mixin.GroupElement.prototype.addItems = function ( items, index ) {
4639 var i, len, item, event, events, currentIndex,
4640 itemElements = [];
4641
4642 for ( i = 0, len = items.length; i < len; i++ ) {
4643 item = items[ i ];
4644
4645 // Check if item exists then remove it first, effectively "moving" it
4646 currentIndex = $.inArray( item, this.items );
4647 if ( currentIndex >= 0 ) {
4648 this.removeItems( [ item ] );
4649 // Adjust index to compensate for removal
4650 if ( currentIndex < index ) {
4651 index--;
4652 }
4653 }
4654 // Add the item
4655 if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
4656 events = {};
4657 for ( event in this.aggregateItemEvents ) {
4658 events[ event ] = [ 'emit', this.aggregateItemEvents[ event ], item ];
4659 }
4660 item.connect( this, events );
4661 }
4662 item.setElementGroup( this );
4663 itemElements.push( item.$element.get( 0 ) );
4664 }
4665
4666 if ( index === undefined || index < 0 || index >= this.items.length ) {
4667 this.$group.append( itemElements );
4668 this.items.push.apply( this.items, items );
4669 } else if ( index === 0 ) {
4670 this.$group.prepend( itemElements );
4671 this.items.unshift.apply( this.items, items );
4672 } else {
4673 this.items[ index ].$element.before( itemElements );
4674 this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
4675 }
4676
4677 return this;
4678 };
4679
4680 /**
4681 * Remove the specified items from a group.
4682 *
4683 * Removed items are detached (not removed) from the DOM so that they may be reused.
4684 * To remove all items from a group, you may wish to use the #clearItems method instead.
4685 *
4686 * @param {OO.ui.Element[]} items An array of items to remove
4687 * @chainable
4688 */
4689 OO.ui.mixin.GroupElement.prototype.removeItems = function ( items ) {
4690 var i, len, item, index, remove, itemEvent;
4691
4692 // Remove specific items
4693 for ( i = 0, len = items.length; i < len; i++ ) {
4694 item = items[ i ];
4695 index = $.inArray( item, this.items );
4696 if ( index !== -1 ) {
4697 if (
4698 item.connect && item.disconnect &&
4699 !$.isEmptyObject( this.aggregateItemEvents )
4700 ) {
4701 remove = {};
4702 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4703 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
4704 }
4705 item.disconnect( this, remove );
4706 }
4707 item.setElementGroup( null );
4708 this.items.splice( index, 1 );
4709 item.$element.detach();
4710 }
4711 }
4712
4713 return this;
4714 };
4715
4716 /**
4717 * Clear all items from the group.
4718 *
4719 * Cleared items are detached from the DOM, not removed, so that they may be reused.
4720 * To remove only a subset of items from a group, use the #removeItems method.
4721 *
4722 * @chainable
4723 */
4724 OO.ui.mixin.GroupElement.prototype.clearItems = function () {
4725 var i, len, item, remove, itemEvent;
4726
4727 // Remove all items
4728 for ( i = 0, len = this.items.length; i < len; i++ ) {
4729 item = this.items[ i ];
4730 if (
4731 item.connect && item.disconnect &&
4732 !$.isEmptyObject( this.aggregateItemEvents )
4733 ) {
4734 remove = {};
4735 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4736 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
4737 }
4738 item.disconnect( this, remove );
4739 }
4740 item.setElementGroup( null );
4741 item.$element.detach();
4742 }
4743
4744 this.items = [];
4745 return this;
4746 };
4747
4748 /**
4749 * DraggableElement is a mixin class used to create elements that can be clicked
4750 * and dragged by a mouse to a new position within a group. This class must be used
4751 * in conjunction with OO.ui.mixin.DraggableGroupElement, which provides a container for
4752 * the draggable elements.
4753 *
4754 * @abstract
4755 * @class
4756 *
4757 * @constructor
4758 */
4759 OO.ui.mixin.DraggableElement = function OoUiMixinDraggableElement() {
4760 // Properties
4761 this.index = null;
4762
4763 // Initialize and events
4764 this.$element
4765 .attr( 'draggable', true )
4766 .addClass( 'oo-ui-draggableElement' )
4767 .on( {
4768 dragstart: this.onDragStart.bind( this ),
4769 dragover: this.onDragOver.bind( this ),
4770 dragend: this.onDragEnd.bind( this ),
4771 drop: this.onDrop.bind( this )
4772 } );
4773 };
4774
4775 OO.initClass( OO.ui.mixin.DraggableElement );
4776
4777 /* Events */
4778
4779 /**
4780 * @event dragstart
4781 *
4782 * A dragstart event is emitted when the user clicks and begins dragging an item.
4783 * @param {OO.ui.mixin.DraggableElement} item The item the user has clicked and is dragging with the mouse.
4784 */
4785
4786 /**
4787 * @event dragend
4788 * A dragend event is emitted when the user drags an item and releases the mouse,
4789 * thus terminating the drag operation.
4790 */
4791
4792 /**
4793 * @event drop
4794 * A drop event is emitted when the user drags an item and then releases the mouse button
4795 * over a valid target.
4796 */
4797
4798 /* Static Properties */
4799
4800 /**
4801 * @inheritdoc OO.ui.mixin.ButtonElement
4802 */
4803 OO.ui.mixin.DraggableElement.static.cancelButtonMouseDownEvents = false;
4804
4805 /* Methods */
4806
4807 /**
4808 * Respond to dragstart event.
4809 *
4810 * @private
4811 * @param {jQuery.Event} event jQuery event
4812 * @fires dragstart
4813 */
4814 OO.ui.mixin.DraggableElement.prototype.onDragStart = function ( e ) {
4815 var dataTransfer = e.originalEvent.dataTransfer;
4816 // Define drop effect
4817 dataTransfer.dropEffect = 'none';
4818 dataTransfer.effectAllowed = 'move';
4819 // We must set up a dataTransfer data property or Firefox seems to
4820 // ignore the fact the element is draggable.
4821 try {
4822 dataTransfer.setData( 'application-x/OOjs-UI-draggable', this.getIndex() );
4823 } catch ( err ) {
4824 // The above is only for firefox. No need to set a catch clause
4825 // if it fails, move on.
4826 }
4827 // Add dragging class
4828 this.$element.addClass( 'oo-ui-draggableElement-dragging' );
4829 // Emit event
4830 this.emit( 'dragstart', this );
4831 return true;
4832 };
4833
4834 /**
4835 * Respond to dragend event.
4836 *
4837 * @private
4838 * @fires dragend
4839 */
4840 OO.ui.mixin.DraggableElement.prototype.onDragEnd = function () {
4841 this.$element.removeClass( 'oo-ui-draggableElement-dragging' );
4842 this.emit( 'dragend' );
4843 };
4844
4845 /**
4846 * Handle drop event.
4847 *
4848 * @private
4849 * @param {jQuery.Event} event jQuery event
4850 * @fires drop
4851 */
4852 OO.ui.mixin.DraggableElement.prototype.onDrop = function ( e ) {
4853 e.preventDefault();
4854 this.emit( 'drop', e );
4855 };
4856
4857 /**
4858 * In order for drag/drop to work, the dragover event must
4859 * return false and stop propogation.
4860 *
4861 * @private
4862 */
4863 OO.ui.mixin.DraggableElement.prototype.onDragOver = function ( e ) {
4864 e.preventDefault();
4865 };
4866
4867 /**
4868 * Set item index.
4869 * Store it in the DOM so we can access from the widget drag event
4870 *
4871 * @private
4872 * @param {number} Item index
4873 */
4874 OO.ui.mixin.DraggableElement.prototype.setIndex = function ( index ) {
4875 if ( this.index !== index ) {
4876 this.index = index;
4877 this.$element.data( 'index', index );
4878 }
4879 };
4880
4881 /**
4882 * Get item index
4883 *
4884 * @private
4885 * @return {number} Item index
4886 */
4887 OO.ui.mixin.DraggableElement.prototype.getIndex = function () {
4888 return this.index;
4889 };
4890
4891 /**
4892 * DraggableGroupElement is a mixin class used to create a group element to
4893 * contain draggable elements, which are items that can be clicked and dragged by a mouse.
4894 * The class is used with OO.ui.mixin.DraggableElement.
4895 *
4896 * @abstract
4897 * @class
4898 * @mixins OO.ui.mixin.GroupElement
4899 *
4900 * @constructor
4901 * @param {Object} [config] Configuration options
4902 * @cfg {string} [orientation] Item orientation: 'horizontal' or 'vertical'. The orientation
4903 * should match the layout of the items. Items displayed in a single row
4904 * or in several rows should use horizontal orientation. The vertical orientation should only be
4905 * used when the items are displayed in a single column. Defaults to 'vertical'
4906 */
4907 OO.ui.mixin.DraggableGroupElement = function OoUiMixinDraggableGroupElement( config ) {
4908 // Configuration initialization
4909 config = config || {};
4910
4911 // Parent constructor
4912 OO.ui.mixin.GroupElement.call( this, config );
4913
4914 // Properties
4915 this.orientation = config.orientation || 'vertical';
4916 this.dragItem = null;
4917 this.itemDragOver = null;
4918 this.itemKeys = {};
4919 this.sideInsertion = '';
4920
4921 // Events
4922 this.aggregate( {
4923 dragstart: 'itemDragStart',
4924 dragend: 'itemDragEnd',
4925 drop: 'itemDrop'
4926 } );
4927 this.connect( this, {
4928 itemDragStart: 'onItemDragStart',
4929 itemDrop: 'onItemDrop',
4930 itemDragEnd: 'onItemDragEnd'
4931 } );
4932 this.$element.on( {
4933 dragover: $.proxy( this.onDragOver, this ),
4934 dragleave: $.proxy( this.onDragLeave, this )
4935 } );
4936
4937 // Initialize
4938 if ( Array.isArray( config.items ) ) {
4939 this.addItems( config.items );
4940 }
4941 this.$placeholder = $( '<div>' )
4942 .addClass( 'oo-ui-draggableGroupElement-placeholder' );
4943 this.$element
4944 .addClass( 'oo-ui-draggableGroupElement' )
4945 .append( this.$status )
4946 .toggleClass( 'oo-ui-draggableGroupElement-horizontal', this.orientation === 'horizontal' )
4947 .prepend( this.$placeholder );
4948 };
4949
4950 /* Setup */
4951 OO.mixinClass( OO.ui.mixin.DraggableGroupElement, OO.ui.mixin.GroupElement );
4952
4953 /* Events */
4954
4955 /**
4956 * A 'reorder' event is emitted when the order of items in the group changes.
4957 *
4958 * @event reorder
4959 * @param {OO.ui.mixin.DraggableElement} item Reordered item
4960 * @param {number} [newIndex] New index for the item
4961 */
4962
4963 /* Methods */
4964
4965 /**
4966 * Respond to item drag start event
4967 *
4968 * @private
4969 * @param {OO.ui.mixin.DraggableElement} item Dragged item
4970 */
4971 OO.ui.mixin.DraggableGroupElement.prototype.onItemDragStart = function ( item ) {
4972 var i, len;
4973
4974 // Map the index of each object
4975 for ( i = 0, len = this.items.length; i < len; i++ ) {
4976 this.items[ i ].setIndex( i );
4977 }
4978
4979 if ( this.orientation === 'horizontal' ) {
4980 // Set the height of the indicator
4981 this.$placeholder.css( {
4982 height: item.$element.outerHeight(),
4983 width: 2
4984 } );
4985 } else {
4986 // Set the width of the indicator
4987 this.$placeholder.css( {
4988 height: 2,
4989 width: item.$element.outerWidth()
4990 } );
4991 }
4992 this.setDragItem( item );
4993 };
4994
4995 /**
4996 * Respond to item drag end event
4997 *
4998 * @private
4999 */
5000 OO.ui.mixin.DraggableGroupElement.prototype.onItemDragEnd = function () {
5001 this.unsetDragItem();
5002 return false;
5003 };
5004
5005 /**
5006 * Handle drop event and switch the order of the items accordingly
5007 *
5008 * @private
5009 * @param {OO.ui.mixin.DraggableElement} item Dropped item
5010 * @fires reorder
5011 */
5012 OO.ui.mixin.DraggableGroupElement.prototype.onItemDrop = function ( item ) {
5013 var toIndex = item.getIndex();
5014 // Check if the dropped item is from the current group
5015 // TODO: Figure out a way to configure a list of legally droppable
5016 // elements even if they are not yet in the list
5017 if ( this.getDragItem() ) {
5018 // If the insertion point is 'after', the insertion index
5019 // is shifted to the right (or to the left in RTL, hence 'after')
5020 if ( this.sideInsertion === 'after' ) {
5021 toIndex++;
5022 }
5023 // Emit change event
5024 this.emit( 'reorder', this.getDragItem(), toIndex );
5025 }
5026 this.unsetDragItem();
5027 // Return false to prevent propogation
5028 return false;
5029 };
5030
5031 /**
5032 * Handle dragleave event.
5033 *
5034 * @private
5035 */
5036 OO.ui.mixin.DraggableGroupElement.prototype.onDragLeave = function () {
5037 // This means the item was dragged outside the widget
5038 this.$placeholder
5039 .css( 'left', 0 )
5040 .addClass( 'oo-ui-element-hidden' );
5041 };
5042
5043 /**
5044 * Respond to dragover event
5045 *
5046 * @private
5047 * @param {jQuery.Event} event Event details
5048 */
5049 OO.ui.mixin.DraggableGroupElement.prototype.onDragOver = function ( e ) {
5050 var dragOverObj, $optionWidget, itemOffset, itemMidpoint, itemBoundingRect,
5051 itemSize, cssOutput, dragPosition, itemIndex, itemPosition,
5052 clientX = e.originalEvent.clientX,
5053 clientY = e.originalEvent.clientY;
5054
5055 // Get the OptionWidget item we are dragging over
5056 dragOverObj = this.getElementDocument().elementFromPoint( clientX, clientY );
5057 $optionWidget = $( dragOverObj ).closest( '.oo-ui-draggableElement' );
5058 if ( $optionWidget[ 0 ] ) {
5059 itemOffset = $optionWidget.offset();
5060 itemBoundingRect = $optionWidget[ 0 ].getBoundingClientRect();
5061 itemPosition = $optionWidget.position();
5062 itemIndex = $optionWidget.data( 'index' );
5063 }
5064
5065 if (
5066 itemOffset &&
5067 this.isDragging() &&
5068 itemIndex !== this.getDragItem().getIndex()
5069 ) {
5070 if ( this.orientation === 'horizontal' ) {
5071 // Calculate where the mouse is relative to the item width
5072 itemSize = itemBoundingRect.width;
5073 itemMidpoint = itemBoundingRect.left + itemSize / 2;
5074 dragPosition = clientX;
5075 // Which side of the item we hover over will dictate
5076 // where the placeholder will appear, on the left or
5077 // on the right
5078 cssOutput = {
5079 left: dragPosition < itemMidpoint ? itemPosition.left : itemPosition.left + itemSize,
5080 top: itemPosition.top
5081 };
5082 } else {
5083 // Calculate where the mouse is relative to the item height
5084 itemSize = itemBoundingRect.height;
5085 itemMidpoint = itemBoundingRect.top + itemSize / 2;
5086 dragPosition = clientY;
5087 // Which side of the item we hover over will dictate
5088 // where the placeholder will appear, on the top or
5089 // on the bottom
5090 cssOutput = {
5091 top: dragPosition < itemMidpoint ? itemPosition.top : itemPosition.top + itemSize,
5092 left: itemPosition.left
5093 };
5094 }
5095 // Store whether we are before or after an item to rearrange
5096 // For horizontal layout, we need to account for RTL, as this is flipped
5097 if ( this.orientation === 'horizontal' && this.$element.css( 'direction' ) === 'rtl' ) {
5098 this.sideInsertion = dragPosition < itemMidpoint ? 'after' : 'before';
5099 } else {
5100 this.sideInsertion = dragPosition < itemMidpoint ? 'before' : 'after';
5101 }
5102 // Add drop indicator between objects
5103 this.$placeholder
5104 .css( cssOutput )
5105 .removeClass( 'oo-ui-element-hidden' );
5106 } else {
5107 // This means the item was dragged outside the widget
5108 this.$placeholder
5109 .css( 'left', 0 )
5110 .addClass( 'oo-ui-element-hidden' );
5111 }
5112 // Prevent default
5113 e.preventDefault();
5114 };
5115
5116 /**
5117 * Set a dragged item
5118 *
5119 * @param {OO.ui.mixin.DraggableElement} item Dragged item
5120 */
5121 OO.ui.mixin.DraggableGroupElement.prototype.setDragItem = function ( item ) {
5122 this.dragItem = item;
5123 };
5124
5125 /**
5126 * Unset the current dragged item
5127 */
5128 OO.ui.mixin.DraggableGroupElement.prototype.unsetDragItem = function () {
5129 this.dragItem = null;
5130 this.itemDragOver = null;
5131 this.$placeholder.addClass( 'oo-ui-element-hidden' );
5132 this.sideInsertion = '';
5133 };
5134
5135 /**
5136 * Get the item that is currently being dragged.
5137 *
5138 * @return {OO.ui.mixin.DraggableElement|null} The currently dragged item, or `null` if no item is being dragged
5139 */
5140 OO.ui.mixin.DraggableGroupElement.prototype.getDragItem = function () {
5141 return this.dragItem;
5142 };
5143
5144 /**
5145 * Check if an item in the group is currently being dragged.
5146 *
5147 * @return {Boolean} Item is being dragged
5148 */
5149 OO.ui.mixin.DraggableGroupElement.prototype.isDragging = function () {
5150 return this.getDragItem() !== null;
5151 };
5152
5153 /**
5154 * IconElement is often mixed into other classes to generate an icon.
5155 * Icons are graphics, about the size of normal text. They are used to aid the user
5156 * in locating a control or to convey information in a space-efficient way. See the
5157 * [OOjs UI documentation on MediaWiki] [1] for a list of icons
5158 * included in the library.
5159 *
5160 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
5161 *
5162 * @abstract
5163 * @class
5164 *
5165 * @constructor
5166 * @param {Object} [config] Configuration options
5167 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
5168 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
5169 * the icon element be set to an existing icon instead of the one generated by this class, set a
5170 * value using a jQuery selection. For example:
5171 *
5172 * // Use a <div> tag instead of a <span>
5173 * $icon: $("<div>")
5174 * // Use an existing icon element instead of the one generated by the class
5175 * $icon: this.$element
5176 * // Use an icon element from a child widget
5177 * $icon: this.childwidget.$element
5178 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
5179 * symbolic names. A map is used for i18n purposes and contains a `default` icon
5180 * name and additional names keyed by language code. The `default` name is used when no icon is keyed
5181 * by the user's language.
5182 *
5183 * Example of an i18n map:
5184 *
5185 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
5186 * See the [OOjs UI documentation on MediaWiki] [2] for a list of icons included in the library.
5187 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
5188 * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
5189 * text. The icon title is displayed when users move the mouse over the icon.
5190 */
5191 OO.ui.mixin.IconElement = function OoUiMixinIconElement( config ) {
5192 // Configuration initialization
5193 config = config || {};
5194
5195 // Properties
5196 this.$icon = null;
5197 this.icon = null;
5198 this.iconTitle = null;
5199
5200 // Initialization
5201 this.setIcon( config.icon || this.constructor.static.icon );
5202 this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle );
5203 this.setIconElement( config.$icon || $( '<span>' ) );
5204 };
5205
5206 /* Setup */
5207
5208 OO.initClass( OO.ui.mixin.IconElement );
5209
5210 /* Static Properties */
5211
5212 /**
5213 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
5214 * for i18n purposes and contains a `default` icon name and additional names keyed by
5215 * language code. The `default` name is used when no icon is keyed by the user's language.
5216 *
5217 * Example of an i18n map:
5218 *
5219 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
5220 *
5221 * Note: the static property will be overridden if the #icon configuration is used.
5222 *
5223 * @static
5224 * @inheritable
5225 * @property {Object|string}
5226 */
5227 OO.ui.mixin.IconElement.static.icon = null;
5228
5229 /**
5230 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
5231 * function that returns title text, or `null` for no title.
5232 *
5233 * The static property will be overridden if the #iconTitle configuration is used.
5234 *
5235 * @static
5236 * @inheritable
5237 * @property {string|Function|null}
5238 */
5239 OO.ui.mixin.IconElement.static.iconTitle = null;
5240
5241 /* Methods */
5242
5243 /**
5244 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
5245 * applies to the specified icon element instead of the one created by the class. If an icon
5246 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
5247 * and mixin methods will no longer affect the element.
5248 *
5249 * @param {jQuery} $icon Element to use as icon
5250 */
5251 OO.ui.mixin.IconElement.prototype.setIconElement = function ( $icon ) {
5252 if ( this.$icon ) {
5253 this.$icon
5254 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon )
5255 .removeAttr( 'title' );
5256 }
5257
5258 this.$icon = $icon
5259 .addClass( 'oo-ui-iconElement-icon' )
5260 .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon );
5261 if ( this.iconTitle !== null ) {
5262 this.$icon.attr( 'title', this.iconTitle );
5263 }
5264 };
5265
5266 /**
5267 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
5268 * The icon parameter can also be set to a map of icon names. See the #icon config setting
5269 * for an example.
5270 *
5271 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
5272 * by language code, or `null` to remove the icon.
5273 * @chainable
5274 */
5275 OO.ui.mixin.IconElement.prototype.setIcon = function ( icon ) {
5276 icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
5277 icon = typeof icon === 'string' && icon.trim().length ? icon.trim() : null;
5278
5279 if ( this.icon !== icon ) {
5280 if ( this.$icon ) {
5281 if ( this.icon !== null ) {
5282 this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
5283 }
5284 if ( icon !== null ) {
5285 this.$icon.addClass( 'oo-ui-icon-' + icon );
5286 }
5287 }
5288 this.icon = icon;
5289 }
5290
5291 this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon );
5292 this.updateThemeClasses();
5293
5294 return this;
5295 };
5296
5297 /**
5298 * Set the icon title. Use `null` to remove the title.
5299 *
5300 * @param {string|Function|null} iconTitle A text string used as the icon title,
5301 * a function that returns title text, or `null` for no title.
5302 * @chainable
5303 */
5304 OO.ui.mixin.IconElement.prototype.setIconTitle = function ( iconTitle ) {
5305 iconTitle = typeof iconTitle === 'function' ||
5306 ( typeof iconTitle === 'string' && iconTitle.length ) ?
5307 OO.ui.resolveMsg( iconTitle ) : null;
5308
5309 if ( this.iconTitle !== iconTitle ) {
5310 this.iconTitle = iconTitle;
5311 if ( this.$icon ) {
5312 if ( this.iconTitle !== null ) {
5313 this.$icon.attr( 'title', iconTitle );
5314 } else {
5315 this.$icon.removeAttr( 'title' );
5316 }
5317 }
5318 }
5319
5320 return this;
5321 };
5322
5323 /**
5324 * Get the symbolic name of the icon.
5325 *
5326 * @return {string} Icon name
5327 */
5328 OO.ui.mixin.IconElement.prototype.getIcon = function () {
5329 return this.icon;
5330 };
5331
5332 /**
5333 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
5334 *
5335 * @return {string} Icon title text
5336 */
5337 OO.ui.mixin.IconElement.prototype.getIconTitle = function () {
5338 return this.iconTitle;
5339 };
5340
5341 /**
5342 * IndicatorElement is often mixed into other classes to generate an indicator.
5343 * Indicators are small graphics that are generally used in two ways:
5344 *
5345 * - To draw attention to the status of an item. For example, an indicator might be
5346 * used to show that an item in a list has errors that need to be resolved.
5347 * - To clarify the function of a control that acts in an exceptional way (a button
5348 * that opens a menu instead of performing an action directly, for example).
5349 *
5350 * For a list of indicators included in the library, please see the
5351 * [OOjs UI documentation on MediaWiki] [1].
5352 *
5353 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
5354 *
5355 * @abstract
5356 * @class
5357 *
5358 * @constructor
5359 * @param {Object} [config] Configuration options
5360 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
5361 * configuration is omitted, the indicator element will use a generated `<span>`.
5362 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
5363 * See the [OOjs UI documentation on MediaWiki][2] for a list of indicators included
5364 * in the library.
5365 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
5366 * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
5367 * or a function that returns title text. The indicator title is displayed when users move
5368 * the mouse over the indicator.
5369 */
5370 OO.ui.mixin.IndicatorElement = function OoUiMixinIndicatorElement( config ) {
5371 // Configuration initialization
5372 config = config || {};
5373
5374 // Properties
5375 this.$indicator = null;
5376 this.indicator = null;
5377 this.indicatorTitle = null;
5378
5379 // Initialization
5380 this.setIndicator( config.indicator || this.constructor.static.indicator );
5381 this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle );
5382 this.setIndicatorElement( config.$indicator || $( '<span>' ) );
5383 };
5384
5385 /* Setup */
5386
5387 OO.initClass( OO.ui.mixin.IndicatorElement );
5388
5389 /* Static Properties */
5390
5391 /**
5392 * Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
5393 * The static property will be overridden if the #indicator configuration is used.
5394 *
5395 * @static
5396 * @inheritable
5397 * @property {string|null}
5398 */
5399 OO.ui.mixin.IndicatorElement.static.indicator = null;
5400
5401 /**
5402 * A text string used as the indicator title, a function that returns title text, or `null`
5403 * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
5404 *
5405 * @static
5406 * @inheritable
5407 * @property {string|Function|null}
5408 */
5409 OO.ui.mixin.IndicatorElement.static.indicatorTitle = null;
5410
5411 /* Methods */
5412
5413 /**
5414 * Set the indicator element.
5415 *
5416 * If an element is already set, it will be cleaned up before setting up the new element.
5417 *
5418 * @param {jQuery} $indicator Element to use as indicator
5419 */
5420 OO.ui.mixin.IndicatorElement.prototype.setIndicatorElement = function ( $indicator ) {
5421 if ( this.$indicator ) {
5422 this.$indicator
5423 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator )
5424 .removeAttr( 'title' );
5425 }
5426
5427 this.$indicator = $indicator
5428 .addClass( 'oo-ui-indicatorElement-indicator' )
5429 .toggleClass( 'oo-ui-indicator-' + this.indicator, !!this.indicator );
5430 if ( this.indicatorTitle !== null ) {
5431 this.$indicator.attr( 'title', this.indicatorTitle );
5432 }
5433 };
5434
5435 /**
5436 * Set the indicator by its symbolic name: ‘alert’, ‘down’, ‘next’, ‘previous’, ‘required’, ‘up’. Use `null` to remove the indicator.
5437 *
5438 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
5439 * @chainable
5440 */
5441 OO.ui.mixin.IndicatorElement.prototype.setIndicator = function ( indicator ) {
5442 indicator = typeof indicator === 'string' && indicator.length ? indicator.trim() : null;
5443
5444 if ( this.indicator !== indicator ) {
5445 if ( this.$indicator ) {
5446 if ( this.indicator !== null ) {
5447 this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
5448 }
5449 if ( indicator !== null ) {
5450 this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
5451 }
5452 }
5453 this.indicator = indicator;
5454 }
5455
5456 this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator );
5457 this.updateThemeClasses();
5458
5459 return this;
5460 };
5461
5462 /**
5463 * Set the indicator title.
5464 *
5465 * The title is displayed when a user moves the mouse over the indicator.
5466 *
5467 * @param {string|Function|null} indicator Indicator title text, a function that returns text, or
5468 * `null` for no indicator title
5469 * @chainable
5470 */
5471 OO.ui.mixin.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
5472 indicatorTitle = typeof indicatorTitle === 'function' ||
5473 ( typeof indicatorTitle === 'string' && indicatorTitle.length ) ?
5474 OO.ui.resolveMsg( indicatorTitle ) : null;
5475
5476 if ( this.indicatorTitle !== indicatorTitle ) {
5477 this.indicatorTitle = indicatorTitle;
5478 if ( this.$indicator ) {
5479 if ( this.indicatorTitle !== null ) {
5480 this.$indicator.attr( 'title', indicatorTitle );
5481 } else {
5482 this.$indicator.removeAttr( 'title' );
5483 }
5484 }
5485 }
5486
5487 return this;
5488 };
5489
5490 /**
5491 * Get the symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
5492 *
5493 * @return {string} Symbolic name of indicator
5494 */
5495 OO.ui.mixin.IndicatorElement.prototype.getIndicator = function () {
5496 return this.indicator;
5497 };
5498
5499 /**
5500 * Get the indicator title.
5501 *
5502 * The title is displayed when a user moves the mouse over the indicator.
5503 *
5504 * @return {string} Indicator title text
5505 */
5506 OO.ui.mixin.IndicatorElement.prototype.getIndicatorTitle = function () {
5507 return this.indicatorTitle;
5508 };
5509
5510 /**
5511 * LabelElement is often mixed into other classes to generate a label, which
5512 * helps identify the function of an interface element.
5513 * See the [OOjs UI documentation on MediaWiki] [1] for more information.
5514 *
5515 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
5516 *
5517 * @abstract
5518 * @class
5519 *
5520 * @constructor
5521 * @param {Object} [config] Configuration options
5522 * @cfg {jQuery} [$label] The label element created by the class. If this
5523 * configuration is omitted, the label element will use a generated `<span>`.
5524 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified
5525 * as a plaintext string, a jQuery selection of elements, or a function that will produce a string
5526 * in the future. See the [OOjs UI documentation on MediaWiki] [2] for examples.
5527 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
5528 * @cfg {boolean} [autoFitLabel=true] Fit the label to the width of the parent element.
5529 * The label will be truncated to fit if necessary.
5530 */
5531 OO.ui.mixin.LabelElement = function OoUiMixinLabelElement( config ) {
5532 // Configuration initialization
5533 config = config || {};
5534
5535 // Properties
5536 this.$label = null;
5537 this.label = null;
5538 this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel;
5539
5540 // Initialization
5541 this.setLabel( config.label || this.constructor.static.label );
5542 this.setLabelElement( config.$label || $( '<span>' ) );
5543 };
5544
5545 /* Setup */
5546
5547 OO.initClass( OO.ui.mixin.LabelElement );
5548
5549 /* Events */
5550
5551 /**
5552 * @event labelChange
5553 * @param {string} value
5554 */
5555
5556 /* Static Properties */
5557
5558 /**
5559 * The label text. The label can be specified as a plaintext string, a function that will
5560 * produce a string in the future, or `null` for no label. The static value will
5561 * be overridden if a label is specified with the #label config option.
5562 *
5563 * @static
5564 * @inheritable
5565 * @property {string|Function|null}
5566 */
5567 OO.ui.mixin.LabelElement.static.label = null;
5568
5569 /* Methods */
5570
5571 /**
5572 * Set the label element.
5573 *
5574 * If an element is already set, it will be cleaned up before setting up the new element.
5575 *
5576 * @param {jQuery} $label Element to use as label
5577 */
5578 OO.ui.mixin.LabelElement.prototype.setLabelElement = function ( $label ) {
5579 if ( this.$label ) {
5580 this.$label.removeClass( 'oo-ui-labelElement-label' ).empty();
5581 }
5582
5583 this.$label = $label.addClass( 'oo-ui-labelElement-label' );
5584 this.setLabelContent( this.label );
5585 };
5586
5587 /**
5588 * Set the label.
5589 *
5590 * An empty string will result in the label being hidden. A string containing only whitespace will
5591 * be converted to a single `&nbsp;`.
5592 *
5593 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
5594 * text; or null for no label
5595 * @chainable
5596 */
5597 OO.ui.mixin.LabelElement.prototype.setLabel = function ( label ) {
5598 label = typeof label === 'function' ? OO.ui.resolveMsg( label ) : label;
5599 label = ( ( typeof label === 'string' && label.length ) || label instanceof jQuery || label instanceof OO.ui.HtmlSnippet ) ? label : null;
5600
5601 this.$element.toggleClass( 'oo-ui-labelElement', !!label );
5602
5603 if ( this.label !== label ) {
5604 if ( this.$label ) {
5605 this.setLabelContent( label );
5606 }
5607 this.label = label;
5608 this.emit( 'labelChange' );
5609 }
5610
5611 return this;
5612 };
5613
5614 /**
5615 * Get the label.
5616 *
5617 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
5618 * text; or null for no label
5619 */
5620 OO.ui.mixin.LabelElement.prototype.getLabel = function () {
5621 return this.label;
5622 };
5623
5624 /**
5625 * Fit the label.
5626 *
5627 * @chainable
5628 */
5629 OO.ui.mixin.LabelElement.prototype.fitLabel = function () {
5630 if ( this.$label && this.$label.autoEllipsis && this.autoFitLabel ) {
5631 this.$label.autoEllipsis( { hasSpan: false, tooltip: true } );
5632 }
5633
5634 return this;
5635 };
5636
5637 /**
5638 * Set the content of the label.
5639 *
5640 * Do not call this method until after the label element has been set by #setLabelElement.
5641 *
5642 * @private
5643 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
5644 * text; or null for no label
5645 */
5646 OO.ui.mixin.LabelElement.prototype.setLabelContent = function ( label ) {
5647 if ( typeof label === 'string' ) {
5648 if ( label.match( /^\s*$/ ) ) {
5649 // Convert whitespace only string to a single non-breaking space
5650 this.$label.html( '&nbsp;' );
5651 } else {
5652 this.$label.text( label );
5653 }
5654 } else if ( label instanceof OO.ui.HtmlSnippet ) {
5655 this.$label.html( label.toString() );
5656 } else if ( label instanceof jQuery ) {
5657 this.$label.empty().append( label );
5658 } else {
5659 this.$label.empty();
5660 }
5661 };
5662
5663 /**
5664 * LookupElement is a mixin that creates a {@link OO.ui.TextInputMenuSelectWidget menu} of suggested values for
5665 * a {@link OO.ui.TextInputWidget text input widget}. Suggested values are based on the characters the user types
5666 * into the text input field and, in general, the menu is only displayed when the user types. If a suggested value is chosen
5667 * from the lookup menu, that value becomes the value of the input field.
5668 *
5669 * Note that a new menu of suggested items is displayed when a value is chosen from the lookup menu. If this is
5670 * not the desired behavior, disable lookup menus with the #setLookupsDisabled method, then set the value, then
5671 * re-enable lookups.
5672 *
5673 * See the [OOjs UI demos][1] for an example.
5674 *
5675 * [1]: https://tools.wmflabs.org/oojs-ui/oojs-ui/demos/index.html#widgets-apex-vector-ltr
5676 *
5677 * @class
5678 * @abstract
5679 *
5680 * @constructor
5681 * @param {Object} [config] Configuration options
5682 * @cfg {jQuery} [$overlay] Overlay for the lookup menu; defaults to relative positioning
5683 * @cfg {jQuery} [$container=this.$element] The container element. The lookup menu is rendered beneath the specified element.
5684 * @cfg {boolean} [allowSuggestionsWhenEmpty=false] Request and display a lookup menu when the text input is empty.
5685 * By default, the lookup menu is not generated and displayed until the user begins to type.
5686 */
5687 OO.ui.mixin.LookupElement = function OoUiMixinLookupElement( config ) {
5688 // Configuration initialization
5689 config = config || {};
5690
5691 // Properties
5692 this.$overlay = config.$overlay || this.$element;
5693 this.lookupMenu = new OO.ui.TextInputMenuSelectWidget( this, {
5694 widget: this,
5695 input: this,
5696 $container: config.$container
5697 } );
5698
5699 this.allowSuggestionsWhenEmpty = config.allowSuggestionsWhenEmpty || false;
5700
5701 this.lookupCache = {};
5702 this.lookupQuery = null;
5703 this.lookupRequest = null;
5704 this.lookupsDisabled = false;
5705 this.lookupInputFocused = false;
5706
5707 // Events
5708 this.$input.on( {
5709 focus: this.onLookupInputFocus.bind( this ),
5710 blur: this.onLookupInputBlur.bind( this ),
5711 mousedown: this.onLookupInputMouseDown.bind( this )
5712 } );
5713 this.connect( this, { change: 'onLookupInputChange' } );
5714 this.lookupMenu.connect( this, {
5715 toggle: 'onLookupMenuToggle',
5716 choose: 'onLookupMenuItemChoose'
5717 } );
5718
5719 // Initialization
5720 this.$element.addClass( 'oo-ui-lookupElement' );
5721 this.lookupMenu.$element.addClass( 'oo-ui-lookupElement-menu' );
5722 this.$overlay.append( this.lookupMenu.$element );
5723 };
5724
5725 /* Methods */
5726
5727 /**
5728 * Handle input focus event.
5729 *
5730 * @protected
5731 * @param {jQuery.Event} e Input focus event
5732 */
5733 OO.ui.mixin.LookupElement.prototype.onLookupInputFocus = function () {
5734 this.lookupInputFocused = true;
5735 this.populateLookupMenu();
5736 };
5737
5738 /**
5739 * Handle input blur event.
5740 *
5741 * @protected
5742 * @param {jQuery.Event} e Input blur event
5743 */
5744 OO.ui.mixin.LookupElement.prototype.onLookupInputBlur = function () {
5745 this.closeLookupMenu();
5746 this.lookupInputFocused = false;
5747 };
5748
5749 /**
5750 * Handle input mouse down event.
5751 *
5752 * @protected
5753 * @param {jQuery.Event} e Input mouse down event
5754 */
5755 OO.ui.mixin.LookupElement.prototype.onLookupInputMouseDown = function () {
5756 // Only open the menu if the input was already focused.
5757 // This way we allow the user to open the menu again after closing it with Esc
5758 // by clicking in the input. Opening (and populating) the menu when initially
5759 // clicking into the input is handled by the focus handler.
5760 if ( this.lookupInputFocused && !this.lookupMenu.isVisible() ) {
5761 this.populateLookupMenu();
5762 }
5763 };
5764
5765 /**
5766 * Handle input change event.
5767 *
5768 * @protected
5769 * @param {string} value New input value
5770 */
5771 OO.ui.mixin.LookupElement.prototype.onLookupInputChange = function () {
5772 if ( this.lookupInputFocused ) {
5773 this.populateLookupMenu();
5774 }
5775 };
5776
5777 /**
5778 * Handle the lookup menu being shown/hidden.
5779 *
5780 * @protected
5781 * @param {boolean} visible Whether the lookup menu is now visible.
5782 */
5783 OO.ui.mixin.LookupElement.prototype.onLookupMenuToggle = function ( visible ) {
5784 if ( !visible ) {
5785 // When the menu is hidden, abort any active request and clear the menu.
5786 // This has to be done here in addition to closeLookupMenu(), because
5787 // MenuSelectWidget will close itself when the user presses Esc.
5788 this.abortLookupRequest();
5789 this.lookupMenu.clearItems();
5790 }
5791 };
5792
5793 /**
5794 * Handle menu item 'choose' event, updating the text input value to the value of the clicked item.
5795 *
5796 * @protected
5797 * @param {OO.ui.MenuOptionWidget} item Selected item
5798 */
5799 OO.ui.mixin.LookupElement.prototype.onLookupMenuItemChoose = function ( item ) {
5800 this.setValue( item.getData() );
5801 };
5802
5803 /**
5804 * Get lookup menu.
5805 *
5806 * @private
5807 * @return {OO.ui.TextInputMenuSelectWidget}
5808 */
5809 OO.ui.mixin.LookupElement.prototype.getLookupMenu = function () {
5810 return this.lookupMenu;
5811 };
5812
5813 /**
5814 * Disable or re-enable lookups.
5815 *
5816 * When lookups are disabled, calls to #populateLookupMenu will be ignored.
5817 *
5818 * @param {boolean} disabled Disable lookups
5819 */
5820 OO.ui.mixin.LookupElement.prototype.setLookupsDisabled = function ( disabled ) {
5821 this.lookupsDisabled = !!disabled;
5822 };
5823
5824 /**
5825 * Open the menu. If there are no entries in the menu, this does nothing.
5826 *
5827 * @private
5828 * @chainable
5829 */
5830 OO.ui.mixin.LookupElement.prototype.openLookupMenu = function () {
5831 if ( !this.lookupMenu.isEmpty() ) {
5832 this.lookupMenu.toggle( true );
5833 }
5834 return this;
5835 };
5836
5837 /**
5838 * Close the menu, empty it, and abort any pending request.
5839 *
5840 * @private
5841 * @chainable
5842 */
5843 OO.ui.mixin.LookupElement.prototype.closeLookupMenu = function () {
5844 this.lookupMenu.toggle( false );
5845 this.abortLookupRequest();
5846 this.lookupMenu.clearItems();
5847 return this;
5848 };
5849
5850 /**
5851 * Request menu items based on the input's current value, and when they arrive,
5852 * populate the menu with these items and show the menu.
5853 *
5854 * If lookups have been disabled with #setLookupsDisabled, this function does nothing.
5855 *
5856 * @private
5857 * @chainable
5858 */
5859 OO.ui.mixin.LookupElement.prototype.populateLookupMenu = function () {
5860 var widget = this,
5861 value = this.getValue();
5862
5863 if ( this.lookupsDisabled ) {
5864 return;
5865 }
5866
5867 // If the input is empty, clear the menu, unless suggestions when empty are allowed.
5868 if ( !this.allowSuggestionsWhenEmpty && value === '' ) {
5869 this.closeLookupMenu();
5870 // Skip population if there is already a request pending for the current value
5871 } else if ( value !== this.lookupQuery ) {
5872 this.getLookupMenuItems()
5873 .done( function ( items ) {
5874 widget.lookupMenu.clearItems();
5875 if ( items.length ) {
5876 widget.lookupMenu
5877 .addItems( items )
5878 .toggle( true );
5879 widget.initializeLookupMenuSelection();
5880 } else {
5881 widget.lookupMenu.toggle( false );
5882 }
5883 } )
5884 .fail( function () {
5885 widget.lookupMenu.clearItems();
5886 } );
5887 }
5888
5889 return this;
5890 };
5891
5892 /**
5893 * Highlight the first selectable item in the menu.
5894 *
5895 * @private
5896 * @chainable
5897 */
5898 OO.ui.mixin.LookupElement.prototype.initializeLookupMenuSelection = function () {
5899 if ( !this.lookupMenu.getSelectedItem() ) {
5900 this.lookupMenu.highlightItem( this.lookupMenu.getFirstSelectableItem() );
5901 }
5902 };
5903
5904 /**
5905 * Get lookup menu items for the current query.
5906 *
5907 * @private
5908 * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument of
5909 * the done event. If the request was aborted to make way for a subsequent request, this promise
5910 * will not be rejected: it will remain pending forever.
5911 */
5912 OO.ui.mixin.LookupElement.prototype.getLookupMenuItems = function () {
5913 var widget = this,
5914 value = this.getValue(),
5915 deferred = $.Deferred(),
5916 ourRequest;
5917
5918 this.abortLookupRequest();
5919 if ( Object.prototype.hasOwnProperty.call( this.lookupCache, value ) ) {
5920 deferred.resolve( this.getLookupMenuOptionsFromData( this.lookupCache[ value ] ) );
5921 } else {
5922 this.pushPending();
5923 this.lookupQuery = value;
5924 ourRequest = this.lookupRequest = this.getLookupRequest();
5925 ourRequest
5926 .always( function () {
5927 // We need to pop pending even if this is an old request, otherwise
5928 // the widget will remain pending forever.
5929 // TODO: this assumes that an aborted request will fail or succeed soon after
5930 // being aborted, or at least eventually. It would be nice if we could popPending()
5931 // at abort time, but only if we knew that we hadn't already called popPending()
5932 // for that request.
5933 widget.popPending();
5934 } )
5935 .done( function ( response ) {
5936 // If this is an old request (and aborting it somehow caused it to still succeed),
5937 // ignore its success completely
5938 if ( ourRequest === widget.lookupRequest ) {
5939 widget.lookupQuery = null;
5940 widget.lookupRequest = null;
5941 widget.lookupCache[ value ] = widget.getLookupCacheDataFromResponse( response );
5942 deferred.resolve( widget.getLookupMenuOptionsFromData( widget.lookupCache[ value ] ) );
5943 }
5944 } )
5945 .fail( function () {
5946 // If this is an old request (or a request failing because it's being aborted),
5947 // ignore its failure completely
5948 if ( ourRequest === widget.lookupRequest ) {
5949 widget.lookupQuery = null;
5950 widget.lookupRequest = null;
5951 deferred.reject();
5952 }
5953 } );
5954 }
5955 return deferred.promise();
5956 };
5957
5958 /**
5959 * Abort the currently pending lookup request, if any.
5960 *
5961 * @private
5962 */
5963 OO.ui.mixin.LookupElement.prototype.abortLookupRequest = function () {
5964 var oldRequest = this.lookupRequest;
5965 if ( oldRequest ) {
5966 // First unset this.lookupRequest to the fail handler will notice
5967 // that the request is no longer current
5968 this.lookupRequest = null;
5969 this.lookupQuery = null;
5970 oldRequest.abort();
5971 }
5972 };
5973
5974 /**
5975 * Get a new request object of the current lookup query value.
5976 *
5977 * @protected
5978 * @abstract
5979 * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
5980 */
5981 OO.ui.mixin.LookupElement.prototype.getLookupRequest = function () {
5982 // Stub, implemented in subclass
5983 return null;
5984 };
5985
5986 /**
5987 * Pre-process data returned by the request from #getLookupRequest.
5988 *
5989 * The return value of this function will be cached, and any further queries for the given value
5990 * will use the cache rather than doing API requests.
5991 *
5992 * @protected
5993 * @abstract
5994 * @param {Mixed} response Response from server
5995 * @return {Mixed} Cached result data
5996 */
5997 OO.ui.mixin.LookupElement.prototype.getLookupCacheDataFromResponse = function () {
5998 // Stub, implemented in subclass
5999 return [];
6000 };
6001
6002 /**
6003 * Get a list of menu option widgets from the (possibly cached) data returned by
6004 * #getLookupCacheDataFromResponse.
6005 *
6006 * @protected
6007 * @abstract
6008 * @param {Mixed} data Cached result data, usually an array
6009 * @return {OO.ui.MenuOptionWidget[]} Menu items
6010 */
6011 OO.ui.mixin.LookupElement.prototype.getLookupMenuOptionsFromData = function () {
6012 // Stub, implemented in subclass
6013 return [];
6014 };
6015
6016 /**
6017 * Set the read-only state of the widget.
6018 *
6019 * This will also disable/enable the lookups functionality.
6020 *
6021 * @param {boolean} readOnly Make input read-only
6022 * @chainable
6023 */
6024 OO.ui.mixin.LookupElement.prototype.setReadOnly = function ( readOnly ) {
6025 // Parent method
6026 // Note: Calling #setReadOnly this way assumes this is mixed into an OO.ui.TextInputWidget
6027 OO.ui.TextInputWidget.prototype.setReadOnly.call( this, readOnly );
6028
6029 this.setLookupsDisabled( readOnly );
6030 // During construction, #setReadOnly is called before the OO.ui.mixin.LookupElement constructor
6031 if ( readOnly && this.lookupMenu ) {
6032 this.closeLookupMenu();
6033 }
6034
6035 return this;
6036 };
6037
6038 /**
6039 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
6040 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
6041 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
6042 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
6043 *
6044 * @abstract
6045 * @class
6046 *
6047 * @constructor
6048 * @param {Object} [config] Configuration options
6049 * @cfg {Object} [popup] Configuration to pass to popup
6050 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
6051 */
6052 OO.ui.mixin.PopupElement = function OoUiMixinPopupElement( config ) {
6053 // Configuration initialization
6054 config = config || {};
6055
6056 // Properties
6057 this.popup = new OO.ui.PopupWidget( $.extend(
6058 { autoClose: true },
6059 config.popup,
6060 { $autoCloseIgnore: this.$element }
6061 ) );
6062 };
6063
6064 /* Methods */
6065
6066 /**
6067 * Get popup.
6068 *
6069 * @return {OO.ui.PopupWidget} Popup widget
6070 */
6071 OO.ui.mixin.PopupElement.prototype.getPopup = function () {
6072 return this.popup;
6073 };
6074
6075 /**
6076 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
6077 * additional functionality to an element created by another class. The class provides
6078 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
6079 * which are used to customize the look and feel of a widget to better describe its
6080 * importance and functionality.
6081 *
6082 * The library currently contains the following styling flags for general use:
6083 *
6084 * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
6085 * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
6086 * - **constructive**: Constructive styling is applied to convey that the widget will create something.
6087 *
6088 * The flags affect the appearance of the buttons:
6089 *
6090 * @example
6091 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
6092 * var button1 = new OO.ui.ButtonWidget( {
6093 * label: 'Constructive',
6094 * flags: 'constructive'
6095 * } );
6096 * var button2 = new OO.ui.ButtonWidget( {
6097 * label: 'Destructive',
6098 * flags: 'destructive'
6099 * } );
6100 * var button3 = new OO.ui.ButtonWidget( {
6101 * label: 'Progressive',
6102 * flags: 'progressive'
6103 * } );
6104 * $( 'body' ).append( button1.$element, button2.$element, button3.$element );
6105 *
6106 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
6107 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
6108 *
6109 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
6110 *
6111 * @abstract
6112 * @class
6113 *
6114 * @constructor
6115 * @param {Object} [config] Configuration options
6116 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'constructive' or 'primary') to apply.
6117 * Please see the [OOjs UI documentation on MediaWiki] [2] for more information about available flags.
6118 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
6119 * @cfg {jQuery} [$flagged] The flagged element. By default,
6120 * the flagged functionality is applied to the element created by the class ($element).
6121 * If a different element is specified, the flagged functionality will be applied to it instead.
6122 */
6123 OO.ui.mixin.FlaggedElement = function OoUiMixinFlaggedElement( config ) {
6124 // Configuration initialization
6125 config = config || {};
6126
6127 // Properties
6128 this.flags = {};
6129 this.$flagged = null;
6130
6131 // Initialization
6132 this.setFlags( config.flags );
6133 this.setFlaggedElement( config.$flagged || this.$element );
6134 };
6135
6136 /* Events */
6137
6138 /**
6139 * @event flag
6140 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
6141 * parameter contains the name of each modified flag and indicates whether it was
6142 * added or removed.
6143 *
6144 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
6145 * that the flag was added, `false` that the flag was removed.
6146 */
6147
6148 /* Methods */
6149
6150 /**
6151 * Set the flagged element.
6152 *
6153 * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
6154 * If an element is already set, the method will remove the mixin’s effect on that element.
6155 *
6156 * @param {jQuery} $flagged Element that should be flagged
6157 */
6158 OO.ui.mixin.FlaggedElement.prototype.setFlaggedElement = function ( $flagged ) {
6159 var classNames = Object.keys( this.flags ).map( function ( flag ) {
6160 return 'oo-ui-flaggedElement-' + flag;
6161 } ).join( ' ' );
6162
6163 if ( this.$flagged ) {
6164 this.$flagged.removeClass( classNames );
6165 }
6166
6167 this.$flagged = $flagged.addClass( classNames );
6168 };
6169
6170 /**
6171 * Check if the specified flag is set.
6172 *
6173 * @param {string} flag Name of flag
6174 * @return {boolean} The flag is set
6175 */
6176 OO.ui.mixin.FlaggedElement.prototype.hasFlag = function ( flag ) {
6177 return flag in this.flags;
6178 };
6179
6180 /**
6181 * Get the names of all flags set.
6182 *
6183 * @return {string[]} Flag names
6184 */
6185 OO.ui.mixin.FlaggedElement.prototype.getFlags = function () {
6186 return Object.keys( this.flags );
6187 };
6188
6189 /**
6190 * Clear all flags.
6191 *
6192 * @chainable
6193 * @fires flag
6194 */
6195 OO.ui.mixin.FlaggedElement.prototype.clearFlags = function () {
6196 var flag, className,
6197 changes = {},
6198 remove = [],
6199 classPrefix = 'oo-ui-flaggedElement-';
6200
6201 for ( flag in this.flags ) {
6202 className = classPrefix + flag;
6203 changes[ flag ] = false;
6204 delete this.flags[ flag ];
6205 remove.push( className );
6206 }
6207
6208 if ( this.$flagged ) {
6209 this.$flagged.removeClass( remove.join( ' ' ) );
6210 }
6211
6212 this.updateThemeClasses();
6213 this.emit( 'flag', changes );
6214
6215 return this;
6216 };
6217
6218 /**
6219 * Add one or more flags.
6220 *
6221 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
6222 * or an object keyed by flag name with a boolean value that indicates whether the flag should
6223 * be added (`true`) or removed (`false`).
6224 * @chainable
6225 * @fires flag
6226 */
6227 OO.ui.mixin.FlaggedElement.prototype.setFlags = function ( flags ) {
6228 var i, len, flag, className,
6229 changes = {},
6230 add = [],
6231 remove = [],
6232 classPrefix = 'oo-ui-flaggedElement-';
6233
6234 if ( typeof flags === 'string' ) {
6235 className = classPrefix + flags;
6236 // Set
6237 if ( !this.flags[ flags ] ) {
6238 this.flags[ flags ] = true;
6239 add.push( className );
6240 }
6241 } else if ( Array.isArray( flags ) ) {
6242 for ( i = 0, len = flags.length; i < len; i++ ) {
6243 flag = flags[ i ];
6244 className = classPrefix + flag;
6245 // Set
6246 if ( !this.flags[ flag ] ) {
6247 changes[ flag ] = true;
6248 this.flags[ flag ] = true;
6249 add.push( className );
6250 }
6251 }
6252 } else if ( OO.isPlainObject( flags ) ) {
6253 for ( flag in flags ) {
6254 className = classPrefix + flag;
6255 if ( flags[ flag ] ) {
6256 // Set
6257 if ( !this.flags[ flag ] ) {
6258 changes[ flag ] = true;
6259 this.flags[ flag ] = true;
6260 add.push( className );
6261 }
6262 } else {
6263 // Remove
6264 if ( this.flags[ flag ] ) {
6265 changes[ flag ] = false;
6266 delete this.flags[ flag ];
6267 remove.push( className );
6268 }
6269 }
6270 }
6271 }
6272
6273 if ( this.$flagged ) {
6274 this.$flagged
6275 .addClass( add.join( ' ' ) )
6276 .removeClass( remove.join( ' ' ) );
6277 }
6278
6279 this.updateThemeClasses();
6280 this.emit( 'flag', changes );
6281
6282 return this;
6283 };
6284
6285 /**
6286 * TitledElement is mixed into other classes to provide a `title` attribute.
6287 * Titles are rendered by the browser and are made visible when the user moves
6288 * the mouse over the element. Titles are not visible on touch devices.
6289 *
6290 * @example
6291 * // TitledElement provides a 'title' attribute to the
6292 * // ButtonWidget class
6293 * var button = new OO.ui.ButtonWidget( {
6294 * label: 'Button with Title',
6295 * title: 'I am a button'
6296 * } );
6297 * $( 'body' ).append( button.$element );
6298 *
6299 * @abstract
6300 * @class
6301 *
6302 * @constructor
6303 * @param {Object} [config] Configuration options
6304 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
6305 * If this config is omitted, the title functionality is applied to $element, the
6306 * element created by the class.
6307 * @cfg {string|Function} [title] The title text or a function that returns text. If
6308 * this config is omitted, the value of the {@link #static-title static title} property is used.
6309 */
6310 OO.ui.mixin.TitledElement = function OoUiMixinTitledElement( config ) {
6311 // Configuration initialization
6312 config = config || {};
6313
6314 // Properties
6315 this.$titled = null;
6316 this.title = null;
6317
6318 // Initialization
6319 this.setTitle( config.title || this.constructor.static.title );
6320 this.setTitledElement( config.$titled || this.$element );
6321 };
6322
6323 /* Setup */
6324
6325 OO.initClass( OO.ui.mixin.TitledElement );
6326
6327 /* Static Properties */
6328
6329 /**
6330 * The title text, a function that returns text, or `null` for no title. The value of the static property
6331 * is overridden if the #title config option is used.
6332 *
6333 * @static
6334 * @inheritable
6335 * @property {string|Function|null}
6336 */
6337 OO.ui.mixin.TitledElement.static.title = null;
6338
6339 /* Methods */
6340
6341 /**
6342 * Set the titled element.
6343 *
6344 * This method is used to retarget a titledElement mixin so that its functionality applies to the specified element.
6345 * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
6346 *
6347 * @param {jQuery} $titled Element that should use the 'titled' functionality
6348 */
6349 OO.ui.mixin.TitledElement.prototype.setTitledElement = function ( $titled ) {
6350 if ( this.$titled ) {
6351 this.$titled.removeAttr( 'title' );
6352 }
6353
6354 this.$titled = $titled;
6355 if ( this.title ) {
6356 this.$titled.attr( 'title', this.title );
6357 }
6358 };
6359
6360 /**
6361 * Set title.
6362 *
6363 * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
6364 * @chainable
6365 */
6366 OO.ui.mixin.TitledElement.prototype.setTitle = function ( title ) {
6367 title = typeof title === 'string' ? OO.ui.resolveMsg( title ) : null;
6368
6369 if ( this.title !== title ) {
6370 if ( this.$titled ) {
6371 if ( title !== null ) {
6372 this.$titled.attr( 'title', title );
6373 } else {
6374 this.$titled.removeAttr( 'title' );
6375 }
6376 }
6377 this.title = title;
6378 }
6379
6380 return this;
6381 };
6382
6383 /**
6384 * Get title.
6385 *
6386 * @return {string} Title string
6387 */
6388 OO.ui.mixin.TitledElement.prototype.getTitle = function () {
6389 return this.title;
6390 };
6391
6392 /**
6393 * Element that can be automatically clipped to visible boundaries.
6394 *
6395 * Whenever the element's natural height changes, you have to call
6396 * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
6397 * clipping correctly.
6398 *
6399 * @abstract
6400 * @class
6401 *
6402 * @constructor
6403 * @param {Object} [config] Configuration options
6404 * @cfg {jQuery} [$clippable] Nodes to clip, assigned to #$clippable, omit to use #$element
6405 */
6406 OO.ui.mixin.ClippableElement = function OoUiMixinClippableElement( config ) {
6407 // Configuration initialization
6408 config = config || {};
6409
6410 // Properties
6411 this.$clippable = null;
6412 this.clipping = false;
6413 this.clippedHorizontally = false;
6414 this.clippedVertically = false;
6415 this.$clippableContainer = null;
6416 this.$clippableScroller = null;
6417 this.$clippableWindow = null;
6418 this.idealWidth = null;
6419 this.idealHeight = null;
6420 this.onClippableContainerScrollHandler = this.clip.bind( this );
6421 this.onClippableWindowResizeHandler = this.clip.bind( this );
6422
6423 // Initialization
6424 this.setClippableElement( config.$clippable || this.$element );
6425 };
6426
6427 /* Methods */
6428
6429 /**
6430 * Set clippable element.
6431 *
6432 * If an element is already set, it will be cleaned up before setting up the new element.
6433 *
6434 * @param {jQuery} $clippable Element to make clippable
6435 */
6436 OO.ui.mixin.ClippableElement.prototype.setClippableElement = function ( $clippable ) {
6437 if ( this.$clippable ) {
6438 this.$clippable.removeClass( 'oo-ui-clippableElement-clippable' );
6439 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
6440 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
6441 }
6442
6443 this.$clippable = $clippable.addClass( 'oo-ui-clippableElement-clippable' );
6444 this.clip();
6445 };
6446
6447 /**
6448 * Toggle clipping.
6449 *
6450 * Do not turn clipping on until after the element is attached to the DOM and visible.
6451 *
6452 * @param {boolean} [clipping] Enable clipping, omit to toggle
6453 * @chainable
6454 */
6455 OO.ui.mixin.ClippableElement.prototype.toggleClipping = function ( clipping ) {
6456 clipping = clipping === undefined ? !this.clipping : !!clipping;
6457
6458 if ( this.clipping !== clipping ) {
6459 this.clipping = clipping;
6460 if ( clipping ) {
6461 this.$clippableContainer = $( this.getClosestScrollableElementContainer() );
6462 // If the clippable container is the root, we have to listen to scroll events and check
6463 // jQuery.scrollTop on the window because of browser inconsistencies
6464 this.$clippableScroller = this.$clippableContainer.is( 'html, body' ) ?
6465 $( OO.ui.Element.static.getWindow( this.$clippableContainer ) ) :
6466 this.$clippableContainer;
6467 this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler );
6468 this.$clippableWindow = $( this.getElementWindow() )
6469 .on( 'resize', this.onClippableWindowResizeHandler );
6470 // Initial clip after visible
6471 this.clip();
6472 } else {
6473 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
6474 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
6475
6476 this.$clippableContainer = null;
6477 this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler );
6478 this.$clippableScroller = null;
6479 this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
6480 this.$clippableWindow = null;
6481 }
6482 }
6483
6484 return this;
6485 };
6486
6487 /**
6488 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
6489 *
6490 * @return {boolean} Element will be clipped to the visible area
6491 */
6492 OO.ui.mixin.ClippableElement.prototype.isClipping = function () {
6493 return this.clipping;
6494 };
6495
6496 /**
6497 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
6498 *
6499 * @return {boolean} Part of the element is being clipped
6500 */
6501 OO.ui.mixin.ClippableElement.prototype.isClipped = function () {
6502 return this.clippedHorizontally || this.clippedVertically;
6503 };
6504
6505 /**
6506 * Check if the right of the element is being clipped by the nearest scrollable container.
6507 *
6508 * @return {boolean} Part of the element is being clipped
6509 */
6510 OO.ui.mixin.ClippableElement.prototype.isClippedHorizontally = function () {
6511 return this.clippedHorizontally;
6512 };
6513
6514 /**
6515 * Check if the bottom of the element is being clipped by the nearest scrollable container.
6516 *
6517 * @return {boolean} Part of the element is being clipped
6518 */
6519 OO.ui.mixin.ClippableElement.prototype.isClippedVertically = function () {
6520 return this.clippedVertically;
6521 };
6522
6523 /**
6524 * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
6525 *
6526 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
6527 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
6528 */
6529 OO.ui.mixin.ClippableElement.prototype.setIdealSize = function ( width, height ) {
6530 this.idealWidth = width;
6531 this.idealHeight = height;
6532
6533 if ( !this.clipping ) {
6534 // Update dimensions
6535 this.$clippable.css( { width: width, height: height } );
6536 }
6537 // While clipping, idealWidth and idealHeight are not considered
6538 };
6539
6540 /**
6541 * Clip element to visible boundaries and allow scrolling when needed. Call this method when
6542 * the element's natural height changes.
6543 *
6544 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
6545 * overlapped by, the visible area of the nearest scrollable container.
6546 *
6547 * @chainable
6548 */
6549 OO.ui.mixin.ClippableElement.prototype.clip = function () {
6550 if ( !this.clipping ) {
6551 // this.$clippableContainer and this.$clippableWindow are null, so the below will fail
6552 return this;
6553 }
6554
6555 var buffer = 7, // Chosen by fair dice roll
6556 cOffset = this.$clippable.offset(),
6557 $container = this.$clippableContainer.is( 'html, body' ) ?
6558 this.$clippableWindow : this.$clippableContainer,
6559 ccOffset = $container.offset() || { top: 0, left: 0 },
6560 ccHeight = $container.innerHeight() - buffer,
6561 ccWidth = $container.innerWidth() - buffer,
6562 cWidth = this.$clippable.outerWidth() + buffer,
6563 scrollerIsWindow = this.$clippableScroller[0] === this.$clippableWindow[0],
6564 scrollTop = scrollerIsWindow ? this.$clippableScroller.scrollTop() : 0,
6565 scrollLeft = scrollerIsWindow ? this.$clippableScroller.scrollLeft() : 0,
6566 desiredWidth = cOffset.left < 0 ?
6567 cWidth + cOffset.left :
6568 ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left,
6569 desiredHeight = ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top,
6570 naturalWidth = this.$clippable.prop( 'scrollWidth' ),
6571 naturalHeight = this.$clippable.prop( 'scrollHeight' ),
6572 clipWidth = desiredWidth < naturalWidth,
6573 clipHeight = desiredHeight < naturalHeight;
6574
6575 if ( clipWidth ) {
6576 this.$clippable.css( { overflowX: 'scroll', width: desiredWidth } );
6577 } else {
6578 this.$clippable.css( { width: this.idealWidth || '', overflowX: '' } );
6579 }
6580 if ( clipHeight ) {
6581 this.$clippable.css( { overflowY: 'scroll', height: desiredHeight } );
6582 } else {
6583 this.$clippable.css( { height: this.idealHeight || '', overflowY: '' } );
6584 }
6585
6586 // If we stopped clipping in at least one of the dimensions
6587 if ( ( this.clippedHorizontally && !clipWidth ) || ( this.clippedVertically && !clipHeight ) ) {
6588 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
6589 }
6590
6591 this.clippedHorizontally = clipWidth;
6592 this.clippedVertically = clipHeight;
6593
6594 return this;
6595 };
6596
6597 /**
6598 * Tools, together with {@link OO.ui.ToolGroup toolgroups}, constitute {@link OO.ui.Toolbar toolbars}.
6599 * Each tool is configured with a static name, title, and icon and is customized with the command to carry
6600 * out when the tool is selected. Tools must also be registered with a {@link OO.ui.ToolFactory tool factory},
6601 * which creates the tools on demand.
6602 *
6603 * Tools are added to toolgroups ({@link OO.ui.ListToolGroup ListToolGroup},
6604 * {@link OO.ui.BarToolGroup BarToolGroup}, or {@link OO.ui.MenuToolGroup MenuToolGroup}), which determine how
6605 * the tool is displayed in the toolbar. See {@link OO.ui.Toolbar toolbars} for an example.
6606 *
6607 * For more information, please see the [OOjs UI documentation on MediaWiki][1].
6608 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
6609 *
6610 * @abstract
6611 * @class
6612 * @extends OO.ui.Widget
6613 * @mixins OO.ui.mixin.IconElement
6614 * @mixins OO.ui.mixin.FlaggedElement
6615 * @mixins OO.ui.mixin.TabIndexedElement
6616 *
6617 * @constructor
6618 * @param {OO.ui.ToolGroup} toolGroup
6619 * @param {Object} [config] Configuration options
6620 * @cfg {string|Function} [title] Title text or a function that returns text. If this config is omitted, the value of
6621 * the {@link #static-title static title} property is used.
6622 *
6623 * The title is used in different ways depending on the type of toolgroup that contains the tool. The
6624 * 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
6625 * part of a {@link OO.ui.ListToolGroup list} or {@link OO.ui.MenuToolGroup menu} toolgroup.
6626 *
6627 * For bar toolgroups, a description of the accelerator key is appended to the title if an accelerator key
6628 * is associated with an action by the same name as the tool and accelerator functionality has been added to the application.
6629 * To add accelerator key functionality, you must subclass OO.ui.Toolbar and override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method.
6630 */
6631 OO.ui.Tool = function OoUiTool( toolGroup, config ) {
6632 // Allow passing positional parameters inside the config object
6633 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
6634 config = toolGroup;
6635 toolGroup = config.toolGroup;
6636 }
6637
6638 // Configuration initialization
6639 config = config || {};
6640
6641 // Parent constructor
6642 OO.ui.Tool.parent.call( this, config );
6643
6644 // Properties
6645 this.toolGroup = toolGroup;
6646 this.toolbar = this.toolGroup.getToolbar();
6647 this.active = false;
6648 this.$title = $( '<span>' );
6649 this.$accel = $( '<span>' );
6650 this.$link = $( '<a>' );
6651 this.title = null;
6652
6653 // Mixin constructors
6654 OO.ui.mixin.IconElement.call( this, config );
6655 OO.ui.mixin.FlaggedElement.call( this, config );
6656 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$link } ) );
6657
6658 // Events
6659 this.toolbar.connect( this, { updateState: 'onUpdateState' } );
6660
6661 // Initialization
6662 this.$title.addClass( 'oo-ui-tool-title' );
6663 this.$accel
6664 .addClass( 'oo-ui-tool-accel' )
6665 .prop( {
6666 // This may need to be changed if the key names are ever localized,
6667 // but for now they are essentially written in English
6668 dir: 'ltr',
6669 lang: 'en'
6670 } );
6671 this.$link
6672 .addClass( 'oo-ui-tool-link' )
6673 .append( this.$icon, this.$title, this.$accel )
6674 .attr( 'role', 'button' );
6675 this.$element
6676 .data( 'oo-ui-tool', this )
6677 .addClass(
6678 'oo-ui-tool ' + 'oo-ui-tool-name-' +
6679 this.constructor.static.name.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' )
6680 )
6681 .toggleClass( 'oo-ui-tool-with-label', this.constructor.static.displayBothIconAndLabel )
6682 .append( this.$link );
6683 this.setTitle( config.title || this.constructor.static.title );
6684 };
6685
6686 /* Setup */
6687
6688 OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
6689 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.IconElement );
6690 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.FlaggedElement );
6691 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.TabIndexedElement );
6692
6693 /* Static Properties */
6694
6695 /**
6696 * @static
6697 * @inheritdoc
6698 */
6699 OO.ui.Tool.static.tagName = 'span';
6700
6701 /**
6702 * Symbolic name of tool.
6703 *
6704 * The symbolic name is used internally to register the tool with a {@link OO.ui.ToolFactory ToolFactory}. It can
6705 * also be used when adding tools to toolgroups.
6706 *
6707 * @abstract
6708 * @static
6709 * @inheritable
6710 * @property {string}
6711 */
6712 OO.ui.Tool.static.name = '';
6713
6714 /**
6715 * Symbolic name of the group.
6716 *
6717 * The group name is used to associate tools with each other so that they can be selected later by
6718 * a {@link OO.ui.ToolGroup toolgroup}.
6719 *
6720 * @abstract
6721 * @static
6722 * @inheritable
6723 * @property {string}
6724 */
6725 OO.ui.Tool.static.group = '';
6726
6727 /**
6728 * 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.
6729 *
6730 * @abstract
6731 * @static
6732 * @inheritable
6733 * @property {string|Function}
6734 */
6735 OO.ui.Tool.static.title = '';
6736
6737 /**
6738 * Display both icon and label when the tool is used in a {@link OO.ui.BarToolGroup bar} toolgroup.
6739 * Normally only the icon is displayed, or only the label if no icon is given.
6740 *
6741 * @static
6742 * @inheritable
6743 * @property {boolean}
6744 */
6745 OO.ui.Tool.static.displayBothIconAndLabel = false;
6746
6747 /**
6748 * Add tool to catch-all groups automatically.
6749 *
6750 * A catch-all group, which contains all tools that do not currently belong to a toolgroup,
6751 * can be included in a toolgroup using the wildcard selector, an asterisk (*).
6752 *
6753 * @static
6754 * @inheritable
6755 * @property {boolean}
6756 */
6757 OO.ui.Tool.static.autoAddToCatchall = true;
6758
6759 /**
6760 * Add tool to named groups automatically.
6761 *
6762 * By default, tools that are configured with a static ‘group’ property are added
6763 * to that group and will be selected when the symbolic name of the group is specified (e.g., when
6764 * toolgroups include tools by group name).
6765 *
6766 * @static
6767 * @property {boolean}
6768 * @inheritable
6769 */
6770 OO.ui.Tool.static.autoAddToGroup = true;
6771
6772 /**
6773 * Check if this tool is compatible with given data.
6774 *
6775 * This is a stub that can be overriden to provide support for filtering tools based on an
6776 * arbitrary piece of information (e.g., where the cursor is in a document). The implementation
6777 * must also call this method so that the compatibility check can be performed.
6778 *
6779 * @static
6780 * @inheritable
6781 * @param {Mixed} data Data to check
6782 * @return {boolean} Tool can be used with data
6783 */
6784 OO.ui.Tool.static.isCompatibleWith = function () {
6785 return false;
6786 };
6787
6788 /* Methods */
6789
6790 /**
6791 * Handle the toolbar state being updated.
6792 *
6793 * This is an abstract method that must be overridden in a concrete subclass.
6794 *
6795 * @protected
6796 * @abstract
6797 */
6798 OO.ui.Tool.prototype.onUpdateState = function () {
6799 throw new Error(
6800 'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor
6801 );
6802 };
6803
6804 /**
6805 * Handle the tool being selected.
6806 *
6807 * This is an abstract method that must be overridden in a concrete subclass.
6808 *
6809 * @protected
6810 * @abstract
6811 */
6812 OO.ui.Tool.prototype.onSelect = function () {
6813 throw new Error(
6814 'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor
6815 );
6816 };
6817
6818 /**
6819 * Check if the tool is active.
6820 *
6821 * Tools become active when their #onSelect or #onUpdateState handlers change them to appear pressed
6822 * with the #setActive method. Additional CSS is applied to the tool to reflect the active state.
6823 *
6824 * @return {boolean} Tool is active
6825 */
6826 OO.ui.Tool.prototype.isActive = function () {
6827 return this.active;
6828 };
6829
6830 /**
6831 * Make the tool appear active or inactive.
6832 *
6833 * This method should be called within #onSelect or #onUpdateState event handlers to make the tool
6834 * appear pressed or not.
6835 *
6836 * @param {boolean} state Make tool appear active
6837 */
6838 OO.ui.Tool.prototype.setActive = function ( state ) {
6839 this.active = !!state;
6840 if ( this.active ) {
6841 this.$element.addClass( 'oo-ui-tool-active' );
6842 } else {
6843 this.$element.removeClass( 'oo-ui-tool-active' );
6844 }
6845 };
6846
6847 /**
6848 * Set the tool #title.
6849 *
6850 * @param {string|Function} title Title text or a function that returns text
6851 * @chainable
6852 */
6853 OO.ui.Tool.prototype.setTitle = function ( title ) {
6854 this.title = OO.ui.resolveMsg( title );
6855 this.updateTitle();
6856 return this;
6857 };
6858
6859 /**
6860 * Get the tool #title.
6861 *
6862 * @return {string} Title text
6863 */
6864 OO.ui.Tool.prototype.getTitle = function () {
6865 return this.title;
6866 };
6867
6868 /**
6869 * Get the tool's symbolic name.
6870 *
6871 * @return {string} Symbolic name of tool
6872 */
6873 OO.ui.Tool.prototype.getName = function () {
6874 return this.constructor.static.name;
6875 };
6876
6877 /**
6878 * Update the title.
6879 */
6880 OO.ui.Tool.prototype.updateTitle = function () {
6881 var titleTooltips = this.toolGroup.constructor.static.titleTooltips,
6882 accelTooltips = this.toolGroup.constructor.static.accelTooltips,
6883 accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
6884 tooltipParts = [];
6885
6886 this.$title.text( this.title );
6887 this.$accel.text( accel );
6888
6889 if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
6890 tooltipParts.push( this.title );
6891 }
6892 if ( accelTooltips && typeof accel === 'string' && accel.length ) {
6893 tooltipParts.push( accel );
6894 }
6895 if ( tooltipParts.length ) {
6896 this.$link.attr( 'title', tooltipParts.join( ' ' ) );
6897 } else {
6898 this.$link.removeAttr( 'title' );
6899 }
6900 };
6901
6902 /**
6903 * Destroy tool.
6904 *
6905 * Destroying the tool removes all event handlers and the tool’s DOM elements.
6906 * Call this method whenever you are done using a tool.
6907 */
6908 OO.ui.Tool.prototype.destroy = function () {
6909 this.toolbar.disconnect( this );
6910 this.$element.remove();
6911 };
6912
6913 /**
6914 * Toolbars are complex interface components that permit users to easily access a variety
6915 * of {@link OO.ui.Tool tools} (e.g., formatting commands) and actions, which are additional commands that are
6916 * part of the toolbar, but not configured as tools.
6917 *
6918 * Individual tools are customized and then registered with a {@link OO.ui.ToolFactory tool factory}, which creates
6919 * the tools on demand. Each tool has a symbolic name (used when registering the tool), a title (e.g., ‘Insert
6920 * picture’), and an icon.
6921 *
6922 * Individual tools are organized in {@link OO.ui.ToolGroup toolgroups}, which can be {@link OO.ui.MenuToolGroup menus}
6923 * of tools, {@link OO.ui.ListToolGroup lists} of tools, or a single {@link OO.ui.BarToolGroup bar} of tools.
6924 * The arrangement and order of the toolgroups is customized when the toolbar is set up. Tools can be presented in
6925 * any order, but each can only appear once in the toolbar.
6926 *
6927 * The following is an example of a basic toolbar.
6928 *
6929 * @example
6930 * // Example of a toolbar
6931 * // Create the toolbar
6932 * var toolFactory = new OO.ui.ToolFactory();
6933 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
6934 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
6935 *
6936 * // We will be placing status text in this element when tools are used
6937 * var $area = $( '<p>' ).text( 'Toolbar example' );
6938 *
6939 * // Define the tools that we're going to place in our toolbar
6940 *
6941 * // Create a class inheriting from OO.ui.Tool
6942 * function PictureTool() {
6943 * PictureTool.parent.apply( this, arguments );
6944 * }
6945 * OO.inheritClass( PictureTool, OO.ui.Tool );
6946 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
6947 * // of 'icon' and 'title' (displayed icon and text).
6948 * PictureTool.static.name = 'picture';
6949 * PictureTool.static.icon = 'picture';
6950 * PictureTool.static.title = 'Insert picture';
6951 * // Defines the action that will happen when this tool is selected (clicked).
6952 * PictureTool.prototype.onSelect = function () {
6953 * $area.text( 'Picture tool clicked!' );
6954 * // Never display this tool as "active" (selected).
6955 * this.setActive( false );
6956 * };
6957 * // Make this tool available in our toolFactory and thus our toolbar
6958 * toolFactory.register( PictureTool );
6959 *
6960 * // Register two more tools, nothing interesting here
6961 * function SettingsTool() {
6962 * SettingsTool.parent.apply( this, arguments );
6963 * }
6964 * OO.inheritClass( SettingsTool, OO.ui.Tool );
6965 * SettingsTool.static.name = 'settings';
6966 * SettingsTool.static.icon = 'settings';
6967 * SettingsTool.static.title = 'Change settings';
6968 * SettingsTool.prototype.onSelect = function () {
6969 * $area.text( 'Settings tool clicked!' );
6970 * this.setActive( false );
6971 * };
6972 * toolFactory.register( SettingsTool );
6973 *
6974 * // Register two more tools, nothing interesting here
6975 * function StuffTool() {
6976 * StuffTool.parent.apply( this, arguments );
6977 * }
6978 * OO.inheritClass( StuffTool, OO.ui.Tool );
6979 * StuffTool.static.name = 'stuff';
6980 * StuffTool.static.icon = 'ellipsis';
6981 * StuffTool.static.title = 'More stuff';
6982 * StuffTool.prototype.onSelect = function () {
6983 * $area.text( 'More stuff tool clicked!' );
6984 * this.setActive( false );
6985 * };
6986 * toolFactory.register( StuffTool );
6987 *
6988 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
6989 * // little popup window (a PopupWidget).
6990 * function HelpTool( toolGroup, config ) {
6991 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
6992 * padded: true,
6993 * label: 'Help',
6994 * head: true
6995 * } }, config ) );
6996 * this.popup.$body.append( '<p>I am helpful!</p>' );
6997 * }
6998 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
6999 * HelpTool.static.name = 'help';
7000 * HelpTool.static.icon = 'help';
7001 * HelpTool.static.title = 'Help';
7002 * toolFactory.register( HelpTool );
7003 *
7004 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
7005 * // used once (but not all defined tools must be used).
7006 * toolbar.setup( [
7007 * {
7008 * // 'bar' tool groups display tools' icons only, side-by-side.
7009 * type: 'bar',
7010 * include: [ 'picture', 'help' ]
7011 * },
7012 * {
7013 * // 'list' tool groups display both the titles and icons, in a dropdown list.
7014 * type: 'list',
7015 * indicator: 'down',
7016 * label: 'More',
7017 * include: [ 'settings', 'stuff' ]
7018 * }
7019 * // Note how the tools themselves are toolgroup-agnostic - the same tool can be displayed
7020 * // either in a 'list' or a 'bar'. There is a 'menu' tool group too, not showcased here,
7021 * // since it's more complicated to use. (See the next example snippet on this page.)
7022 * ] );
7023 *
7024 * // Create some UI around the toolbar and place it in the document
7025 * var frame = new OO.ui.PanelLayout( {
7026 * expanded: false,
7027 * framed: true
7028 * } );
7029 * var contentFrame = new OO.ui.PanelLayout( {
7030 * expanded: false,
7031 * padded: true
7032 * } );
7033 * frame.$element.append(
7034 * toolbar.$element,
7035 * contentFrame.$element.append( $area )
7036 * );
7037 * $( 'body' ).append( frame.$element );
7038 *
7039 * // Here is where the toolbar is actually built. This must be done after inserting it into the
7040 * // document.
7041 * toolbar.initialize();
7042 *
7043 * The following example extends the previous one to illustrate 'menu' toolgroups and the usage of
7044 * 'updateState' event.
7045 *
7046 * @example
7047 * // Create the toolbar
7048 * var toolFactory = new OO.ui.ToolFactory();
7049 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
7050 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
7051 *
7052 * // We will be placing status text in this element when tools are used
7053 * var $area = $( '<p>' ).text( 'Toolbar example' );
7054 *
7055 * // Define the tools that we're going to place in our toolbar
7056 *
7057 * // Create a class inheriting from OO.ui.Tool
7058 * function PictureTool() {
7059 * PictureTool.parent.apply( this, arguments );
7060 * }
7061 * OO.inheritClass( PictureTool, OO.ui.Tool );
7062 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
7063 * // of 'icon' and 'title' (displayed icon and text).
7064 * PictureTool.static.name = 'picture';
7065 * PictureTool.static.icon = 'picture';
7066 * PictureTool.static.title = 'Insert picture';
7067 * // Defines the action that will happen when this tool is selected (clicked).
7068 * PictureTool.prototype.onSelect = function () {
7069 * $area.text( 'Picture tool clicked!' );
7070 * // Never display this tool as "active" (selected).
7071 * this.setActive( false );
7072 * };
7073 * // The toolbar can be synchronized with the state of some external stuff, like a text
7074 * // editor's editing area, highlighting the tools (e.g. a 'bold' tool would be shown as active
7075 * // when the text cursor was inside bolded text). Here we simply disable this feature.
7076 * PictureTool.prototype.onUpdateState = function () {
7077 * };
7078 * // Make this tool available in our toolFactory and thus our toolbar
7079 * toolFactory.register( PictureTool );
7080 *
7081 * // Register two more tools, nothing interesting here
7082 * function SettingsTool() {
7083 * SettingsTool.parent.apply( this, arguments );
7084 * this.reallyActive = false;
7085 * }
7086 * OO.inheritClass( SettingsTool, OO.ui.Tool );
7087 * SettingsTool.static.name = 'settings';
7088 * SettingsTool.static.icon = 'settings';
7089 * SettingsTool.static.title = 'Change settings';
7090 * SettingsTool.prototype.onSelect = function () {
7091 * $area.text( 'Settings tool clicked!' );
7092 * // Toggle the active state on each click
7093 * this.reallyActive = !this.reallyActive;
7094 * this.setActive( this.reallyActive );
7095 * // To update the menu label
7096 * this.toolbar.emit( 'updateState' );
7097 * };
7098 * SettingsTool.prototype.onUpdateState = function () {
7099 * };
7100 * toolFactory.register( SettingsTool );
7101 *
7102 * // Register two more tools, nothing interesting here
7103 * function StuffTool() {
7104 * StuffTool.parent.apply( this, arguments );
7105 * this.reallyActive = false;
7106 * }
7107 * OO.inheritClass( StuffTool, OO.ui.Tool );
7108 * StuffTool.static.name = 'stuff';
7109 * StuffTool.static.icon = 'ellipsis';
7110 * StuffTool.static.title = 'More stuff';
7111 * StuffTool.prototype.onSelect = function () {
7112 * $area.text( 'More stuff tool clicked!' );
7113 * // Toggle the active state on each click
7114 * this.reallyActive = !this.reallyActive;
7115 * this.setActive( this.reallyActive );
7116 * // To update the menu label
7117 * this.toolbar.emit( 'updateState' );
7118 * };
7119 * StuffTool.prototype.onUpdateState = function () {
7120 * };
7121 * toolFactory.register( StuffTool );
7122 *
7123 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
7124 * // little popup window (a PopupWidget). 'onUpdateState' is also already implemented.
7125 * function HelpTool( toolGroup, config ) {
7126 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
7127 * padded: true,
7128 * label: 'Help',
7129 * head: true
7130 * } }, config ) );
7131 * this.popup.$body.append( '<p>I am helpful!</p>' );
7132 * }
7133 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
7134 * HelpTool.static.name = 'help';
7135 * HelpTool.static.icon = 'help';
7136 * HelpTool.static.title = 'Help';
7137 * toolFactory.register( HelpTool );
7138 *
7139 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
7140 * // used once (but not all defined tools must be used).
7141 * toolbar.setup( [
7142 * {
7143 * // 'bar' tool groups display tools' icons only, side-by-side.
7144 * type: 'bar',
7145 * include: [ 'picture', 'help' ]
7146 * },
7147 * {
7148 * // 'menu' tool groups display both the titles and icons, in a dropdown menu.
7149 * // Menu label indicates which items are selected.
7150 * type: 'menu',
7151 * indicator: 'down',
7152 * include: [ 'settings', 'stuff' ]
7153 * }
7154 * ] );
7155 *
7156 * // Create some UI around the toolbar and place it in the document
7157 * var frame = new OO.ui.PanelLayout( {
7158 * expanded: false,
7159 * framed: true
7160 * } );
7161 * var contentFrame = new OO.ui.PanelLayout( {
7162 * expanded: false,
7163 * padded: true
7164 * } );
7165 * frame.$element.append(
7166 * toolbar.$element,
7167 * contentFrame.$element.append( $area )
7168 * );
7169 * $( 'body' ).append( frame.$element );
7170 *
7171 * // Here is where the toolbar is actually built. This must be done after inserting it into the
7172 * // document.
7173 * toolbar.initialize();
7174 * toolbar.emit( 'updateState' );
7175 *
7176 * @class
7177 * @extends OO.ui.Element
7178 * @mixins OO.EventEmitter
7179 * @mixins OO.ui.mixin.GroupElement
7180 *
7181 * @constructor
7182 * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
7183 * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating toolgroups
7184 * @param {Object} [config] Configuration options
7185 * @cfg {boolean} [actions] Add an actions section to the toolbar. Actions are commands that are included
7186 * in the toolbar, but are not configured as tools. By default, actions are displayed on the right side of
7187 * the toolbar.
7188 * @cfg {boolean} [shadow] Add a shadow below the toolbar.
7189 */
7190 OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
7191 // Allow passing positional parameters inside the config object
7192 if ( OO.isPlainObject( toolFactory ) && config === undefined ) {
7193 config = toolFactory;
7194 toolFactory = config.toolFactory;
7195 toolGroupFactory = config.toolGroupFactory;
7196 }
7197
7198 // Configuration initialization
7199 config = config || {};
7200
7201 // Parent constructor
7202 OO.ui.Toolbar.parent.call( this, config );
7203
7204 // Mixin constructors
7205 OO.EventEmitter.call( this );
7206 OO.ui.mixin.GroupElement.call( this, config );
7207
7208 // Properties
7209 this.toolFactory = toolFactory;
7210 this.toolGroupFactory = toolGroupFactory;
7211 this.groups = [];
7212 this.tools = {};
7213 this.$bar = $( '<div>' );
7214 this.$actions = $( '<div>' );
7215 this.initialized = false;
7216 this.onWindowResizeHandler = this.onWindowResize.bind( this );
7217
7218 // Events
7219 this.$element
7220 .add( this.$bar ).add( this.$group ).add( this.$actions )
7221 .on( 'mousedown keydown', this.onPointerDown.bind( this ) );
7222
7223 // Initialization
7224 this.$group.addClass( 'oo-ui-toolbar-tools' );
7225 if ( config.actions ) {
7226 this.$bar.append( this.$actions.addClass( 'oo-ui-toolbar-actions' ) );
7227 }
7228 this.$bar
7229 .addClass( 'oo-ui-toolbar-bar' )
7230 .append( this.$group, '<div style="clear:both"></div>' );
7231 if ( config.shadow ) {
7232 this.$bar.append( '<div class="oo-ui-toolbar-shadow"></div>' );
7233 }
7234 this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar );
7235 };
7236
7237 /* Setup */
7238
7239 OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
7240 OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
7241 OO.mixinClass( OO.ui.Toolbar, OO.ui.mixin.GroupElement );
7242
7243 /* Methods */
7244
7245 /**
7246 * Get the tool factory.
7247 *
7248 * @return {OO.ui.ToolFactory} Tool factory
7249 */
7250 OO.ui.Toolbar.prototype.getToolFactory = function () {
7251 return this.toolFactory;
7252 };
7253
7254 /**
7255 * Get the toolgroup factory.
7256 *
7257 * @return {OO.Factory} Toolgroup factory
7258 */
7259 OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
7260 return this.toolGroupFactory;
7261 };
7262
7263 /**
7264 * Handles mouse down events.
7265 *
7266 * @private
7267 * @param {jQuery.Event} e Mouse down event
7268 */
7269 OO.ui.Toolbar.prototype.onPointerDown = function ( e ) {
7270 var $closestWidgetToEvent = $( e.target ).closest( '.oo-ui-widget' ),
7271 $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
7272 if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[ 0 ] === $closestWidgetToToolbar[ 0 ] ) {
7273 return false;
7274 }
7275 };
7276
7277 /**
7278 * Handle window resize event.
7279 *
7280 * @private
7281 * @param {jQuery.Event} e Window resize event
7282 */
7283 OO.ui.Toolbar.prototype.onWindowResize = function () {
7284 this.$element.toggleClass(
7285 'oo-ui-toolbar-narrow',
7286 this.$bar.width() <= this.narrowThreshold
7287 );
7288 };
7289
7290 /**
7291 * Sets up handles and preloads required information for the toolbar to work.
7292 * This must be called after it is attached to a visible document and before doing anything else.
7293 */
7294 OO.ui.Toolbar.prototype.initialize = function () {
7295 this.initialized = true;
7296 this.narrowThreshold = this.$group.width() + this.$actions.width();
7297 $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
7298 this.onWindowResize();
7299 };
7300
7301 /**
7302 * Set up the toolbar.
7303 *
7304 * The toolbar is set up with a list of toolgroup configurations that specify the type of
7305 * toolgroup ({@link OO.ui.BarToolGroup bar}, {@link OO.ui.MenuToolGroup menu}, or {@link OO.ui.ListToolGroup list})
7306 * to add and which tools to include, exclude, promote, or demote within that toolgroup. Please
7307 * see {@link OO.ui.ToolGroup toolgroups} for more information about including tools in toolgroups.
7308 *
7309 * @param {Object.<string,Array>} groups List of toolgroup configurations
7310 * @param {Array|string} [groups.include] Tools to include in the toolgroup
7311 * @param {Array|string} [groups.exclude] Tools to exclude from the toolgroup
7312 * @param {Array|string} [groups.promote] Tools to promote to the beginning of the toolgroup
7313 * @param {Array|string} [groups.demote] Tools to demote to the end of the toolgroup
7314 */
7315 OO.ui.Toolbar.prototype.setup = function ( groups ) {
7316 var i, len, type, group,
7317 items = [],
7318 defaultType = 'bar';
7319
7320 // Cleanup previous groups
7321 this.reset();
7322
7323 // Build out new groups
7324 for ( i = 0, len = groups.length; i < len; i++ ) {
7325 group = groups[ i ];
7326 if ( group.include === '*' ) {
7327 // Apply defaults to catch-all groups
7328 if ( group.type === undefined ) {
7329 group.type = 'list';
7330 }
7331 if ( group.label === undefined ) {
7332 group.label = OO.ui.msg( 'ooui-toolbar-more' );
7333 }
7334 }
7335 // Check type has been registered
7336 type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType;
7337 items.push(
7338 this.getToolGroupFactory().create( type, this, group )
7339 );
7340 }
7341 this.addItems( items );
7342 };
7343
7344 /**
7345 * Remove all tools and toolgroups from the toolbar.
7346 */
7347 OO.ui.Toolbar.prototype.reset = function () {
7348 var i, len;
7349
7350 this.groups = [];
7351 this.tools = {};
7352 for ( i = 0, len = this.items.length; i < len; i++ ) {
7353 this.items[ i ].destroy();
7354 }
7355 this.clearItems();
7356 };
7357
7358 /**
7359 * Destroy the toolbar.
7360 *
7361 * Destroying the toolbar removes all event handlers and DOM elements that constitute the toolbar. Call
7362 * this method whenever you are done using a toolbar.
7363 */
7364 OO.ui.Toolbar.prototype.destroy = function () {
7365 $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
7366 this.reset();
7367 this.$element.remove();
7368 };
7369
7370 /**
7371 * Check if the tool is available.
7372 *
7373 * Available tools are ones that have not yet been added to the toolbar.
7374 *
7375 * @param {string} name Symbolic name of tool
7376 * @return {boolean} Tool is available
7377 */
7378 OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
7379 return !this.tools[ name ];
7380 };
7381
7382 /**
7383 * Prevent tool from being used again.
7384 *
7385 * @param {OO.ui.Tool} tool Tool to reserve
7386 */
7387 OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
7388 this.tools[ tool.getName() ] = tool;
7389 };
7390
7391 /**
7392 * Allow tool to be used again.
7393 *
7394 * @param {OO.ui.Tool} tool Tool to release
7395 */
7396 OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
7397 delete this.tools[ tool.getName() ];
7398 };
7399
7400 /**
7401 * Get accelerator label for tool.
7402 *
7403 * The OOjs UI library does not contain an accelerator system, but this is the hook for one. To
7404 * use an accelerator system, subclass the toolbar and override this method, which is meant to return a label
7405 * that describes the accelerator keys for the tool passed (by symbolic name) to the method.
7406 *
7407 * @param {string} name Symbolic name of tool
7408 * @return {string|undefined} Tool accelerator label if available
7409 */
7410 OO.ui.Toolbar.prototype.getToolAccelerator = function () {
7411 return undefined;
7412 };
7413
7414 /**
7415 * ToolGroups are collections of {@link OO.ui.Tool tools} that are used in a {@link OO.ui.Toolbar toolbar}.
7416 * The type of toolgroup ({@link OO.ui.ListToolGroup list}, {@link OO.ui.BarToolGroup bar}, or {@link OO.ui.MenuToolGroup menu})
7417 * to which a tool belongs determines how the tool is arranged and displayed in the toolbar. Toolgroups
7418 * themselves are created on demand with a {@link OO.ui.ToolGroupFactory toolgroup factory}.
7419 *
7420 * Toolgroups can contain individual tools, groups of tools, or all available tools:
7421 *
7422 * To include an individual tool (or array of individual tools), specify tools by symbolic name:
7423 *
7424 * include: [ 'tool-name' ] or [ { name: 'tool-name' }]
7425 *
7426 * 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.)
7427 *
7428 * include: [ { group: 'group-name' } ]
7429 *
7430 * To include all tools that are not yet assigned to a toolgroup, use the catch-all selector, an asterisk (*):
7431 *
7432 * include: '*'
7433 *
7434 * See {@link OO.ui.Toolbar toolbars} for a full example. For more information about toolbars in general,
7435 * please see the [OOjs UI documentation on MediaWiki][1].
7436 *
7437 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
7438 *
7439 * @abstract
7440 * @class
7441 * @extends OO.ui.Widget
7442 * @mixins OO.ui.mixin.GroupElement
7443 *
7444 * @constructor
7445 * @param {OO.ui.Toolbar} toolbar
7446 * @param {Object} [config] Configuration options
7447 * @cfg {Array|string} [include=[]] List of tools to include in the toolgroup.
7448 * @cfg {Array|string} [exclude=[]] List of tools to exclude from the toolgroup.
7449 * @cfg {Array|string} [promote=[]] List of tools to promote to the beginning of the toolgroup.
7450 * @cfg {Array|string} [demote=[]] List of tools to demote to the end of the toolgroup.
7451 * This setting is particularly useful when tools have been added to the toolgroup
7452 * en masse (e.g., via the catch-all selector).
7453 */
7454 OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
7455 // Allow passing positional parameters inside the config object
7456 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
7457 config = toolbar;
7458 toolbar = config.toolbar;
7459 }
7460
7461 // Configuration initialization
7462 config = config || {};
7463
7464 // Parent constructor
7465 OO.ui.ToolGroup.parent.call( this, config );
7466
7467 // Mixin constructors
7468 OO.ui.mixin.GroupElement.call( this, config );
7469
7470 // Properties
7471 this.toolbar = toolbar;
7472 this.tools = {};
7473 this.pressed = null;
7474 this.autoDisabled = false;
7475 this.include = config.include || [];
7476 this.exclude = config.exclude || [];
7477 this.promote = config.promote || [];
7478 this.demote = config.demote || [];
7479 this.onCapturedMouseKeyUpHandler = this.onCapturedMouseKeyUp.bind( this );
7480
7481 // Events
7482 this.$element.on( {
7483 mousedown: this.onMouseKeyDown.bind( this ),
7484 mouseup: this.onMouseKeyUp.bind( this ),
7485 keydown: this.onMouseKeyDown.bind( this ),
7486 keyup: this.onMouseKeyUp.bind( this ),
7487 focus: this.onMouseOverFocus.bind( this ),
7488 blur: this.onMouseOutBlur.bind( this ),
7489 mouseover: this.onMouseOverFocus.bind( this ),
7490 mouseout: this.onMouseOutBlur.bind( this )
7491 } );
7492 this.toolbar.getToolFactory().connect( this, { register: 'onToolFactoryRegister' } );
7493 this.aggregate( { disable: 'itemDisable' } );
7494 this.connect( this, { itemDisable: 'updateDisabled' } );
7495
7496 // Initialization
7497 this.$group.addClass( 'oo-ui-toolGroup-tools' );
7498 this.$element
7499 .addClass( 'oo-ui-toolGroup' )
7500 .append( this.$group );
7501 this.populate();
7502 };
7503
7504 /* Setup */
7505
7506 OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget );
7507 OO.mixinClass( OO.ui.ToolGroup, OO.ui.mixin.GroupElement );
7508
7509 /* Events */
7510
7511 /**
7512 * @event update
7513 */
7514
7515 /* Static Properties */
7516
7517 /**
7518 * Show labels in tooltips.
7519 *
7520 * @static
7521 * @inheritable
7522 * @property {boolean}
7523 */
7524 OO.ui.ToolGroup.static.titleTooltips = false;
7525
7526 /**
7527 * Show acceleration labels in tooltips.
7528 *
7529 * Note: The OOjs UI library does not include an accelerator system, but does contain
7530 * a hook for one. To use an accelerator system, subclass the {@link OO.ui.Toolbar toolbar} and
7531 * override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method, which is
7532 * meant to return a label that describes the accelerator keys for a given tool (e.g., 'Ctrl + M').
7533 *
7534 * @static
7535 * @inheritable
7536 * @property {boolean}
7537 */
7538 OO.ui.ToolGroup.static.accelTooltips = false;
7539
7540 /**
7541 * Automatically disable the toolgroup when all tools are disabled
7542 *
7543 * @static
7544 * @inheritable
7545 * @property {boolean}
7546 */
7547 OO.ui.ToolGroup.static.autoDisable = true;
7548
7549 /* Methods */
7550
7551 /**
7552 * @inheritdoc
7553 */
7554 OO.ui.ToolGroup.prototype.isDisabled = function () {
7555 return this.autoDisabled || OO.ui.ToolGroup.parent.prototype.isDisabled.apply( this, arguments );
7556 };
7557
7558 /**
7559 * @inheritdoc
7560 */
7561 OO.ui.ToolGroup.prototype.updateDisabled = function () {
7562 var i, item, allDisabled = true;
7563
7564 if ( this.constructor.static.autoDisable ) {
7565 for ( i = this.items.length - 1; i >= 0; i-- ) {
7566 item = this.items[ i ];
7567 if ( !item.isDisabled() ) {
7568 allDisabled = false;
7569 break;
7570 }
7571 }
7572 this.autoDisabled = allDisabled;
7573 }
7574 OO.ui.ToolGroup.parent.prototype.updateDisabled.apply( this, arguments );
7575 };
7576
7577 /**
7578 * Handle mouse down and key down events.
7579 *
7580 * @protected
7581 * @param {jQuery.Event} e Mouse down or key down event
7582 */
7583 OO.ui.ToolGroup.prototype.onMouseKeyDown = function ( e ) {
7584 if (
7585 !this.isDisabled() &&
7586 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
7587 ) {
7588 this.pressed = this.getTargetTool( e );
7589 if ( this.pressed ) {
7590 this.pressed.setActive( true );
7591 this.getElementDocument().addEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler, true );
7592 this.getElementDocument().addEventListener( 'keyup', this.onCapturedMouseKeyUpHandler, true );
7593 }
7594 return false;
7595 }
7596 };
7597
7598 /**
7599 * Handle captured mouse up and key up events.
7600 *
7601 * @protected
7602 * @param {Event} e Mouse up or key up event
7603 */
7604 OO.ui.ToolGroup.prototype.onCapturedMouseKeyUp = function ( e ) {
7605 this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler, true );
7606 this.getElementDocument().removeEventListener( 'keyup', this.onCapturedMouseKeyUpHandler, true );
7607 // onMouseKeyUp may be called a second time, depending on where the mouse is when the button is
7608 // released, but since `this.pressed` will no longer be true, the second call will be ignored.
7609 this.onMouseKeyUp( e );
7610 };
7611
7612 /**
7613 * Handle mouse up and key up events.
7614 *
7615 * @protected
7616 * @param {jQuery.Event} e Mouse up or key up event
7617 */
7618 OO.ui.ToolGroup.prototype.onMouseKeyUp = function ( e ) {
7619 var tool = this.getTargetTool( e );
7620
7621 if (
7622 !this.isDisabled() && this.pressed && this.pressed === tool &&
7623 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
7624 ) {
7625 this.pressed.onSelect();
7626 this.pressed = null;
7627 return false;
7628 }
7629
7630 this.pressed = null;
7631 };
7632
7633 /**
7634 * Handle mouse over and focus events.
7635 *
7636 * @protected
7637 * @param {jQuery.Event} e Mouse over or focus event
7638 */
7639 OO.ui.ToolGroup.prototype.onMouseOverFocus = function ( e ) {
7640 var tool = this.getTargetTool( e );
7641
7642 if ( this.pressed && this.pressed === tool ) {
7643 this.pressed.setActive( true );
7644 }
7645 };
7646
7647 /**
7648 * Handle mouse out and blur events.
7649 *
7650 * @protected
7651 * @param {jQuery.Event} e Mouse out or blur event
7652 */
7653 OO.ui.ToolGroup.prototype.onMouseOutBlur = function ( e ) {
7654 var tool = this.getTargetTool( e );
7655
7656 if ( this.pressed && this.pressed === tool ) {
7657 this.pressed.setActive( false );
7658 }
7659 };
7660
7661 /**
7662 * Get the closest tool to a jQuery.Event.
7663 *
7664 * Only tool links are considered, which prevents other elements in the tool such as popups from
7665 * triggering tool group interactions.
7666 *
7667 * @private
7668 * @param {jQuery.Event} e
7669 * @return {OO.ui.Tool|null} Tool, `null` if none was found
7670 */
7671 OO.ui.ToolGroup.prototype.getTargetTool = function ( e ) {
7672 var tool,
7673 $item = $( e.target ).closest( '.oo-ui-tool-link' );
7674
7675 if ( $item.length ) {
7676 tool = $item.parent().data( 'oo-ui-tool' );
7677 }
7678
7679 return tool && !tool.isDisabled() ? tool : null;
7680 };
7681
7682 /**
7683 * Handle tool registry register events.
7684 *
7685 * If a tool is registered after the group is created, we must repopulate the list to account for:
7686 *
7687 * - a tool being added that may be included
7688 * - a tool already included being overridden
7689 *
7690 * @protected
7691 * @param {string} name Symbolic name of tool
7692 */
7693 OO.ui.ToolGroup.prototype.onToolFactoryRegister = function () {
7694 this.populate();
7695 };
7696
7697 /**
7698 * Get the toolbar that contains the toolgroup.
7699 *
7700 * @return {OO.ui.Toolbar} Toolbar that contains the toolgroup
7701 */
7702 OO.ui.ToolGroup.prototype.getToolbar = function () {
7703 return this.toolbar;
7704 };
7705
7706 /**
7707 * Add and remove tools based on configuration.
7708 */
7709 OO.ui.ToolGroup.prototype.populate = function () {
7710 var i, len, name, tool,
7711 toolFactory = this.toolbar.getToolFactory(),
7712 names = {},
7713 add = [],
7714 remove = [],
7715 list = this.toolbar.getToolFactory().getTools(
7716 this.include, this.exclude, this.promote, this.demote
7717 );
7718
7719 // Build a list of needed tools
7720 for ( i = 0, len = list.length; i < len; i++ ) {
7721 name = list[ i ];
7722 if (
7723 // Tool exists
7724 toolFactory.lookup( name ) &&
7725 // Tool is available or is already in this group
7726 ( this.toolbar.isToolAvailable( name ) || this.tools[ name ] )
7727 ) {
7728 // Hack to prevent infinite recursion via ToolGroupTool. We need to reserve the tool before
7729 // creating it, but we can't call reserveTool() yet because we haven't created the tool.
7730 this.toolbar.tools[ name ] = true;
7731 tool = this.tools[ name ];
7732 if ( !tool ) {
7733 // Auto-initialize tools on first use
7734 this.tools[ name ] = tool = toolFactory.create( name, this );
7735 tool.updateTitle();
7736 }
7737 this.toolbar.reserveTool( tool );
7738 add.push( tool );
7739 names[ name ] = true;
7740 }
7741 }
7742 // Remove tools that are no longer needed
7743 for ( name in this.tools ) {
7744 if ( !names[ name ] ) {
7745 this.tools[ name ].destroy();
7746 this.toolbar.releaseTool( this.tools[ name ] );
7747 remove.push( this.tools[ name ] );
7748 delete this.tools[ name ];
7749 }
7750 }
7751 if ( remove.length ) {
7752 this.removeItems( remove );
7753 }
7754 // Update emptiness state
7755 if ( add.length ) {
7756 this.$element.removeClass( 'oo-ui-toolGroup-empty' );
7757 } else {
7758 this.$element.addClass( 'oo-ui-toolGroup-empty' );
7759 }
7760 // Re-add tools (moving existing ones to new locations)
7761 this.addItems( add );
7762 // Disabled state may depend on items
7763 this.updateDisabled();
7764 };
7765
7766 /**
7767 * Destroy toolgroup.
7768 */
7769 OO.ui.ToolGroup.prototype.destroy = function () {
7770 var name;
7771
7772 this.clearItems();
7773 this.toolbar.getToolFactory().disconnect( this );
7774 for ( name in this.tools ) {
7775 this.toolbar.releaseTool( this.tools[ name ] );
7776 this.tools[ name ].disconnect( this ).destroy();
7777 delete this.tools[ name ];
7778 }
7779 this.$element.remove();
7780 };
7781
7782 /**
7783 * MessageDialogs display a confirmation or alert message. By default, the rendered dialog box
7784 * consists of a header that contains the dialog title, a body with the message, and a footer that
7785 * contains any {@link OO.ui.ActionWidget action widgets}. The MessageDialog class is the only type
7786 * of {@link OO.ui.Dialog dialog} that is usually instantiated directly.
7787 *
7788 * There are two basic types of message dialogs, confirmation and alert:
7789 *
7790 * - **confirmation**: the dialog title describes what a progressive action will do and the message provides
7791 * more details about the consequences.
7792 * - **alert**: the dialog title describes which event occurred and the message provides more information
7793 * about why the event occurred.
7794 *
7795 * The MessageDialog class specifies two actions: ‘accept’, the primary
7796 * action (e.g., ‘ok’) and ‘reject,’ the safe action (e.g., ‘cancel’). Both will close the window,
7797 * passing along the selected action.
7798 *
7799 * For more information and examples, please see the [OOjs UI documentation on MediaWiki][1].
7800 *
7801 * @example
7802 * // Example: Creating and opening a message dialog window.
7803 * var messageDialog = new OO.ui.MessageDialog();
7804 *
7805 * // Create and append a window manager.
7806 * var windowManager = new OO.ui.WindowManager();
7807 * $( 'body' ).append( windowManager.$element );
7808 * windowManager.addWindows( [ messageDialog ] );
7809 * // Open the window.
7810 * windowManager.openWindow( messageDialog, {
7811 * title: 'Basic message dialog',
7812 * message: 'This is the message'
7813 * } );
7814 *
7815 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Message_Dialogs
7816 *
7817 * @class
7818 * @extends OO.ui.Dialog
7819 *
7820 * @constructor
7821 * @param {Object} [config] Configuration options
7822 */
7823 OO.ui.MessageDialog = function OoUiMessageDialog( config ) {
7824 // Parent constructor
7825 OO.ui.MessageDialog.parent.call( this, config );
7826
7827 // Properties
7828 this.verticalActionLayout = null;
7829
7830 // Initialization
7831 this.$element.addClass( 'oo-ui-messageDialog' );
7832 };
7833
7834 /* Setup */
7835
7836 OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog );
7837
7838 /* Static Properties */
7839
7840 OO.ui.MessageDialog.static.name = 'message';
7841
7842 OO.ui.MessageDialog.static.size = 'small';
7843
7844 OO.ui.MessageDialog.static.verbose = false;
7845
7846 /**
7847 * Dialog title.
7848 *
7849 * The title of a confirmation dialog describes what a progressive action will do. The
7850 * title of an alert dialog describes which event occurred.
7851 *
7852 * @static
7853 * @inheritable
7854 * @property {jQuery|string|Function|null}
7855 */
7856 OO.ui.MessageDialog.static.title = null;
7857
7858 /**
7859 * The message displayed in the dialog body.
7860 *
7861 * A confirmation message describes the consequences of a progressive action. An alert
7862 * message describes why an event occurred.
7863 *
7864 * @static
7865 * @inheritable
7866 * @property {jQuery|string|Function|null}
7867 */
7868 OO.ui.MessageDialog.static.message = null;
7869
7870 OO.ui.MessageDialog.static.actions = [
7871 { action: 'accept', label: OO.ui.deferMsg( 'ooui-dialog-message-accept' ), flags: 'primary' },
7872 { action: 'reject', label: OO.ui.deferMsg( 'ooui-dialog-message-reject' ), flags: 'safe' }
7873 ];
7874
7875 /* Methods */
7876
7877 /**
7878 * @inheritdoc
7879 */
7880 OO.ui.MessageDialog.prototype.setManager = function ( manager ) {
7881 OO.ui.MessageDialog.parent.prototype.setManager.call( this, manager );
7882
7883 // Events
7884 this.manager.connect( this, {
7885 resize: 'onResize'
7886 } );
7887
7888 return this;
7889 };
7890
7891 /**
7892 * @inheritdoc
7893 */
7894 OO.ui.MessageDialog.prototype.onActionResize = function ( action ) {
7895 this.fitActions();
7896 return OO.ui.MessageDialog.parent.prototype.onActionResize.call( this, action );
7897 };
7898
7899 /**
7900 * Handle window resized events.
7901 *
7902 * @private
7903 */
7904 OO.ui.MessageDialog.prototype.onResize = function () {
7905 var dialog = this;
7906 dialog.fitActions();
7907 // Wait for CSS transition to finish and do it again :(
7908 setTimeout( function () {
7909 dialog.fitActions();
7910 }, 300 );
7911 };
7912
7913 /**
7914 * Toggle action layout between vertical and horizontal.
7915 *
7916 *
7917 * @private
7918 * @param {boolean} [value] Layout actions vertically, omit to toggle
7919 * @chainable
7920 */
7921 OO.ui.MessageDialog.prototype.toggleVerticalActionLayout = function ( value ) {
7922 value = value === undefined ? !this.verticalActionLayout : !!value;
7923
7924 if ( value !== this.verticalActionLayout ) {
7925 this.verticalActionLayout = value;
7926 this.$actions
7927 .toggleClass( 'oo-ui-messageDialog-actions-vertical', value )
7928 .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value );
7929 }
7930
7931 return this;
7932 };
7933
7934 /**
7935 * @inheritdoc
7936 */
7937 OO.ui.MessageDialog.prototype.getActionProcess = function ( action ) {
7938 if ( action ) {
7939 return new OO.ui.Process( function () {
7940 this.close( { action: action } );
7941 }, this );
7942 }
7943 return OO.ui.MessageDialog.parent.prototype.getActionProcess.call( this, action );
7944 };
7945
7946 /**
7947 * @inheritdoc
7948 *
7949 * @param {Object} [data] Dialog opening data
7950 * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
7951 * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
7952 * @param {boolean} [data.verbose] Message is verbose and should be styled as a long message
7953 * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each
7954 * action item
7955 */
7956 OO.ui.MessageDialog.prototype.getSetupProcess = function ( data ) {
7957 data = data || {};
7958
7959 // Parent method
7960 return OO.ui.MessageDialog.parent.prototype.getSetupProcess.call( this, data )
7961 .next( function () {
7962 this.title.setLabel(
7963 data.title !== undefined ? data.title : this.constructor.static.title
7964 );
7965 this.message.setLabel(
7966 data.message !== undefined ? data.message : this.constructor.static.message
7967 );
7968 this.message.$element.toggleClass(
7969 'oo-ui-messageDialog-message-verbose',
7970 data.verbose !== undefined ? data.verbose : this.constructor.static.verbose
7971 );
7972 }, this );
7973 };
7974
7975 /**
7976 * @inheritdoc
7977 */
7978 OO.ui.MessageDialog.prototype.getBodyHeight = function () {
7979 var bodyHeight, oldOverflow,
7980 $scrollable = this.container.$element;
7981
7982 oldOverflow = $scrollable[ 0 ].style.overflow;
7983 $scrollable[ 0 ].style.overflow = 'hidden';
7984
7985 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
7986
7987 bodyHeight = this.text.$element.outerHeight( true );
7988 $scrollable[ 0 ].style.overflow = oldOverflow;
7989
7990 return bodyHeight;
7991 };
7992
7993 /**
7994 * @inheritdoc
7995 */
7996 OO.ui.MessageDialog.prototype.setDimensions = function ( dim ) {
7997 var $scrollable = this.container.$element;
7998 OO.ui.MessageDialog.parent.prototype.setDimensions.call( this, dim );
7999
8000 // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
8001 // Need to do it after transition completes (250ms), add 50ms just in case.
8002 setTimeout( function () {
8003 var oldOverflow = $scrollable[ 0 ].style.overflow;
8004 $scrollable[ 0 ].style.overflow = 'hidden';
8005
8006 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
8007
8008 $scrollable[ 0 ].style.overflow = oldOverflow;
8009 }, 300 );
8010
8011 return this;
8012 };
8013
8014 /**
8015 * @inheritdoc
8016 */
8017 OO.ui.MessageDialog.prototype.initialize = function () {
8018 // Parent method
8019 OO.ui.MessageDialog.parent.prototype.initialize.call( this );
8020
8021 // Properties
8022 this.$actions = $( '<div>' );
8023 this.container = new OO.ui.PanelLayout( {
8024 scrollable: true, classes: [ 'oo-ui-messageDialog-container' ]
8025 } );
8026 this.text = new OO.ui.PanelLayout( {
8027 padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ]
8028 } );
8029 this.message = new OO.ui.LabelWidget( {
8030 classes: [ 'oo-ui-messageDialog-message' ]
8031 } );
8032
8033 // Initialization
8034 this.title.$element.addClass( 'oo-ui-messageDialog-title' );
8035 this.$content.addClass( 'oo-ui-messageDialog-content' );
8036 this.container.$element.append( this.text.$element );
8037 this.text.$element.append( this.title.$element, this.message.$element );
8038 this.$body.append( this.container.$element );
8039 this.$actions.addClass( 'oo-ui-messageDialog-actions' );
8040 this.$foot.append( this.$actions );
8041 };
8042
8043 /**
8044 * @inheritdoc
8045 */
8046 OO.ui.MessageDialog.prototype.attachActions = function () {
8047 var i, len, other, special, others;
8048
8049 // Parent method
8050 OO.ui.MessageDialog.parent.prototype.attachActions.call( this );
8051
8052 special = this.actions.getSpecial();
8053 others = this.actions.getOthers();
8054 if ( special.safe ) {
8055 this.$actions.append( special.safe.$element );
8056 special.safe.toggleFramed( false );
8057 }
8058 if ( others.length ) {
8059 for ( i = 0, len = others.length; i < len; i++ ) {
8060 other = others[ i ];
8061 this.$actions.append( other.$element );
8062 other.toggleFramed( false );
8063 }
8064 }
8065 if ( special.primary ) {
8066 this.$actions.append( special.primary.$element );
8067 special.primary.toggleFramed( false );
8068 }
8069
8070 if ( !this.isOpening() ) {
8071 // If the dialog is currently opening, this will be called automatically soon.
8072 // This also calls #fitActions.
8073 this.updateSize();
8074 }
8075 };
8076
8077 /**
8078 * Fit action actions into columns or rows.
8079 *
8080 * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
8081 *
8082 * @private
8083 */
8084 OO.ui.MessageDialog.prototype.fitActions = function () {
8085 var i, len, action,
8086 previous = this.verticalActionLayout,
8087 actions = this.actions.get();
8088
8089 // Detect clipping
8090 this.toggleVerticalActionLayout( false );
8091 for ( i = 0, len = actions.length; i < len; i++ ) {
8092 action = actions[ i ];
8093 if ( action.$element.innerWidth() < action.$label.outerWidth( true ) ) {
8094 this.toggleVerticalActionLayout( true );
8095 break;
8096 }
8097 }
8098
8099 // Move the body out of the way of the foot
8100 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
8101
8102 if ( this.verticalActionLayout !== previous ) {
8103 // We changed the layout, window height might need to be updated.
8104 this.updateSize();
8105 }
8106 };
8107
8108 /**
8109 * ProcessDialog windows encapsulate a {@link OO.ui.Process process} and all of the code necessary
8110 * to complete it. If the process terminates with an error, a customizable {@link OO.ui.Error error
8111 * interface} alerts users to the trouble, permitting the user to dismiss the error and try again when
8112 * relevant. The ProcessDialog class is always extended and customized with the actions and content
8113 * required for each process.
8114 *
8115 * The process dialog box consists of a header that visually represents the ‘working’ state of long
8116 * processes with an animation. The header contains the dialog title as well as
8117 * two {@link OO.ui.ActionWidget action widgets}: a ‘safe’ action on the left (e.g., ‘Cancel’) and
8118 * a ‘primary’ action on the right (e.g., ‘Done’).
8119 *
8120 * Like other windows, the process dialog is managed by a {@link OO.ui.WindowManager window manager}.
8121 * Please see the [OOjs UI documentation on MediaWiki][1] for more information and examples.
8122 *
8123 * @example
8124 * // Example: Creating and opening a process dialog window.
8125 * function MyProcessDialog( config ) {
8126 * MyProcessDialog.parent.call( this, config );
8127 * }
8128 * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
8129 *
8130 * MyProcessDialog.static.title = 'Process dialog';
8131 * MyProcessDialog.static.actions = [
8132 * { action: 'save', label: 'Done', flags: 'primary' },
8133 * { label: 'Cancel', flags: 'safe' }
8134 * ];
8135 *
8136 * MyProcessDialog.prototype.initialize = function () {
8137 * MyProcessDialog.parent.prototype.initialize.apply( this, arguments );
8138 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
8139 * 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>' );
8140 * this.$body.append( this.content.$element );
8141 * };
8142 * MyProcessDialog.prototype.getActionProcess = function ( action ) {
8143 * var dialog = this;
8144 * if ( action ) {
8145 * return new OO.ui.Process( function () {
8146 * dialog.close( { action: action } );
8147 * } );
8148 * }
8149 * return MyProcessDialog.parent.prototype.getActionProcess.call( this, action );
8150 * };
8151 *
8152 * var windowManager = new OO.ui.WindowManager();
8153 * $( 'body' ).append( windowManager.$element );
8154 *
8155 * var dialog = new MyProcessDialog();
8156 * windowManager.addWindows( [ dialog ] );
8157 * windowManager.openWindow( dialog );
8158 *
8159 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
8160 *
8161 * @abstract
8162 * @class
8163 * @extends OO.ui.Dialog
8164 *
8165 * @constructor
8166 * @param {Object} [config] Configuration options
8167 */
8168 OO.ui.ProcessDialog = function OoUiProcessDialog( config ) {
8169 // Parent constructor
8170 OO.ui.ProcessDialog.parent.call( this, config );
8171
8172 // Initialization
8173 this.$element.addClass( 'oo-ui-processDialog' );
8174 };
8175
8176 /* Setup */
8177
8178 OO.inheritClass( OO.ui.ProcessDialog, OO.ui.Dialog );
8179
8180 /* Methods */
8181
8182 /**
8183 * Handle dismiss button click events.
8184 *
8185 * Hides errors.
8186 *
8187 * @private
8188 */
8189 OO.ui.ProcessDialog.prototype.onDismissErrorButtonClick = function () {
8190 this.hideErrors();
8191 };
8192
8193 /**
8194 * Handle retry button click events.
8195 *
8196 * Hides errors and then tries again.
8197 *
8198 * @private
8199 */
8200 OO.ui.ProcessDialog.prototype.onRetryButtonClick = function () {
8201 this.hideErrors();
8202 this.executeAction( this.currentAction );
8203 };
8204
8205 /**
8206 * @inheritdoc
8207 */
8208 OO.ui.ProcessDialog.prototype.onActionResize = function ( action ) {
8209 if ( this.actions.isSpecial( action ) ) {
8210 this.fitLabel();
8211 }
8212 return OO.ui.ProcessDialog.parent.prototype.onActionResize.call( this, action );
8213 };
8214
8215 /**
8216 * @inheritdoc
8217 */
8218 OO.ui.ProcessDialog.prototype.initialize = function () {
8219 // Parent method
8220 OO.ui.ProcessDialog.parent.prototype.initialize.call( this );
8221
8222 // Properties
8223 this.$navigation = $( '<div>' );
8224 this.$location = $( '<div>' );
8225 this.$safeActions = $( '<div>' );
8226 this.$primaryActions = $( '<div>' );
8227 this.$otherActions = $( '<div>' );
8228 this.dismissButton = new OO.ui.ButtonWidget( {
8229 label: OO.ui.msg( 'ooui-dialog-process-dismiss' )
8230 } );
8231 this.retryButton = new OO.ui.ButtonWidget();
8232 this.$errors = $( '<div>' );
8233 this.$errorsTitle = $( '<div>' );
8234
8235 // Events
8236 this.dismissButton.connect( this, { click: 'onDismissErrorButtonClick' } );
8237 this.retryButton.connect( this, { click: 'onRetryButtonClick' } );
8238
8239 // Initialization
8240 this.title.$element.addClass( 'oo-ui-processDialog-title' );
8241 this.$location
8242 .append( this.title.$element )
8243 .addClass( 'oo-ui-processDialog-location' );
8244 this.$safeActions.addClass( 'oo-ui-processDialog-actions-safe' );
8245 this.$primaryActions.addClass( 'oo-ui-processDialog-actions-primary' );
8246 this.$otherActions.addClass( 'oo-ui-processDialog-actions-other' );
8247 this.$errorsTitle
8248 .addClass( 'oo-ui-processDialog-errors-title' )
8249 .text( OO.ui.msg( 'ooui-dialog-process-error' ) );
8250 this.$errors
8251 .addClass( 'oo-ui-processDialog-errors oo-ui-element-hidden' )
8252 .append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element );
8253 this.$content
8254 .addClass( 'oo-ui-processDialog-content' )
8255 .append( this.$errors );
8256 this.$navigation
8257 .addClass( 'oo-ui-processDialog-navigation' )
8258 .append( this.$safeActions, this.$location, this.$primaryActions );
8259 this.$head.append( this.$navigation );
8260 this.$foot.append( this.$otherActions );
8261 };
8262
8263 /**
8264 * @inheritdoc
8265 */
8266 OO.ui.ProcessDialog.prototype.getActionWidgets = function ( actions ) {
8267 var i, len, widgets = [];
8268 for ( i = 0, len = actions.length; i < len; i++ ) {
8269 widgets.push(
8270 new OO.ui.ActionWidget( $.extend( { framed: true }, actions[ i ] ) )
8271 );
8272 }
8273 return widgets;
8274 };
8275
8276 /**
8277 * @inheritdoc
8278 */
8279 OO.ui.ProcessDialog.prototype.attachActions = function () {
8280 var i, len, other, special, others;
8281
8282 // Parent method
8283 OO.ui.ProcessDialog.parent.prototype.attachActions.call( this );
8284
8285 special = this.actions.getSpecial();
8286 others = this.actions.getOthers();
8287 if ( special.primary ) {
8288 this.$primaryActions.append( special.primary.$element );
8289 }
8290 for ( i = 0, len = others.length; i < len; i++ ) {
8291 other = others[ i ];
8292 this.$otherActions.append( other.$element );
8293 }
8294 if ( special.safe ) {
8295 this.$safeActions.append( special.safe.$element );
8296 }
8297
8298 this.fitLabel();
8299 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
8300 };
8301
8302 /**
8303 * @inheritdoc
8304 */
8305 OO.ui.ProcessDialog.prototype.executeAction = function ( action ) {
8306 var process = this;
8307 return OO.ui.ProcessDialog.parent.prototype.executeAction.call( this, action )
8308 .fail( function ( errors ) {
8309 process.showErrors( errors || [] );
8310 } );
8311 };
8312
8313 /**
8314 * Fit label between actions.
8315 *
8316 * @private
8317 * @chainable
8318 */
8319 OO.ui.ProcessDialog.prototype.fitLabel = function () {
8320 var safeWidth, primaryWidth, biggerWidth, labelWidth, navigationWidth, leftWidth, rightWidth;
8321
8322 safeWidth = this.$safeActions.is( ':visible' ) ? this.$safeActions.width() : 0;
8323 primaryWidth = this.$primaryActions.is( ':visible' ) ? this.$primaryActions.width() : 0;
8324 biggerWidth = Math.max( safeWidth, primaryWidth );
8325
8326 labelWidth = this.title.$element.width();
8327 // Is there a better way to calculate this?
8328 navigationWidth = OO.ui.WindowManager.static.sizes[ this.getSize() ].width - 20;
8329
8330 if ( 2 * biggerWidth + labelWidth < navigationWidth ) {
8331 // We have enough space to center the label
8332 leftWidth = rightWidth = biggerWidth;
8333 } else {
8334 // Let's hope we at least have enough space not to overlap, because we can't wrap the label…
8335 if ( this.getDir() === 'ltr' ) {
8336 leftWidth = safeWidth;
8337 rightWidth = primaryWidth;
8338 } else {
8339 leftWidth = primaryWidth;
8340 rightWidth = safeWidth;
8341 }
8342 }
8343
8344 this.$location.css( { paddingLeft: leftWidth, paddingRight: rightWidth } );
8345
8346 return this;
8347 };
8348
8349 /**
8350 * Handle errors that occurred during accept or reject processes.
8351 *
8352 * @private
8353 * @param {OO.ui.Error[]|OO.ui.Error} errors Errors to be handled
8354 */
8355 OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
8356 var i, len, $item, actions,
8357 items = [],
8358 abilities = {},
8359 recoverable = true,
8360 warning = false;
8361
8362 if ( errors instanceof OO.ui.Error ) {
8363 errors = [ errors ];
8364 }
8365
8366 for ( i = 0, len = errors.length; i < len; i++ ) {
8367 if ( !errors[ i ].isRecoverable() ) {
8368 recoverable = false;
8369 }
8370 if ( errors[ i ].isWarning() ) {
8371 warning = true;
8372 }
8373 $item = $( '<div>' )
8374 .addClass( 'oo-ui-processDialog-error' )
8375 .append( errors[ i ].getMessage() );
8376 items.push( $item[ 0 ] );
8377 }
8378 this.$errorItems = $( items );
8379 if ( recoverable ) {
8380 abilities[this.currentAction] = true;
8381 // Copy the flags from the first matching action
8382 actions = this.actions.get( { actions: this.currentAction } );
8383 if ( actions.length ) {
8384 this.retryButton.clearFlags().setFlags( actions[0].getFlags() );
8385 }
8386 } else {
8387 abilities[this.currentAction] = false;
8388 this.actions.setAbilities( abilities );
8389 }
8390 if ( warning ) {
8391 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-continue' ) );
8392 } else {
8393 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-retry' ) );
8394 }
8395 this.retryButton.toggle( recoverable );
8396 this.$errorsTitle.after( this.$errorItems );
8397 this.$errors.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 );
8398 };
8399
8400 /**
8401 * Hide errors.
8402 *
8403 * @private
8404 */
8405 OO.ui.ProcessDialog.prototype.hideErrors = function () {
8406 this.$errors.addClass( 'oo-ui-element-hidden' );
8407 if ( this.$errorItems ) {
8408 this.$errorItems.remove();
8409 this.$errorItems = null;
8410 }
8411 };
8412
8413 /**
8414 * @inheritdoc
8415 */
8416 OO.ui.ProcessDialog.prototype.getTeardownProcess = function ( data ) {
8417 // Parent method
8418 return OO.ui.ProcessDialog.parent.prototype.getTeardownProcess.call( this, data )
8419 .first( function () {
8420 // Make sure to hide errors
8421 this.hideErrors();
8422 }, this );
8423 };
8424
8425 /**
8426 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
8427 * which is a widget that is specified by reference before any optional configuration settings.
8428 *
8429 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
8430 *
8431 * - **left**: The label is placed before the field-widget and aligned with the left margin.
8432 * A left-alignment is used for forms with many fields.
8433 * - **right**: The label is placed before the field-widget and aligned to the right margin.
8434 * A right-alignment is used for long but familiar forms which users tab through,
8435 * verifying the current field with a quick glance at the label.
8436 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
8437 * that users fill out from top to bottom.
8438 * - **inline**: The label is placed after the field-widget and aligned to the left.
8439 * An inline-alignment is best used with checkboxes or radio buttons.
8440 *
8441 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
8442 * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
8443 *
8444 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
8445 * @class
8446 * @extends OO.ui.Layout
8447 * @mixins OO.ui.mixin.LabelElement
8448 *
8449 * @constructor
8450 * @param {OO.ui.Widget} fieldWidget Field widget
8451 * @param {Object} [config] Configuration options
8452 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
8453 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a help icon will appear
8454 * in the upper-right corner of the rendered field.
8455 */
8456 OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
8457 // Allow passing positional parameters inside the config object
8458 if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
8459 config = fieldWidget;
8460 fieldWidget = config.fieldWidget;
8461 }
8462
8463 var hasInputWidget = fieldWidget.constructor.static.supportsSimpleLabel,
8464 div;
8465
8466 // Configuration initialization
8467 config = $.extend( { align: 'left' }, config );
8468
8469 // Parent constructor
8470 OO.ui.FieldLayout.parent.call( this, config );
8471
8472 // Mixin constructors
8473 OO.ui.mixin.LabelElement.call( this, config );
8474
8475 // Properties
8476 this.fieldWidget = fieldWidget;
8477 this.$field = $( '<div>' );
8478 this.$body = $( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
8479 this.align = null;
8480 if ( config.help ) {
8481 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
8482 classes: [ 'oo-ui-fieldLayout-help' ],
8483 framed: false,
8484 icon: 'info'
8485 } );
8486
8487 div = $( '<div>' );
8488 if ( config.help instanceof OO.ui.HtmlSnippet ) {
8489 div.html( config.help.toString() );
8490 } else {
8491 div.text( config.help );
8492 }
8493 this.popupButtonWidget.getPopup().$body.append(
8494 div.addClass( 'oo-ui-fieldLayout-help-content' )
8495 );
8496 this.$help = this.popupButtonWidget.$element;
8497 } else {
8498 this.$help = $( [] );
8499 }
8500
8501 // Events
8502 if ( hasInputWidget ) {
8503 this.$label.on( 'click', this.onLabelClick.bind( this ) );
8504 }
8505 this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
8506
8507 // Initialization
8508 this.$element
8509 .addClass( 'oo-ui-fieldLayout' )
8510 .append( this.$help, this.$body );
8511 this.$body.addClass( 'oo-ui-fieldLayout-body' );
8512 this.$field
8513 .addClass( 'oo-ui-fieldLayout-field' )
8514 .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget.isDisabled() )
8515 .append( this.fieldWidget.$element );
8516
8517 this.setAlignment( config.align );
8518 };
8519
8520 /* Setup */
8521
8522 OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
8523 OO.mixinClass( OO.ui.FieldLayout, OO.ui.mixin.LabelElement );
8524
8525 /* Methods */
8526
8527 /**
8528 * Handle field disable events.
8529 *
8530 * @private
8531 * @param {boolean} value Field is disabled
8532 */
8533 OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
8534 this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
8535 };
8536
8537 /**
8538 * Handle label mouse click events.
8539 *
8540 * @private
8541 * @param {jQuery.Event} e Mouse click event
8542 */
8543 OO.ui.FieldLayout.prototype.onLabelClick = function () {
8544 this.fieldWidget.simulateLabelClick();
8545 return false;
8546 };
8547
8548 /**
8549 * Get the widget contained by the field.
8550 *
8551 * @return {OO.ui.Widget} Field widget
8552 */
8553 OO.ui.FieldLayout.prototype.getField = function () {
8554 return this.fieldWidget;
8555 };
8556
8557 /**
8558 * Set the field alignment mode.
8559 *
8560 * @private
8561 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
8562 * @chainable
8563 */
8564 OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
8565 if ( value !== this.align ) {
8566 // Default to 'left'
8567 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) {
8568 value = 'left';
8569 }
8570 // Reorder elements
8571 if ( value === 'inline' ) {
8572 this.$body.append( this.$field, this.$label );
8573 } else {
8574 this.$body.append( this.$label, this.$field );
8575 }
8576 // Set classes. The following classes can be used here:
8577 // * oo-ui-fieldLayout-align-left
8578 // * oo-ui-fieldLayout-align-right
8579 // * oo-ui-fieldLayout-align-top
8580 // * oo-ui-fieldLayout-align-inline
8581 if ( this.align ) {
8582 this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
8583 }
8584 this.$element.addClass( 'oo-ui-fieldLayout-align-' + value );
8585 this.align = value;
8586 }
8587
8588 return this;
8589 };
8590
8591 /**
8592 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
8593 * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
8594 * is required and is specified before any optional configuration settings.
8595 *
8596 * Labels can be aligned in one of four ways:
8597 *
8598 * - **left**: The label is placed before the field-widget and aligned with the left margin.
8599 * A left-alignment is used for forms with many fields.
8600 * - **right**: The label is placed before the field-widget and aligned to the right margin.
8601 * A right-alignment is used for long but familiar forms which users tab through,
8602 * verifying the current field with a quick glance at the label.
8603 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
8604 * that users fill out from top to bottom.
8605 * - **inline**: The label is placed after the field-widget and aligned to the left.
8606 * An inline-alignment is best used with checkboxes or radio buttons.
8607 *
8608 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
8609 * text is specified.
8610 *
8611 * @example
8612 * // Example of an ActionFieldLayout
8613 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
8614 * new OO.ui.TextInputWidget( {
8615 * placeholder: 'Field widget'
8616 * } ),
8617 * new OO.ui.ButtonWidget( {
8618 * label: 'Button'
8619 * } ),
8620 * {
8621 * label: 'An ActionFieldLayout. This label is aligned top',
8622 * align: 'top',
8623 * help: 'This is help text'
8624 * }
8625 * );
8626 *
8627 * $( 'body' ).append( actionFieldLayout.$element );
8628 *
8629 *
8630 * @class
8631 * @extends OO.ui.FieldLayout
8632 *
8633 * @constructor
8634 * @param {OO.ui.Widget} fieldWidget Field widget
8635 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
8636 */
8637 OO.ui.ActionFieldLayout = function OoUiActionFieldLayout( fieldWidget, buttonWidget, config ) {
8638 // Allow passing positional parameters inside the config object
8639 if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
8640 config = fieldWidget;
8641 fieldWidget = config.fieldWidget;
8642 buttonWidget = config.buttonWidget;
8643 }
8644
8645 // Parent constructor
8646 OO.ui.ActionFieldLayout.parent.call( this, fieldWidget, config );
8647
8648 // Properties
8649 this.buttonWidget = buttonWidget;
8650 this.$button = $( '<div>' );
8651 this.$input = $( '<div>' );
8652
8653 // Initialization
8654 this.$element
8655 .addClass( 'oo-ui-actionFieldLayout' );
8656 this.$button
8657 .addClass( 'oo-ui-actionFieldLayout-button' )
8658 .append( this.buttonWidget.$element );
8659 this.$input
8660 .addClass( 'oo-ui-actionFieldLayout-input' )
8661 .append( this.fieldWidget.$element );
8662 this.$field
8663 .append( this.$input, this.$button );
8664 };
8665
8666 /* Setup */
8667
8668 OO.inheritClass( OO.ui.ActionFieldLayout, OO.ui.FieldLayout );
8669
8670 /**
8671 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
8672 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
8673 * configured with a label as well. For more information and examples,
8674 * please see the [OOjs UI documentation on MediaWiki][1].
8675 *
8676 * @example
8677 * // Example of a fieldset layout
8678 * var input1 = new OO.ui.TextInputWidget( {
8679 * placeholder: 'A text input field'
8680 * } );
8681 *
8682 * var input2 = new OO.ui.TextInputWidget( {
8683 * placeholder: 'A text input field'
8684 * } );
8685 *
8686 * var fieldset = new OO.ui.FieldsetLayout( {
8687 * label: 'Example of a fieldset layout'
8688 * } );
8689 *
8690 * fieldset.addItems( [
8691 * new OO.ui.FieldLayout( input1, {
8692 * label: 'Field One'
8693 * } ),
8694 * new OO.ui.FieldLayout( input2, {
8695 * label: 'Field Two'
8696 * } )
8697 * ] );
8698 * $( 'body' ).append( fieldset.$element );
8699 *
8700 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
8701 *
8702 * @class
8703 * @extends OO.ui.Layout
8704 * @mixins OO.ui.mixin.IconElement
8705 * @mixins OO.ui.mixin.LabelElement
8706 * @mixins OO.ui.mixin.GroupElement
8707 *
8708 * @constructor
8709 * @param {Object} [config] Configuration options
8710 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
8711 */
8712 OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
8713 // Configuration initialization
8714 config = config || {};
8715
8716 // Parent constructor
8717 OO.ui.FieldsetLayout.parent.call( this, config );
8718
8719 // Mixin constructors
8720 OO.ui.mixin.IconElement.call( this, config );
8721 OO.ui.mixin.LabelElement.call( this, config );
8722 OO.ui.mixin.GroupElement.call( this, config );
8723
8724 if ( config.help ) {
8725 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
8726 classes: [ 'oo-ui-fieldsetLayout-help' ],
8727 framed: false,
8728 icon: 'info'
8729 } );
8730
8731 this.popupButtonWidget.getPopup().$body.append(
8732 $( '<div>' )
8733 .text( config.help )
8734 .addClass( 'oo-ui-fieldsetLayout-help-content' )
8735 );
8736 this.$help = this.popupButtonWidget.$element;
8737 } else {
8738 this.$help = $( [] );
8739 }
8740
8741 // Initialization
8742 this.$element
8743 .addClass( 'oo-ui-fieldsetLayout' )
8744 .prepend( this.$help, this.$icon, this.$label, this.$group );
8745 if ( Array.isArray( config.items ) ) {
8746 this.addItems( config.items );
8747 }
8748 };
8749
8750 /* Setup */
8751
8752 OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
8753 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.IconElement );
8754 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.LabelElement );
8755 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.GroupElement );
8756
8757 /**
8758 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
8759 * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
8760 * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
8761 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
8762 *
8763 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
8764 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
8765 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
8766 * some fancier controls. Some controls have both regular and InputWidget variants, for example
8767 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
8768 * often have simplified APIs to match the capabilities of HTML forms.
8769 * See the [OOjs UI Inputs documentation on MediaWiki] [2] for more information about InputWidgets.
8770 *
8771 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Forms
8772 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8773 *
8774 * @example
8775 * // Example of a form layout that wraps a fieldset layout
8776 * var input1 = new OO.ui.TextInputWidget( {
8777 * placeholder: 'Username'
8778 * } );
8779 * var input2 = new OO.ui.TextInputWidget( {
8780 * placeholder: 'Password',
8781 * type: 'password'
8782 * } );
8783 * var submit = new OO.ui.ButtonInputWidget( {
8784 * label: 'Submit'
8785 * } );
8786 *
8787 * var fieldset = new OO.ui.FieldsetLayout( {
8788 * label: 'A form layout'
8789 * } );
8790 * fieldset.addItems( [
8791 * new OO.ui.FieldLayout( input1, {
8792 * label: 'Username',
8793 * align: 'top'
8794 * } ),
8795 * new OO.ui.FieldLayout( input2, {
8796 * label: 'Password',
8797 * align: 'top'
8798 * } ),
8799 * new OO.ui.FieldLayout( submit )
8800 * ] );
8801 * var form = new OO.ui.FormLayout( {
8802 * items: [ fieldset ],
8803 * action: '/api/formhandler',
8804 * method: 'get'
8805 * } )
8806 * $( 'body' ).append( form.$element );
8807 *
8808 * @class
8809 * @extends OO.ui.Layout
8810 * @mixins OO.ui.mixin.GroupElement
8811 *
8812 * @constructor
8813 * @param {Object} [config] Configuration options
8814 * @cfg {string} [method] HTML form `method` attribute
8815 * @cfg {string} [action] HTML form `action` attribute
8816 * @cfg {string} [enctype] HTML form `enctype` attribute
8817 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
8818 */
8819 OO.ui.FormLayout = function OoUiFormLayout( config ) {
8820 // Configuration initialization
8821 config = config || {};
8822
8823 // Parent constructor
8824 OO.ui.FormLayout.parent.call( this, config );
8825
8826 // Mixin constructors
8827 OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
8828
8829 // Events
8830 this.$element.on( 'submit', this.onFormSubmit.bind( this ) );
8831
8832 // Initialization
8833 this.$element
8834 .addClass( 'oo-ui-formLayout' )
8835 .attr( {
8836 method: config.method,
8837 action: config.action,
8838 enctype: config.enctype
8839 } );
8840 if ( Array.isArray( config.items ) ) {
8841 this.addItems( config.items );
8842 }
8843 };
8844
8845 /* Setup */
8846
8847 OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout );
8848 OO.mixinClass( OO.ui.FormLayout, OO.ui.mixin.GroupElement );
8849
8850 /* Events */
8851
8852 /**
8853 * A 'submit' event is emitted when the form is submitted.
8854 *
8855 * @event submit
8856 */
8857
8858 /* Static Properties */
8859
8860 OO.ui.FormLayout.static.tagName = 'form';
8861
8862 /* Methods */
8863
8864 /**
8865 * Handle form submit events.
8866 *
8867 * @private
8868 * @param {jQuery.Event} e Submit event
8869 * @fires submit
8870 */
8871 OO.ui.FormLayout.prototype.onFormSubmit = function () {
8872 if ( this.emit( 'submit' ) ) {
8873 return false;
8874 }
8875 };
8876
8877 /**
8878 * 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)
8879 * and its size is customized with the #menuSize config. The content area will fill all remaining space.
8880 *
8881 * @example
8882 * var menuLayout = new OO.ui.MenuLayout( {
8883 * position: 'top'
8884 * } ),
8885 * menuPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
8886 * contentPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
8887 * select = new OO.ui.SelectWidget( {
8888 * items: [
8889 * new OO.ui.OptionWidget( {
8890 * data: 'before',
8891 * label: 'Before',
8892 * } ),
8893 * new OO.ui.OptionWidget( {
8894 * data: 'after',
8895 * label: 'After',
8896 * } ),
8897 * new OO.ui.OptionWidget( {
8898 * data: 'top',
8899 * label: 'Top',
8900 * } ),
8901 * new OO.ui.OptionWidget( {
8902 * data: 'bottom',
8903 * label: 'Bottom',
8904 * } )
8905 * ]
8906 * } ).on( 'select', function ( item ) {
8907 * menuLayout.setMenuPosition( item.getData() );
8908 * } );
8909 *
8910 * menuLayout.$menu.append(
8911 * menuPanel.$element.append( '<b>Menu panel</b>', select.$element )
8912 * );
8913 * menuLayout.$content.append(
8914 * contentPanel.$element.append( '<b>Content panel</b>', '<p>Note that the menu is positioned relative to the content panel: top, bottom, after, before.</p>')
8915 * );
8916 * $( 'body' ).append( menuLayout.$element );
8917 *
8918 * If menu size needs to be overridden, it can be accomplished using CSS similar to the snippet
8919 * below. MenuLayout's CSS will override the appropriate values with 'auto' or '0' to display the
8920 * menu correctly. If `menuPosition` is known beforehand, CSS rules corresponding to other positions
8921 * may be omitted.
8922 *
8923 * .oo-ui-menuLayout-menu {
8924 * height: 200px;
8925 * width: 200px;
8926 * }
8927 * .oo-ui-menuLayout-content {
8928 * top: 200px;
8929 * left: 200px;
8930 * right: 200px;
8931 * bottom: 200px;
8932 * }
8933 *
8934 * @class
8935 * @extends OO.ui.Layout
8936 *
8937 * @constructor
8938 * @param {Object} [config] Configuration options
8939 * @cfg {boolean} [showMenu=true] Show menu
8940 * @cfg {string} [menuPosition='before'] Position of menu: `top`, `after`, `bottom` or `before`
8941 */
8942 OO.ui.MenuLayout = function OoUiMenuLayout( config ) {
8943 // Configuration initialization
8944 config = $.extend( {
8945 showMenu: true,
8946 menuPosition: 'before'
8947 }, config );
8948
8949 // Parent constructor
8950 OO.ui.MenuLayout.parent.call( this, config );
8951
8952 /**
8953 * Menu DOM node
8954 *
8955 * @property {jQuery}
8956 */
8957 this.$menu = $( '<div>' );
8958 /**
8959 * Content DOM node
8960 *
8961 * @property {jQuery}
8962 */
8963 this.$content = $( '<div>' );
8964
8965 // Initialization
8966 this.$menu
8967 .addClass( 'oo-ui-menuLayout-menu' );
8968 this.$content.addClass( 'oo-ui-menuLayout-content' );
8969 this.$element
8970 .addClass( 'oo-ui-menuLayout' )
8971 .append( this.$content, this.$menu );
8972 this.setMenuPosition( config.menuPosition );
8973 this.toggleMenu( config.showMenu );
8974 };
8975
8976 /* Setup */
8977
8978 OO.inheritClass( OO.ui.MenuLayout, OO.ui.Layout );
8979
8980 /* Methods */
8981
8982 /**
8983 * Toggle menu.
8984 *
8985 * @param {boolean} showMenu Show menu, omit to toggle
8986 * @chainable
8987 */
8988 OO.ui.MenuLayout.prototype.toggleMenu = function ( showMenu ) {
8989 showMenu = showMenu === undefined ? !this.showMenu : !!showMenu;
8990
8991 if ( this.showMenu !== showMenu ) {
8992 this.showMenu = showMenu;
8993 this.$element
8994 .toggleClass( 'oo-ui-menuLayout-showMenu', this.showMenu )
8995 .toggleClass( 'oo-ui-menuLayout-hideMenu', !this.showMenu );
8996 }
8997
8998 return this;
8999 };
9000
9001 /**
9002 * Check if menu is visible
9003 *
9004 * @return {boolean} Menu is visible
9005 */
9006 OO.ui.MenuLayout.prototype.isMenuVisible = function () {
9007 return this.showMenu;
9008 };
9009
9010 /**
9011 * Set menu position.
9012 *
9013 * @param {string} position Position of menu, either `top`, `after`, `bottom` or `before`
9014 * @throws {Error} If position value is not supported
9015 * @chainable
9016 */
9017 OO.ui.MenuLayout.prototype.setMenuPosition = function ( position ) {
9018 this.$element.removeClass( 'oo-ui-menuLayout-' + this.menuPosition );
9019 this.menuPosition = position;
9020 this.$element.addClass( 'oo-ui-menuLayout-' + position );
9021
9022 return this;
9023 };
9024
9025 /**
9026 * Get menu position.
9027 *
9028 * @return {string} Menu position
9029 */
9030 OO.ui.MenuLayout.prototype.getMenuPosition = function () {
9031 return this.menuPosition;
9032 };
9033
9034 /**
9035 * BookletLayouts contain {@link OO.ui.PageLayout page layouts} as well as
9036 * an {@link OO.ui.OutlineSelectWidget outline} that allows users to easily navigate
9037 * through the pages and select which one to display. By default, only one page is
9038 * displayed at a time and the outline is hidden. When a user navigates to a new page,
9039 * the booklet layout automatically focuses on the first focusable element, unless the
9040 * default setting is changed. Optionally, booklets can be configured to show
9041 * {@link OO.ui.OutlineControlsWidget controls} for adding, moving, and removing items.
9042 *
9043 * @example
9044 * // Example of a BookletLayout that contains two PageLayouts.
9045 *
9046 * function PageOneLayout( name, config ) {
9047 * PageOneLayout.parent.call( this, name, config );
9048 * this.$element.append( '<p>First page</p><p>(This booklet has an outline, displayed on the left)</p>' );
9049 * }
9050 * OO.inheritClass( PageOneLayout, OO.ui.PageLayout );
9051 * PageOneLayout.prototype.setupOutlineItem = function () {
9052 * this.outlineItem.setLabel( 'Page One' );
9053 * };
9054 *
9055 * function PageTwoLayout( name, config ) {
9056 * PageTwoLayout.parent.call( this, name, config );
9057 * this.$element.append( '<p>Second page</p>' );
9058 * }
9059 * OO.inheritClass( PageTwoLayout, OO.ui.PageLayout );
9060 * PageTwoLayout.prototype.setupOutlineItem = function () {
9061 * this.outlineItem.setLabel( 'Page Two' );
9062 * };
9063 *
9064 * var page1 = new PageOneLayout( 'one' ),
9065 * page2 = new PageTwoLayout( 'two' );
9066 *
9067 * var booklet = new OO.ui.BookletLayout( {
9068 * outlined: true
9069 * } );
9070 *
9071 * booklet.addPages ( [ page1, page2 ] );
9072 * $( 'body' ).append( booklet.$element );
9073 *
9074 * @class
9075 * @extends OO.ui.MenuLayout
9076 *
9077 * @constructor
9078 * @param {Object} [config] Configuration options
9079 * @cfg {boolean} [continuous=false] Show all pages, one after another
9080 * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new page is displayed.
9081 * @cfg {boolean} [outlined=false] Show the outline. The outline is used to navigate through the pages of the booklet.
9082 * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
9083 */
9084 OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
9085 // Configuration initialization
9086 config = config || {};
9087
9088 // Parent constructor
9089 OO.ui.BookletLayout.parent.call( this, config );
9090
9091 // Properties
9092 this.currentPageName = null;
9093 this.pages = {};
9094 this.ignoreFocus = false;
9095 this.stackLayout = new OO.ui.StackLayout( { continuous: !!config.continuous } );
9096 this.$content.append( this.stackLayout.$element );
9097 this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
9098 this.outlineVisible = false;
9099 this.outlined = !!config.outlined;
9100 if ( this.outlined ) {
9101 this.editable = !!config.editable;
9102 this.outlineControlsWidget = null;
9103 this.outlineSelectWidget = new OO.ui.OutlineSelectWidget();
9104 this.outlinePanel = new OO.ui.PanelLayout( { scrollable: true } );
9105 this.$menu.append( this.outlinePanel.$element );
9106 this.outlineVisible = true;
9107 if ( this.editable ) {
9108 this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
9109 this.outlineSelectWidget
9110 );
9111 }
9112 }
9113 this.toggleMenu( this.outlined );
9114
9115 // Events
9116 this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
9117 if ( this.outlined ) {
9118 this.outlineSelectWidget.connect( this, { select: 'onOutlineSelectWidgetSelect' } );
9119 }
9120 if ( this.autoFocus ) {
9121 // Event 'focus' does not bubble, but 'focusin' does
9122 this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
9123 }
9124
9125 // Initialization
9126 this.$element.addClass( 'oo-ui-bookletLayout' );
9127 this.stackLayout.$element.addClass( 'oo-ui-bookletLayout-stackLayout' );
9128 if ( this.outlined ) {
9129 this.outlinePanel.$element
9130 .addClass( 'oo-ui-bookletLayout-outlinePanel' )
9131 .append( this.outlineSelectWidget.$element );
9132 if ( this.editable ) {
9133 this.outlinePanel.$element
9134 .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
9135 .append( this.outlineControlsWidget.$element );
9136 }
9137 }
9138 };
9139
9140 /* Setup */
9141
9142 OO.inheritClass( OO.ui.BookletLayout, OO.ui.MenuLayout );
9143
9144 /* Events */
9145
9146 /**
9147 * A 'set' event is emitted when a page is {@link #setPage set} to be displayed by the booklet layout.
9148 * @event set
9149 * @param {OO.ui.PageLayout} page Current page
9150 */
9151
9152 /**
9153 * An 'add' event is emitted when pages are {@link #addPages added} to the booklet layout.
9154 *
9155 * @event add
9156 * @param {OO.ui.PageLayout[]} page Added pages
9157 * @param {number} index Index pages were added at
9158 */
9159
9160 /**
9161 * A 'remove' event is emitted when pages are {@link #clearPages cleared} or
9162 * {@link #removePages removed} from the booklet.
9163 *
9164 * @event remove
9165 * @param {OO.ui.PageLayout[]} pages Removed pages
9166 */
9167
9168 /* Methods */
9169
9170 /**
9171 * Handle stack layout focus.
9172 *
9173 * @private
9174 * @param {jQuery.Event} e Focusin event
9175 */
9176 OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) {
9177 var name, $target;
9178
9179 // Find the page that an element was focused within
9180 $target = $( e.target ).closest( '.oo-ui-pageLayout' );
9181 for ( name in this.pages ) {
9182 // Check for page match, exclude current page to find only page changes
9183 if ( this.pages[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentPageName ) {
9184 this.setPage( name );
9185 break;
9186 }
9187 }
9188 };
9189
9190 /**
9191 * Handle stack layout set events.
9192 *
9193 * @private
9194 * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
9195 */
9196 OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
9197 var layout = this;
9198 if ( page ) {
9199 page.scrollElementIntoView( { complete: function () {
9200 if ( layout.autoFocus ) {
9201 layout.focus();
9202 }
9203 } } );
9204 }
9205 };
9206
9207 /**
9208 * Focus the first input in the current page.
9209 *
9210 * If no page is selected, the first selectable page will be selected.
9211 * If the focus is already in an element on the current page, nothing will happen.
9212 * @param {number} [itemIndex] A specific item to focus on
9213 */
9214 OO.ui.BookletLayout.prototype.focus = function ( itemIndex ) {
9215 var $input, page,
9216 items = this.stackLayout.getItems();
9217
9218 if ( itemIndex !== undefined && items[ itemIndex ] ) {
9219 page = items[ itemIndex ];
9220 } else {
9221 page = this.stackLayout.getCurrentItem();
9222 }
9223
9224 if ( !page && this.outlined ) {
9225 this.selectFirstSelectablePage();
9226 page = this.stackLayout.getCurrentItem();
9227 }
9228 if ( !page ) {
9229 return;
9230 }
9231 // Only change the focus if is not already in the current page
9232 if ( !page.$element.find( ':focus' ).length ) {
9233 $input = page.$element.find( ':input:first' );
9234 if ( $input.length ) {
9235 $input[ 0 ].focus();
9236 }
9237 }
9238 };
9239
9240 /**
9241 * Find the first focusable input in the booklet layout and focus
9242 * on it.
9243 */
9244 OO.ui.BookletLayout.prototype.focusFirstFocusable = function () {
9245 var i, len,
9246 found = false,
9247 items = this.stackLayout.getItems(),
9248 checkAndFocus = function () {
9249 if ( OO.ui.isFocusableElement( $( this ) ) ) {
9250 $( this ).focus();
9251 found = true;
9252 return false;
9253 }
9254 };
9255
9256 for ( i = 0, len = items.length; i < len; i++ ) {
9257 if ( found ) {
9258 break;
9259 }
9260 // Find all potentially focusable elements in the item
9261 // and check if they are focusable
9262 items[i].$element
9263 .find( 'input, select, textarea, button, object' )
9264 /* jshint loopfunc:true */
9265 .each( checkAndFocus );
9266 }
9267 };
9268
9269 /**
9270 * Handle outline widget select events.
9271 *
9272 * @private
9273 * @param {OO.ui.OptionWidget|null} item Selected item
9274 */
9275 OO.ui.BookletLayout.prototype.onOutlineSelectWidgetSelect = function ( item ) {
9276 if ( item ) {
9277 this.setPage( item.getData() );
9278 }
9279 };
9280
9281 /**
9282 * Check if booklet has an outline.
9283 *
9284 * @return {boolean} Booklet has an outline
9285 */
9286 OO.ui.BookletLayout.prototype.isOutlined = function () {
9287 return this.outlined;
9288 };
9289
9290 /**
9291 * Check if booklet has editing controls.
9292 *
9293 * @return {boolean} Booklet is editable
9294 */
9295 OO.ui.BookletLayout.prototype.isEditable = function () {
9296 return this.editable;
9297 };
9298
9299 /**
9300 * Check if booklet has a visible outline.
9301 *
9302 * @return {boolean} Outline is visible
9303 */
9304 OO.ui.BookletLayout.prototype.isOutlineVisible = function () {
9305 return this.outlined && this.outlineVisible;
9306 };
9307
9308 /**
9309 * Hide or show the outline.
9310 *
9311 * @param {boolean} [show] Show outline, omit to invert current state
9312 * @chainable
9313 */
9314 OO.ui.BookletLayout.prototype.toggleOutline = function ( show ) {
9315 if ( this.outlined ) {
9316 show = show === undefined ? !this.outlineVisible : !!show;
9317 this.outlineVisible = show;
9318 this.toggleMenu( show );
9319 }
9320
9321 return this;
9322 };
9323
9324 /**
9325 * Get the page closest to the specified page.
9326 *
9327 * @param {OO.ui.PageLayout} page Page to use as a reference point
9328 * @return {OO.ui.PageLayout|null} Page closest to the specified page
9329 */
9330 OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) {
9331 var next, prev, level,
9332 pages = this.stackLayout.getItems(),
9333 index = $.inArray( page, pages );
9334
9335 if ( index !== -1 ) {
9336 next = pages[ index + 1 ];
9337 prev = pages[ index - 1 ];
9338 // Prefer adjacent pages at the same level
9339 if ( this.outlined ) {
9340 level = this.outlineSelectWidget.getItemFromData( page.getName() ).getLevel();
9341 if (
9342 prev &&
9343 level === this.outlineSelectWidget.getItemFromData( prev.getName() ).getLevel()
9344 ) {
9345 return prev;
9346 }
9347 if (
9348 next &&
9349 level === this.outlineSelectWidget.getItemFromData( next.getName() ).getLevel()
9350 ) {
9351 return next;
9352 }
9353 }
9354 }
9355 return prev || next || null;
9356 };
9357
9358 /**
9359 * Get the outline widget.
9360 *
9361 * If the booklet is not outlined, the method will return `null`.
9362 *
9363 * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if the booklet is not outlined
9364 */
9365 OO.ui.BookletLayout.prototype.getOutline = function () {
9366 return this.outlineSelectWidget;
9367 };
9368
9369 /**
9370 * Get the outline controls widget.
9371 *
9372 * If the outline is not editable, the method will return `null`.
9373 *
9374 * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
9375 */
9376 OO.ui.BookletLayout.prototype.getOutlineControls = function () {
9377 return this.outlineControlsWidget;
9378 };
9379
9380 /**
9381 * Get a page by its symbolic name.
9382 *
9383 * @param {string} name Symbolic name of page
9384 * @return {OO.ui.PageLayout|undefined} Page, if found
9385 */
9386 OO.ui.BookletLayout.prototype.getPage = function ( name ) {
9387 return this.pages[ name ];
9388 };
9389
9390 /**
9391 * Get the current page.
9392 *
9393 * @return {OO.ui.PageLayout|undefined} Current page, if found
9394 */
9395 OO.ui.BookletLayout.prototype.getCurrentPage = function () {
9396 var name = this.getCurrentPageName();
9397 return name ? this.getPage( name ) : undefined;
9398 };
9399
9400 /**
9401 * Get the symbolic name of the current page.
9402 *
9403 * @return {string|null} Symbolic name of the current page
9404 */
9405 OO.ui.BookletLayout.prototype.getCurrentPageName = function () {
9406 return this.currentPageName;
9407 };
9408
9409 /**
9410 * Add pages to the booklet layout
9411 *
9412 * When pages are added with the same names as existing pages, the existing pages will be
9413 * automatically removed before the new pages are added.
9414 *
9415 * @param {OO.ui.PageLayout[]} pages Pages to add
9416 * @param {number} index Index of the insertion point
9417 * @fires add
9418 * @chainable
9419 */
9420 OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) {
9421 var i, len, name, page, item, currentIndex,
9422 stackLayoutPages = this.stackLayout.getItems(),
9423 remove = [],
9424 items = [];
9425
9426 // Remove pages with same names
9427 for ( i = 0, len = pages.length; i < len; i++ ) {
9428 page = pages[ i ];
9429 name = page.getName();
9430
9431 if ( Object.prototype.hasOwnProperty.call( this.pages, name ) ) {
9432 // Correct the insertion index
9433 currentIndex = $.inArray( this.pages[ name ], stackLayoutPages );
9434 if ( currentIndex !== -1 && currentIndex + 1 < index ) {
9435 index--;
9436 }
9437 remove.push( this.pages[ name ] );
9438 }
9439 }
9440 if ( remove.length ) {
9441 this.removePages( remove );
9442 }
9443
9444 // Add new pages
9445 for ( i = 0, len = pages.length; i < len; i++ ) {
9446 page = pages[ i ];
9447 name = page.getName();
9448 this.pages[ page.getName() ] = page;
9449 if ( this.outlined ) {
9450 item = new OO.ui.OutlineOptionWidget( { data: name } );
9451 page.setOutlineItem( item );
9452 items.push( item );
9453 }
9454 }
9455
9456 if ( this.outlined && items.length ) {
9457 this.outlineSelectWidget.addItems( items, index );
9458 this.selectFirstSelectablePage();
9459 }
9460 this.stackLayout.addItems( pages, index );
9461 this.emit( 'add', pages, index );
9462
9463 return this;
9464 };
9465
9466 /**
9467 * Remove the specified pages from the booklet layout.
9468 *
9469 * To remove all pages from the booklet, you may wish to use the #clearPages method instead.
9470 *
9471 * @param {OO.ui.PageLayout[]} pages An array of pages to remove
9472 * @fires remove
9473 * @chainable
9474 */
9475 OO.ui.BookletLayout.prototype.removePages = function ( pages ) {
9476 var i, len, name, page,
9477 items = [];
9478
9479 for ( i = 0, len = pages.length; i < len; i++ ) {
9480 page = pages[ i ];
9481 name = page.getName();
9482 delete this.pages[ name ];
9483 if ( this.outlined ) {
9484 items.push( this.outlineSelectWidget.getItemFromData( name ) );
9485 page.setOutlineItem( null );
9486 }
9487 }
9488 if ( this.outlined && items.length ) {
9489 this.outlineSelectWidget.removeItems( items );
9490 this.selectFirstSelectablePage();
9491 }
9492 this.stackLayout.removeItems( pages );
9493 this.emit( 'remove', pages );
9494
9495 return this;
9496 };
9497
9498 /**
9499 * Clear all pages from the booklet layout.
9500 *
9501 * To remove only a subset of pages from the booklet, use the #removePages method.
9502 *
9503 * @fires remove
9504 * @chainable
9505 */
9506 OO.ui.BookletLayout.prototype.clearPages = function () {
9507 var i, len,
9508 pages = this.stackLayout.getItems();
9509
9510 this.pages = {};
9511 this.currentPageName = null;
9512 if ( this.outlined ) {
9513 this.outlineSelectWidget.clearItems();
9514 for ( i = 0, len = pages.length; i < len; i++ ) {
9515 pages[ i ].setOutlineItem( null );
9516 }
9517 }
9518 this.stackLayout.clearItems();
9519
9520 this.emit( 'remove', pages );
9521
9522 return this;
9523 };
9524
9525 /**
9526 * Set the current page by symbolic name.
9527 *
9528 * @fires set
9529 * @param {string} name Symbolic name of page
9530 */
9531 OO.ui.BookletLayout.prototype.setPage = function ( name ) {
9532 var selectedItem,
9533 $focused,
9534 page = this.pages[ name ];
9535
9536 if ( name !== this.currentPageName ) {
9537 if ( this.outlined ) {
9538 selectedItem = this.outlineSelectWidget.getSelectedItem();
9539 if ( selectedItem && selectedItem.getData() !== name ) {
9540 this.outlineSelectWidget.selectItemByData( name );
9541 }
9542 }
9543 if ( page ) {
9544 if ( this.currentPageName && this.pages[ this.currentPageName ] ) {
9545 this.pages[ this.currentPageName ].setActive( false );
9546 // Blur anything focused if the next page doesn't have anything focusable - this
9547 // is not needed if the next page has something focusable because once it is focused
9548 // this blur happens automatically
9549 if ( this.autoFocus && !page.$element.find( ':input' ).length ) {
9550 $focused = this.pages[ this.currentPageName ].$element.find( ':focus' );
9551 if ( $focused.length ) {
9552 $focused[ 0 ].blur();
9553 }
9554 }
9555 }
9556 this.currentPageName = name;
9557 this.stackLayout.setItem( page );
9558 page.setActive( true );
9559 this.emit( 'set', page );
9560 }
9561 }
9562 };
9563
9564 /**
9565 * Select the first selectable page.
9566 *
9567 * @chainable
9568 */
9569 OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () {
9570 if ( !this.outlineSelectWidget.getSelectedItem() ) {
9571 this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getFirstSelectableItem() );
9572 }
9573
9574 return this;
9575 };
9576
9577 /**
9578 * IndexLayouts contain {@link OO.ui.CardLayout card layouts} as well as
9579 * {@link OO.ui.TabSelectWidget tabs} that allow users to easily navigate through the cards and
9580 * select which one to display. By default, only one card is displayed at a time. When a user
9581 * navigates to a new card, the index layout automatically focuses on the first focusable element,
9582 * unless the default setting is changed.
9583 *
9584 * TODO: This class is similar to BookletLayout, we may want to refactor to reduce duplication
9585 *
9586 * @example
9587 * // Example of a IndexLayout that contains two CardLayouts.
9588 *
9589 * function CardOneLayout( name, config ) {
9590 * CardOneLayout.parent.call( this, name, config );
9591 * this.$element.append( '<p>First card</p>' );
9592 * }
9593 * OO.inheritClass( CardOneLayout, OO.ui.CardLayout );
9594 * CardOneLayout.prototype.setupTabItem = function () {
9595 * this.tabItem.setLabel( 'Card One' );
9596 * };
9597 *
9598 * function CardTwoLayout( name, config ) {
9599 * CardTwoLayout.parent.call( this, name, config );
9600 * this.$element.append( '<p>Second card</p>' );
9601 * }
9602 * OO.inheritClass( CardTwoLayout, OO.ui.CardLayout );
9603 * CardTwoLayout.prototype.setupTabItem = function () {
9604 * this.tabItem.setLabel( 'Card Two' );
9605 * };
9606 *
9607 * var card1 = new CardOneLayout( 'one' ),
9608 * card2 = new CardTwoLayout( 'two' );
9609 *
9610 * var index = new OO.ui.IndexLayout();
9611 *
9612 * index.addCards ( [ card1, card2 ] );
9613 * $( 'body' ).append( index.$element );
9614 *
9615 * @class
9616 * @extends OO.ui.MenuLayout
9617 *
9618 * @constructor
9619 * @param {Object} [config] Configuration options
9620 * @cfg {boolean} [continuous=false] Show all cards, one after another
9621 * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new card is displayed.
9622 */
9623 OO.ui.IndexLayout = function OoUiIndexLayout( config ) {
9624 // Configuration initialization
9625 config = $.extend( {}, config, { menuPosition: 'top' } );
9626
9627 // Parent constructor
9628 OO.ui.IndexLayout.parent.call( this, config );
9629
9630 // Properties
9631 this.currentCardName = null;
9632 this.cards = {};
9633 this.ignoreFocus = false;
9634 this.stackLayout = new OO.ui.StackLayout( { continuous: !!config.continuous } );
9635 this.$content.append( this.stackLayout.$element );
9636 this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
9637
9638 this.tabSelectWidget = new OO.ui.TabSelectWidget();
9639 this.tabPanel = new OO.ui.PanelLayout();
9640 this.$menu.append( this.tabPanel.$element );
9641
9642 this.toggleMenu( true );
9643
9644 // Events
9645 this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
9646 this.tabSelectWidget.connect( this, { select: 'onTabSelectWidgetSelect' } );
9647 if ( this.autoFocus ) {
9648 // Event 'focus' does not bubble, but 'focusin' does
9649 this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
9650 }
9651
9652 // Initialization
9653 this.$element.addClass( 'oo-ui-indexLayout' );
9654 this.stackLayout.$element.addClass( 'oo-ui-indexLayout-stackLayout' );
9655 this.tabPanel.$element
9656 .addClass( 'oo-ui-indexLayout-tabPanel' )
9657 .append( this.tabSelectWidget.$element );
9658 };
9659
9660 /* Setup */
9661
9662 OO.inheritClass( OO.ui.IndexLayout, OO.ui.MenuLayout );
9663
9664 /* Events */
9665
9666 /**
9667 * A 'set' event is emitted when a card is {@link #setCard set} to be displayed by the index layout.
9668 * @event set
9669 * @param {OO.ui.CardLayout} card Current card
9670 */
9671
9672 /**
9673 * An 'add' event is emitted when cards are {@link #addCards added} to the index layout.
9674 *
9675 * @event add
9676 * @param {OO.ui.CardLayout[]} card Added cards
9677 * @param {number} index Index cards were added at
9678 */
9679
9680 /**
9681 * A 'remove' event is emitted when cards are {@link #clearCards cleared} or
9682 * {@link #removeCards removed} from the index.
9683 *
9684 * @event remove
9685 * @param {OO.ui.CardLayout[]} cards Removed cards
9686 */
9687
9688 /* Methods */
9689
9690 /**
9691 * Handle stack layout focus.
9692 *
9693 * @private
9694 * @param {jQuery.Event} e Focusin event
9695 */
9696 OO.ui.IndexLayout.prototype.onStackLayoutFocus = function ( e ) {
9697 var name, $target;
9698
9699 // Find the card that an element was focused within
9700 $target = $( e.target ).closest( '.oo-ui-cardLayout' );
9701 for ( name in this.cards ) {
9702 // Check for card match, exclude current card to find only card changes
9703 if ( this.cards[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentCardName ) {
9704 this.setCard( name );
9705 break;
9706 }
9707 }
9708 };
9709
9710 /**
9711 * Handle stack layout set events.
9712 *
9713 * @private
9714 * @param {OO.ui.PanelLayout|null} card The card panel that is now the current panel
9715 */
9716 OO.ui.IndexLayout.prototype.onStackLayoutSet = function ( card ) {
9717 var layout = this;
9718 if ( card ) {
9719 card.scrollElementIntoView( { complete: function () {
9720 if ( layout.autoFocus ) {
9721 layout.focus();
9722 }
9723 } } );
9724 }
9725 };
9726
9727 /**
9728 * Focus the first input in the current card.
9729 *
9730 * If no card is selected, the first selectable card will be selected.
9731 * If the focus is already in an element on the current card, nothing will happen.
9732 * @param {number} [itemIndex] A specific item to focus on
9733 */
9734 OO.ui.IndexLayout.prototype.focus = function ( itemIndex ) {
9735 var $input, card,
9736 items = this.stackLayout.getItems();
9737
9738 if ( itemIndex !== undefined && items[ itemIndex ] ) {
9739 card = items[ itemIndex ];
9740 } else {
9741 card = this.stackLayout.getCurrentItem();
9742 }
9743
9744 if ( !card ) {
9745 this.selectFirstSelectableCard();
9746 card = this.stackLayout.getCurrentItem();
9747 }
9748 if ( !card ) {
9749 return;
9750 }
9751 // Only change the focus if is not already in the current card
9752 if ( !card.$element.find( ':focus' ).length ) {
9753 $input = card.$element.find( ':input:first' );
9754 if ( $input.length ) {
9755 $input[ 0 ].focus();
9756 }
9757 }
9758 };
9759
9760 /**
9761 * Find the first focusable input in the index layout and focus
9762 * on it.
9763 */
9764 OO.ui.IndexLayout.prototype.focusFirstFocusable = function () {
9765 var i, len,
9766 found = false,
9767 items = this.stackLayout.getItems(),
9768 checkAndFocus = function () {
9769 if ( OO.ui.isFocusableElement( $( this ) ) ) {
9770 $( this ).focus();
9771 found = true;
9772 return false;
9773 }
9774 };
9775
9776 for ( i = 0, len = items.length; i < len; i++ ) {
9777 if ( found ) {
9778 break;
9779 }
9780 // Find all potentially focusable elements in the item
9781 // and check if they are focusable
9782 items[i].$element
9783 .find( 'input, select, textarea, button, object' )
9784 .each( checkAndFocus );
9785 }
9786 };
9787
9788 /**
9789 * Handle tab widget select events.
9790 *
9791 * @private
9792 * @param {OO.ui.OptionWidget|null} item Selected item
9793 */
9794 OO.ui.IndexLayout.prototype.onTabSelectWidgetSelect = function ( item ) {
9795 if ( item ) {
9796 this.setCard( item.getData() );
9797 }
9798 };
9799
9800 /**
9801 * Get the card closest to the specified card.
9802 *
9803 * @param {OO.ui.CardLayout} card Card to use as a reference point
9804 * @return {OO.ui.CardLayout|null} Card closest to the specified card
9805 */
9806 OO.ui.IndexLayout.prototype.getClosestCard = function ( card ) {
9807 var next, prev, level,
9808 cards = this.stackLayout.getItems(),
9809 index = $.inArray( card, cards );
9810
9811 if ( index !== -1 ) {
9812 next = cards[ index + 1 ];
9813 prev = cards[ index - 1 ];
9814 // Prefer adjacent cards at the same level
9815 level = this.tabSelectWidget.getItemFromData( card.getName() ).getLevel();
9816 if (
9817 prev &&
9818 level === this.tabSelectWidget.getItemFromData( prev.getName() ).getLevel()
9819 ) {
9820 return prev;
9821 }
9822 if (
9823 next &&
9824 level === this.tabSelectWidget.getItemFromData( next.getName() ).getLevel()
9825 ) {
9826 return next;
9827 }
9828 }
9829 return prev || next || null;
9830 };
9831
9832 /**
9833 * Get the tabs widget.
9834 *
9835 * @return {OO.ui.TabSelectWidget} Tabs widget
9836 */
9837 OO.ui.IndexLayout.prototype.getTabs = function () {
9838 return this.tabSelectWidget;
9839 };
9840
9841 /**
9842 * Get a card by its symbolic name.
9843 *
9844 * @param {string} name Symbolic name of card
9845 * @return {OO.ui.CardLayout|undefined} Card, if found
9846 */
9847 OO.ui.IndexLayout.prototype.getCard = function ( name ) {
9848 return this.cards[ name ];
9849 };
9850
9851 /**
9852 * Get the current card.
9853 *
9854 * @return {OO.ui.CardLayout|undefined} Current card, if found
9855 */
9856 OO.ui.IndexLayout.prototype.getCurrentCard = function () {
9857 var name = this.getCurrentCardName();
9858 return name ? this.getCard( name ) : undefined;
9859 };
9860
9861 /**
9862 * Get the symbolic name of the current card.
9863 *
9864 * @return {string|null} Symbolic name of the current card
9865 */
9866 OO.ui.IndexLayout.prototype.getCurrentCardName = function () {
9867 return this.currentCardName;
9868 };
9869
9870 /**
9871 * Add cards to the index layout
9872 *
9873 * When cards are added with the same names as existing cards, the existing cards will be
9874 * automatically removed before the new cards are added.
9875 *
9876 * @param {OO.ui.CardLayout[]} cards Cards to add
9877 * @param {number} index Index of the insertion point
9878 * @fires add
9879 * @chainable
9880 */
9881 OO.ui.IndexLayout.prototype.addCards = function ( cards, index ) {
9882 var i, len, name, card, item, currentIndex,
9883 stackLayoutCards = this.stackLayout.getItems(),
9884 remove = [],
9885 items = [];
9886
9887 // Remove cards with same names
9888 for ( i = 0, len = cards.length; i < len; i++ ) {
9889 card = cards[ i ];
9890 name = card.getName();
9891
9892 if ( Object.prototype.hasOwnProperty.call( this.cards, name ) ) {
9893 // Correct the insertion index
9894 currentIndex = $.inArray( this.cards[ name ], stackLayoutCards );
9895 if ( currentIndex !== -1 && currentIndex + 1 < index ) {
9896 index--;
9897 }
9898 remove.push( this.cards[ name ] );
9899 }
9900 }
9901 if ( remove.length ) {
9902 this.removeCards( remove );
9903 }
9904
9905 // Add new cards
9906 for ( i = 0, len = cards.length; i < len; i++ ) {
9907 card = cards[ i ];
9908 name = card.getName();
9909 this.cards[ card.getName() ] = card;
9910 item = new OO.ui.TabOptionWidget( { data: name } );
9911 card.setTabItem( item );
9912 items.push( item );
9913 }
9914
9915 if ( items.length ) {
9916 this.tabSelectWidget.addItems( items, index );
9917 this.selectFirstSelectableCard();
9918 }
9919 this.stackLayout.addItems( cards, index );
9920 this.emit( 'add', cards, index );
9921
9922 return this;
9923 };
9924
9925 /**
9926 * Remove the specified cards from the index layout.
9927 *
9928 * To remove all cards from the index, you may wish to use the #clearCards method instead.
9929 *
9930 * @param {OO.ui.CardLayout[]} cards An array of cards to remove
9931 * @fires remove
9932 * @chainable
9933 */
9934 OO.ui.IndexLayout.prototype.removeCards = function ( cards ) {
9935 var i, len, name, card,
9936 items = [];
9937
9938 for ( i = 0, len = cards.length; i < len; i++ ) {
9939 card = cards[ i ];
9940 name = card.getName();
9941 delete this.cards[ name ];
9942 items.push( this.tabSelectWidget.getItemFromData( name ) );
9943 card.setTabItem( null );
9944 }
9945 if ( items.length ) {
9946 this.tabSelectWidget.removeItems( items );
9947 this.selectFirstSelectableCard();
9948 }
9949 this.stackLayout.removeItems( cards );
9950 this.emit( 'remove', cards );
9951
9952 return this;
9953 };
9954
9955 /**
9956 * Clear all cards from the index layout.
9957 *
9958 * To remove only a subset of cards from the index, use the #removeCards method.
9959 *
9960 * @fires remove
9961 * @chainable
9962 */
9963 OO.ui.IndexLayout.prototype.clearCards = function () {
9964 var i, len,
9965 cards = this.stackLayout.getItems();
9966
9967 this.cards = {};
9968 this.currentCardName = null;
9969 this.tabSelectWidget.clearItems();
9970 for ( i = 0, len = cards.length; i < len; i++ ) {
9971 cards[ i ].setTabItem( null );
9972 }
9973 this.stackLayout.clearItems();
9974
9975 this.emit( 'remove', cards );
9976
9977 return this;
9978 };
9979
9980 /**
9981 * Set the current card by symbolic name.
9982 *
9983 * @fires set
9984 * @param {string} name Symbolic name of card
9985 */
9986 OO.ui.IndexLayout.prototype.setCard = function ( name ) {
9987 var selectedItem,
9988 $focused,
9989 card = this.cards[ name ];
9990
9991 if ( name !== this.currentCardName ) {
9992 selectedItem = this.tabSelectWidget.getSelectedItem();
9993 if ( selectedItem && selectedItem.getData() !== name ) {
9994 this.tabSelectWidget.selectItemByData( name );
9995 }
9996 if ( card ) {
9997 if ( this.currentCardName && this.cards[ this.currentCardName ] ) {
9998 this.cards[ this.currentCardName ].setActive( false );
9999 // Blur anything focused if the next card doesn't have anything focusable - this
10000 // is not needed if the next card has something focusable because once it is focused
10001 // this blur happens automatically
10002 if ( this.autoFocus && !card.$element.find( ':input' ).length ) {
10003 $focused = this.cards[ this.currentCardName ].$element.find( ':focus' );
10004 if ( $focused.length ) {
10005 $focused[ 0 ].blur();
10006 }
10007 }
10008 }
10009 this.currentCardName = name;
10010 this.stackLayout.setItem( card );
10011 card.setActive( true );
10012 this.emit( 'set', card );
10013 }
10014 }
10015 };
10016
10017 /**
10018 * Select the first selectable card.
10019 *
10020 * @chainable
10021 */
10022 OO.ui.IndexLayout.prototype.selectFirstSelectableCard = function () {
10023 if ( !this.tabSelectWidget.getSelectedItem() ) {
10024 this.tabSelectWidget.selectItem( this.tabSelectWidget.getFirstSelectableItem() );
10025 }
10026
10027 return this;
10028 };
10029
10030 /**
10031 * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
10032 * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
10033 *
10034 * @example
10035 * // Example of a panel layout
10036 * var panel = new OO.ui.PanelLayout( {
10037 * expanded: false,
10038 * framed: true,
10039 * padded: true,
10040 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
10041 * } );
10042 * $( 'body' ).append( panel.$element );
10043 *
10044 * @class
10045 * @extends OO.ui.Layout
10046 *
10047 * @constructor
10048 * @param {Object} [config] Configuration options
10049 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
10050 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
10051 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
10052 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
10053 */
10054 OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
10055 // Configuration initialization
10056 config = $.extend( {
10057 scrollable: false,
10058 padded: false,
10059 expanded: true,
10060 framed: false
10061 }, config );
10062
10063 // Parent constructor
10064 OO.ui.PanelLayout.parent.call( this, config );
10065
10066 // Initialization
10067 this.$element.addClass( 'oo-ui-panelLayout' );
10068 if ( config.scrollable ) {
10069 this.$element.addClass( 'oo-ui-panelLayout-scrollable' );
10070 }
10071 if ( config.padded ) {
10072 this.$element.addClass( 'oo-ui-panelLayout-padded' );
10073 }
10074 if ( config.expanded ) {
10075 this.$element.addClass( 'oo-ui-panelLayout-expanded' );
10076 }
10077 if ( config.framed ) {
10078 this.$element.addClass( 'oo-ui-panelLayout-framed' );
10079 }
10080 };
10081
10082 /* Setup */
10083
10084 OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
10085
10086 /**
10087 * CardLayouts are used within {@link OO.ui.IndexLayout index layouts} to create cards that users can select and display
10088 * from the index's optional {@link OO.ui.TabSelectWidget tab} navigation. Cards are usually not instantiated directly,
10089 * rather extended to include the required content and functionality.
10090 *
10091 * Each card must have a unique symbolic name, which is passed to the constructor. In addition, the card's tab
10092 * item is customized (with a label) using the #setupTabItem method. See
10093 * {@link OO.ui.IndexLayout IndexLayout} for an example.
10094 *
10095 * @class
10096 * @extends OO.ui.PanelLayout
10097 *
10098 * @constructor
10099 * @param {string} name Unique symbolic name of card
10100 * @param {Object} [config] Configuration options
10101 */
10102 OO.ui.CardLayout = function OoUiCardLayout( name, config ) {
10103 // Allow passing positional parameters inside the config object
10104 if ( OO.isPlainObject( name ) && config === undefined ) {
10105 config = name;
10106 name = config.name;
10107 }
10108
10109 // Configuration initialization
10110 config = $.extend( { scrollable: true }, config );
10111
10112 // Parent constructor
10113 OO.ui.CardLayout.parent.call( this, config );
10114
10115 // Properties
10116 this.name = name;
10117 this.tabItem = null;
10118 this.active = false;
10119
10120 // Initialization
10121 this.$element.addClass( 'oo-ui-cardLayout' );
10122 };
10123
10124 /* Setup */
10125
10126 OO.inheritClass( OO.ui.CardLayout, OO.ui.PanelLayout );
10127
10128 /* Events */
10129
10130 /**
10131 * An 'active' event is emitted when the card becomes active. Cards become active when they are
10132 * shown in a index layout that is configured to display only one card at a time.
10133 *
10134 * @event active
10135 * @param {boolean} active Card is active
10136 */
10137
10138 /* Methods */
10139
10140 /**
10141 * Get the symbolic name of the card.
10142 *
10143 * @return {string} Symbolic name of card
10144 */
10145 OO.ui.CardLayout.prototype.getName = function () {
10146 return this.name;
10147 };
10148
10149 /**
10150 * Check if card is active.
10151 *
10152 * Cards become active when they are shown in a {@link OO.ui.IndexLayout index layout} that is configured to display
10153 * only one card at a time. Additional CSS is applied to the card's tab item to reflect the active state.
10154 *
10155 * @return {boolean} Card is active
10156 */
10157 OO.ui.CardLayout.prototype.isActive = function () {
10158 return this.active;
10159 };
10160
10161 /**
10162 * Get tab item.
10163 *
10164 * The tab item allows users to access the card from the index's tab
10165 * navigation. The tab item itself can be customized (with a label, level, etc.) using the #setupTabItem method.
10166 *
10167 * @return {OO.ui.TabOptionWidget|null} Tab option widget
10168 */
10169 OO.ui.CardLayout.prototype.getTabItem = function () {
10170 return this.tabItem;
10171 };
10172
10173 /**
10174 * Set or unset the tab item.
10175 *
10176 * Specify a {@link OO.ui.TabOptionWidget tab option} to set it,
10177 * or `null` to clear the tab item. To customize the tab item itself (e.g., to set a label or tab
10178 * level), use #setupTabItem instead of this method.
10179 *
10180 * @param {OO.ui.TabOptionWidget|null} tabItem Tab option widget, null to clear
10181 * @chainable
10182 */
10183 OO.ui.CardLayout.prototype.setTabItem = function ( tabItem ) {
10184 this.tabItem = tabItem || null;
10185 if ( tabItem ) {
10186 this.setupTabItem();
10187 }
10188 return this;
10189 };
10190
10191 /**
10192 * Set up the tab item.
10193 *
10194 * Use this method to customize the tab item (e.g., to add a label or tab level). To set or unset
10195 * the tab item itself (with a {@link OO.ui.TabOptionWidget tab option} or `null`), use
10196 * the #setTabItem method instead.
10197 *
10198 * @param {OO.ui.TabOptionWidget} tabItem Tab option widget to set up
10199 * @chainable
10200 */
10201 OO.ui.CardLayout.prototype.setupTabItem = function () {
10202 return this;
10203 };
10204
10205 /**
10206 * Set the card to its 'active' state.
10207 *
10208 * Cards become active when they are shown in a index layout that is configured to display only one card at a time. Additional
10209 * CSS is applied to the tab item to reflect the card's active state. Outside of the index
10210 * context, setting the active state on a card does nothing.
10211 *
10212 * @param {boolean} value Card is active
10213 * @fires active
10214 */
10215 OO.ui.CardLayout.prototype.setActive = function ( active ) {
10216 active = !!active;
10217
10218 if ( active !== this.active ) {
10219 this.active = active;
10220 this.$element.toggleClass( 'oo-ui-cardLayout-active', this.active );
10221 this.emit( 'active', this.active );
10222 }
10223 };
10224
10225 /**
10226 * PageLayouts are used within {@link OO.ui.BookletLayout booklet layouts} to create pages that users can select and display
10227 * from the booklet's optional {@link OO.ui.OutlineSelectWidget outline} navigation. Pages are usually not instantiated directly,
10228 * rather extended to include the required content and functionality.
10229 *
10230 * Each page must have a unique symbolic name, which is passed to the constructor. In addition, the page's outline
10231 * item is customized (with a label, outline level, etc.) using the #setupOutlineItem method. See
10232 * {@link OO.ui.BookletLayout BookletLayout} for an example.
10233 *
10234 * @class
10235 * @extends OO.ui.PanelLayout
10236 *
10237 * @constructor
10238 * @param {string} name Unique symbolic name of page
10239 * @param {Object} [config] Configuration options
10240 */
10241 OO.ui.PageLayout = function OoUiPageLayout( name, config ) {
10242 // Allow passing positional parameters inside the config object
10243 if ( OO.isPlainObject( name ) && config === undefined ) {
10244 config = name;
10245 name = config.name;
10246 }
10247
10248 // Configuration initialization
10249 config = $.extend( { scrollable: true }, config );
10250
10251 // Parent constructor
10252 OO.ui.PageLayout.parent.call( this, config );
10253
10254 // Properties
10255 this.name = name;
10256 this.outlineItem = null;
10257 this.active = false;
10258
10259 // Initialization
10260 this.$element.addClass( 'oo-ui-pageLayout' );
10261 };
10262
10263 /* Setup */
10264
10265 OO.inheritClass( OO.ui.PageLayout, OO.ui.PanelLayout );
10266
10267 /* Events */
10268
10269 /**
10270 * An 'active' event is emitted when the page becomes active. Pages become active when they are
10271 * shown in a booklet layout that is configured to display only one page at a time.
10272 *
10273 * @event active
10274 * @param {boolean} active Page is active
10275 */
10276
10277 /* Methods */
10278
10279 /**
10280 * Get the symbolic name of the page.
10281 *
10282 * @return {string} Symbolic name of page
10283 */
10284 OO.ui.PageLayout.prototype.getName = function () {
10285 return this.name;
10286 };
10287
10288 /**
10289 * Check if page is active.
10290 *
10291 * Pages become active when they are shown in a {@link OO.ui.BookletLayout booklet layout} that is configured to display
10292 * only one page at a time. Additional CSS is applied to the page's outline item to reflect the active state.
10293 *
10294 * @return {boolean} Page is active
10295 */
10296 OO.ui.PageLayout.prototype.isActive = function () {
10297 return this.active;
10298 };
10299
10300 /**
10301 * Get outline item.
10302 *
10303 * The outline item allows users to access the page from the booklet's outline
10304 * navigation. The outline item itself can be customized (with a label, level, etc.) using the #setupOutlineItem method.
10305 *
10306 * @return {OO.ui.OutlineOptionWidget|null} Outline option widget
10307 */
10308 OO.ui.PageLayout.prototype.getOutlineItem = function () {
10309 return this.outlineItem;
10310 };
10311
10312 /**
10313 * Set or unset the outline item.
10314 *
10315 * Specify an {@link OO.ui.OutlineOptionWidget outline option} to set it,
10316 * or `null` to clear the outline item. To customize the outline item itself (e.g., to set a label or outline
10317 * level), use #setupOutlineItem instead of this method.
10318 *
10319 * @param {OO.ui.OutlineOptionWidget|null} outlineItem Outline option widget, null to clear
10320 * @chainable
10321 */
10322 OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) {
10323 this.outlineItem = outlineItem || null;
10324 if ( outlineItem ) {
10325 this.setupOutlineItem();
10326 }
10327 return this;
10328 };
10329
10330 /**
10331 * Set up the outline item.
10332 *
10333 * Use this method to customize the outline item (e.g., to add a label or outline level). To set or unset
10334 * the outline item itself (with an {@link OO.ui.OutlineOptionWidget outline option} or `null`), use
10335 * the #setOutlineItem method instead.
10336 *
10337 * @param {OO.ui.OutlineOptionWidget} outlineItem Outline option widget to set up
10338 * @chainable
10339 */
10340 OO.ui.PageLayout.prototype.setupOutlineItem = function () {
10341 return this;
10342 };
10343
10344 /**
10345 * Set the page to its 'active' state.
10346 *
10347 * Pages become active when they are shown in a booklet layout that is configured to display only one page at a time. Additional
10348 * CSS is applied to the outline item to reflect the page's active state. Outside of the booklet
10349 * context, setting the active state on a page does nothing.
10350 *
10351 * @param {boolean} value Page is active
10352 * @fires active
10353 */
10354 OO.ui.PageLayout.prototype.setActive = function ( active ) {
10355 active = !!active;
10356
10357 if ( active !== this.active ) {
10358 this.active = active;
10359 this.$element.toggleClass( 'oo-ui-pageLayout-active', active );
10360 this.emit( 'active', this.active );
10361 }
10362 };
10363
10364 /**
10365 * StackLayouts contain a series of {@link OO.ui.PanelLayout panel layouts}. By default, only one panel is displayed
10366 * at a time, though the stack layout can also be configured to show all contained panels, one after another,
10367 * by setting the #continuous option to 'true'.
10368 *
10369 * @example
10370 * // A stack layout with two panels, configured to be displayed continously
10371 * var myStack = new OO.ui.StackLayout( {
10372 * items: [
10373 * new OO.ui.PanelLayout( {
10374 * $content: $( '<p>Panel One</p>' ),
10375 * padded: true,
10376 * framed: true
10377 * } ),
10378 * new OO.ui.PanelLayout( {
10379 * $content: $( '<p>Panel Two</p>' ),
10380 * padded: true,
10381 * framed: true
10382 * } )
10383 * ],
10384 * continuous: true
10385 * } );
10386 * $( 'body' ).append( myStack.$element );
10387 *
10388 * @class
10389 * @extends OO.ui.PanelLayout
10390 * @mixins OO.ui.mixin.GroupElement
10391 *
10392 * @constructor
10393 * @param {Object} [config] Configuration options
10394 * @cfg {boolean} [continuous=false] Show all panels, one after another. By default, only one panel is displayed at a time.
10395 * @cfg {OO.ui.Layout[]} [items] Panel layouts to add to the stack layout.
10396 */
10397 OO.ui.StackLayout = function OoUiStackLayout( config ) {
10398 // Configuration initialization
10399 config = $.extend( { scrollable: true }, config );
10400
10401 // Parent constructor
10402 OO.ui.StackLayout.parent.call( this, config );
10403
10404 // Mixin constructors
10405 OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
10406
10407 // Properties
10408 this.currentItem = null;
10409 this.continuous = !!config.continuous;
10410
10411 // Initialization
10412 this.$element.addClass( 'oo-ui-stackLayout' );
10413 if ( this.continuous ) {
10414 this.$element.addClass( 'oo-ui-stackLayout-continuous' );
10415 }
10416 if ( Array.isArray( config.items ) ) {
10417 this.addItems( config.items );
10418 }
10419 };
10420
10421 /* Setup */
10422
10423 OO.inheritClass( OO.ui.StackLayout, OO.ui.PanelLayout );
10424 OO.mixinClass( OO.ui.StackLayout, OO.ui.mixin.GroupElement );
10425
10426 /* Events */
10427
10428 /**
10429 * A 'set' event is emitted when panels are {@link #addItems added}, {@link #removeItems removed},
10430 * {@link #clearItems cleared} or {@link #setItem displayed}.
10431 *
10432 * @event set
10433 * @param {OO.ui.Layout|null} item Current panel or `null` if no panel is shown
10434 */
10435
10436 /* Methods */
10437
10438 /**
10439 * Get the current panel.
10440 *
10441 * @return {OO.ui.Layout|null}
10442 */
10443 OO.ui.StackLayout.prototype.getCurrentItem = function () {
10444 return this.currentItem;
10445 };
10446
10447 /**
10448 * Unset the current item.
10449 *
10450 * @private
10451 * @param {OO.ui.StackLayout} layout
10452 * @fires set
10453 */
10454 OO.ui.StackLayout.prototype.unsetCurrentItem = function () {
10455 var prevItem = this.currentItem;
10456 if ( prevItem === null ) {
10457 return;
10458 }
10459
10460 this.currentItem = null;
10461 this.emit( 'set', null );
10462 };
10463
10464 /**
10465 * Add panel layouts to the stack layout.
10466 *
10467 * Panels will be added to the end of the stack layout array unless the optional index parameter specifies a different
10468 * insertion point. Adding a panel that is already in the stack will move it to the end of the array or the point specified
10469 * by the index.
10470 *
10471 * @param {OO.ui.Layout[]} items Panels to add
10472 * @param {number} [index] Index of the insertion point
10473 * @chainable
10474 */
10475 OO.ui.StackLayout.prototype.addItems = function ( items, index ) {
10476 // Update the visibility
10477 this.updateHiddenState( items, this.currentItem );
10478
10479 // Mixin method
10480 OO.ui.mixin.GroupElement.prototype.addItems.call( this, items, index );
10481
10482 if ( !this.currentItem && items.length ) {
10483 this.setItem( items[ 0 ] );
10484 }
10485
10486 return this;
10487 };
10488
10489 /**
10490 * Remove the specified panels from the stack layout.
10491 *
10492 * Removed panels are detached from the DOM, not removed, so that they may be reused. To remove all panels,
10493 * you may wish to use the #clearItems method instead.
10494 *
10495 * @param {OO.ui.Layout[]} items Panels to remove
10496 * @chainable
10497 * @fires set
10498 */
10499 OO.ui.StackLayout.prototype.removeItems = function ( items ) {
10500 // Mixin method
10501 OO.ui.mixin.GroupElement.prototype.removeItems.call( this, items );
10502
10503 if ( $.inArray( this.currentItem, items ) !== -1 ) {
10504 if ( this.items.length ) {
10505 this.setItem( this.items[ 0 ] );
10506 } else {
10507 this.unsetCurrentItem();
10508 }
10509 }
10510
10511 return this;
10512 };
10513
10514 /**
10515 * Clear all panels from the stack layout.
10516 *
10517 * Cleared panels are detached from the DOM, not removed, so that they may be reused. To remove only
10518 * a subset of panels, use the #removeItems method.
10519 *
10520 * @chainable
10521 * @fires set
10522 */
10523 OO.ui.StackLayout.prototype.clearItems = function () {
10524 this.unsetCurrentItem();
10525 OO.ui.mixin.GroupElement.prototype.clearItems.call( this );
10526
10527 return this;
10528 };
10529
10530 /**
10531 * Show the specified panel.
10532 *
10533 * If another panel is currently displayed, it will be hidden.
10534 *
10535 * @param {OO.ui.Layout} item Panel to show
10536 * @chainable
10537 * @fires set
10538 */
10539 OO.ui.StackLayout.prototype.setItem = function ( item ) {
10540 if ( item !== this.currentItem ) {
10541 this.updateHiddenState( this.items, item );
10542
10543 if ( $.inArray( item, this.items ) !== -1 ) {
10544 this.currentItem = item;
10545 this.emit( 'set', item );
10546 } else {
10547 this.unsetCurrentItem();
10548 }
10549 }
10550
10551 return this;
10552 };
10553
10554 /**
10555 * Update the visibility of all items in case of non-continuous view.
10556 *
10557 * Ensure all items are hidden except for the selected one.
10558 * This method does nothing when the stack is continuous.
10559 *
10560 * @private
10561 * @param {OO.ui.Layout[]} items Item list iterate over
10562 * @param {OO.ui.Layout} [selectedItem] Selected item to show
10563 */
10564 OO.ui.StackLayout.prototype.updateHiddenState = function ( items, selectedItem ) {
10565 var i, len;
10566
10567 if ( !this.continuous ) {
10568 for ( i = 0, len = items.length; i < len; i++ ) {
10569 if ( !selectedItem || selectedItem !== items[ i ] ) {
10570 items[ i ].$element.addClass( 'oo-ui-element-hidden' );
10571 }
10572 }
10573 if ( selectedItem ) {
10574 selectedItem.$element.removeClass( 'oo-ui-element-hidden' );
10575 }
10576 }
10577 };
10578
10579 /**
10580 * BarToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
10581 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.MenuToolGroup MenuToolGroup}
10582 * and {@link OO.ui.ListToolGroup ListToolGroup}). The {@link OO.ui.Tool tools} in a BarToolGroup are
10583 * displayed by icon in a single row. The title of the tool is displayed when users move the mouse over
10584 * the tool.
10585 *
10586 * BarToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar is
10587 * set up.
10588 *
10589 * @example
10590 * // Example of a BarToolGroup with two tools
10591 * var toolFactory = new OO.ui.ToolFactory();
10592 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
10593 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
10594 *
10595 * // We will be placing status text in this element when tools are used
10596 * var $area = $( '<p>' ).text( 'Example of a BarToolGroup with two tools.' );
10597 *
10598 * // Define the tools that we're going to place in our toolbar
10599 *
10600 * // Create a class inheriting from OO.ui.Tool
10601 * function PictureTool() {
10602 * PictureTool.parent.apply( this, arguments );
10603 * }
10604 * OO.inheritClass( PictureTool, OO.ui.Tool );
10605 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
10606 * // of 'icon' and 'title' (displayed icon and text).
10607 * PictureTool.static.name = 'picture';
10608 * PictureTool.static.icon = 'picture';
10609 * PictureTool.static.title = 'Insert picture';
10610 * // Defines the action that will happen when this tool is selected (clicked).
10611 * PictureTool.prototype.onSelect = function () {
10612 * $area.text( 'Picture tool clicked!' );
10613 * // Never display this tool as "active" (selected).
10614 * this.setActive( false );
10615 * };
10616 * // Make this tool available in our toolFactory and thus our toolbar
10617 * toolFactory.register( PictureTool );
10618 *
10619 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
10620 * // little popup window (a PopupWidget).
10621 * function HelpTool( toolGroup, config ) {
10622 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
10623 * padded: true,
10624 * label: 'Help',
10625 * head: true
10626 * } }, config ) );
10627 * this.popup.$body.append( '<p>I am helpful!</p>' );
10628 * }
10629 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
10630 * HelpTool.static.name = 'help';
10631 * HelpTool.static.icon = 'help';
10632 * HelpTool.static.title = 'Help';
10633 * toolFactory.register( HelpTool );
10634 *
10635 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
10636 * // used once (but not all defined tools must be used).
10637 * toolbar.setup( [
10638 * {
10639 * // 'bar' tool groups display tools by icon only
10640 * type: 'bar',
10641 * include: [ 'picture', 'help' ]
10642 * }
10643 * ] );
10644 *
10645 * // Create some UI around the toolbar and place it in the document
10646 * var frame = new OO.ui.PanelLayout( {
10647 * expanded: false,
10648 * framed: true
10649 * } );
10650 * var contentFrame = new OO.ui.PanelLayout( {
10651 * expanded: false,
10652 * padded: true
10653 * } );
10654 * frame.$element.append(
10655 * toolbar.$element,
10656 * contentFrame.$element.append( $area )
10657 * );
10658 * $( 'body' ).append( frame.$element );
10659 *
10660 * // Here is where the toolbar is actually built. This must be done after inserting it into the
10661 * // document.
10662 * toolbar.initialize();
10663 *
10664 * For more information about how to add tools to a bar tool group, please see {@link OO.ui.ToolGroup toolgroup}.
10665 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
10666 *
10667 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
10668 *
10669 * @class
10670 * @extends OO.ui.ToolGroup
10671 *
10672 * @constructor
10673 * @param {OO.ui.Toolbar} toolbar
10674 * @param {Object} [config] Configuration options
10675 */
10676 OO.ui.BarToolGroup = function OoUiBarToolGroup( toolbar, config ) {
10677 // Allow passing positional parameters inside the config object
10678 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
10679 config = toolbar;
10680 toolbar = config.toolbar;
10681 }
10682
10683 // Parent constructor
10684 OO.ui.BarToolGroup.parent.call( this, toolbar, config );
10685
10686 // Initialization
10687 this.$element.addClass( 'oo-ui-barToolGroup' );
10688 };
10689
10690 /* Setup */
10691
10692 OO.inheritClass( OO.ui.BarToolGroup, OO.ui.ToolGroup );
10693
10694 /* Static Properties */
10695
10696 OO.ui.BarToolGroup.static.titleTooltips = true;
10697
10698 OO.ui.BarToolGroup.static.accelTooltips = true;
10699
10700 OO.ui.BarToolGroup.static.name = 'bar';
10701
10702 /**
10703 * PopupToolGroup is an abstract base class used by both {@link OO.ui.MenuToolGroup MenuToolGroup}
10704 * and {@link OO.ui.ListToolGroup ListToolGroup} to provide a popup--an overlaid menu or list of tools with an
10705 * optional icon and label. This class can be used for other base classes that also use this functionality.
10706 *
10707 * @abstract
10708 * @class
10709 * @extends OO.ui.ToolGroup
10710 * @mixins OO.ui.mixin.IconElement
10711 * @mixins OO.ui.mixin.IndicatorElement
10712 * @mixins OO.ui.mixin.LabelElement
10713 * @mixins OO.ui.mixin.TitledElement
10714 * @mixins OO.ui.mixin.ClippableElement
10715 * @mixins OO.ui.mixin.TabIndexedElement
10716 *
10717 * @constructor
10718 * @param {OO.ui.Toolbar} toolbar
10719 * @param {Object} [config] Configuration options
10720 * @cfg {string} [header] Text to display at the top of the popup
10721 */
10722 OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
10723 // Allow passing positional parameters inside the config object
10724 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
10725 config = toolbar;
10726 toolbar = config.toolbar;
10727 }
10728
10729 // Configuration initialization
10730 config = config || {};
10731
10732 // Parent constructor
10733 OO.ui.PopupToolGroup.parent.call( this, toolbar, config );
10734
10735 // Properties
10736 this.active = false;
10737 this.dragging = false;
10738 this.onBlurHandler = this.onBlur.bind( this );
10739 this.$handle = $( '<span>' );
10740
10741 // Mixin constructors
10742 OO.ui.mixin.IconElement.call( this, config );
10743 OO.ui.mixin.IndicatorElement.call( this, config );
10744 OO.ui.mixin.LabelElement.call( this, config );
10745 OO.ui.mixin.TitledElement.call( this, config );
10746 OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
10747 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
10748
10749 // Events
10750 this.$handle.on( {
10751 keydown: this.onHandleMouseKeyDown.bind( this ),
10752 keyup: this.onHandleMouseKeyUp.bind( this ),
10753 mousedown: this.onHandleMouseKeyDown.bind( this ),
10754 mouseup: this.onHandleMouseKeyUp.bind( this )
10755 } );
10756
10757 // Initialization
10758 this.$handle
10759 .addClass( 'oo-ui-popupToolGroup-handle' )
10760 .append( this.$icon, this.$label, this.$indicator );
10761 // If the pop-up should have a header, add it to the top of the toolGroup.
10762 // Note: If this feature is useful for other widgets, we could abstract it into an
10763 // OO.ui.HeaderedElement mixin constructor.
10764 if ( config.header !== undefined ) {
10765 this.$group
10766 .prepend( $( '<span>' )
10767 .addClass( 'oo-ui-popupToolGroup-header' )
10768 .text( config.header )
10769 );
10770 }
10771 this.$element
10772 .addClass( 'oo-ui-popupToolGroup' )
10773 .prepend( this.$handle );
10774 };
10775
10776 /* Setup */
10777
10778 OO.inheritClass( OO.ui.PopupToolGroup, OO.ui.ToolGroup );
10779 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.IconElement );
10780 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.IndicatorElement );
10781 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.LabelElement );
10782 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.TitledElement );
10783 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.ClippableElement );
10784 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.TabIndexedElement );
10785
10786 /* Methods */
10787
10788 /**
10789 * @inheritdoc
10790 */
10791 OO.ui.PopupToolGroup.prototype.setDisabled = function () {
10792 // Parent method
10793 OO.ui.PopupToolGroup.parent.prototype.setDisabled.apply( this, arguments );
10794
10795 if ( this.isDisabled() && this.isElementAttached() ) {
10796 this.setActive( false );
10797 }
10798 };
10799
10800 /**
10801 * Handle focus being lost.
10802 *
10803 * The event is actually generated from a mouseup/keyup, so it is not a normal blur event object.
10804 *
10805 * @protected
10806 * @param {jQuery.Event} e Mouse up or key up event
10807 */
10808 OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) {
10809 // Only deactivate when clicking outside the dropdown element
10810 if ( $( e.target ).closest( '.oo-ui-popupToolGroup' )[ 0 ] !== this.$element[ 0 ] ) {
10811 this.setActive( false );
10812 }
10813 };
10814
10815 /**
10816 * @inheritdoc
10817 */
10818 OO.ui.PopupToolGroup.prototype.onMouseKeyUp = function ( e ) {
10819 // Only close toolgroup when a tool was actually selected
10820 if (
10821 !this.isDisabled() && this.pressed && this.pressed === this.getTargetTool( e ) &&
10822 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
10823 ) {
10824 this.setActive( false );
10825 }
10826 return OO.ui.PopupToolGroup.parent.prototype.onMouseKeyUp.call( this, e );
10827 };
10828
10829 /**
10830 * Handle mouse up and key up events.
10831 *
10832 * @protected
10833 * @param {jQuery.Event} e Mouse up or key up event
10834 */
10835 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyUp = function ( e ) {
10836 if (
10837 !this.isDisabled() &&
10838 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
10839 ) {
10840 return false;
10841 }
10842 };
10843
10844 /**
10845 * Handle mouse down and key down events.
10846 *
10847 * @protected
10848 * @param {jQuery.Event} e Mouse down or key down event
10849 */
10850 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyDown = function ( e ) {
10851 if (
10852 !this.isDisabled() &&
10853 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
10854 ) {
10855 this.setActive( !this.active );
10856 return false;
10857 }
10858 };
10859
10860 /**
10861 * Switch into 'active' mode.
10862 *
10863 * When active, the popup is visible. A mouseup event anywhere in the document will trigger
10864 * deactivation.
10865 */
10866 OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
10867 value = !!value;
10868 if ( this.active !== value ) {
10869 this.active = value;
10870 if ( value ) {
10871 this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler, true );
10872 this.getElementDocument().addEventListener( 'keyup', this.onBlurHandler, true );
10873
10874 // Try anchoring the popup to the left first
10875 this.$element.addClass( 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left' );
10876 this.toggleClipping( true );
10877 if ( this.isClippedHorizontally() ) {
10878 // Anchoring to the left caused the popup to clip, so anchor it to the right instead
10879 this.toggleClipping( false );
10880 this.$element
10881 .removeClass( 'oo-ui-popupToolGroup-left' )
10882 .addClass( 'oo-ui-popupToolGroup-right' );
10883 this.toggleClipping( true );
10884 }
10885 } else {
10886 this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler, true );
10887 this.getElementDocument().removeEventListener( 'keyup', this.onBlurHandler, true );
10888 this.$element.removeClass(
10889 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left oo-ui-popupToolGroup-right'
10890 );
10891 this.toggleClipping( false );
10892 }
10893 }
10894 };
10895
10896 /**
10897 * ListToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
10898 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.MenuToolGroup MenuToolGroup}
10899 * and {@link OO.ui.BarToolGroup BarToolGroup}). The {@link OO.ui.Tool tools} in a ListToolGroup are displayed
10900 * by label in a dropdown menu. The title of the tool is used as the label text. The menu itself can be configured
10901 * with a label, icon, indicator, header, and title.
10902 *
10903 * ListToolGroups can be configured to be expanded and collapsed. Collapsed lists will have a ‘More’ option that
10904 * users can select to see the full list of tools. If a collapsed toolgroup is expanded, a ‘Fewer’ option permits
10905 * users to collapse the list again.
10906 *
10907 * ListToolGroups are created by a {@link OO.ui.ToolGroupFactory toolgroup factory} when the toolbar is set up. The factory
10908 * requires the ListToolGroup's symbolic name, 'list', which is specified along with the other configurations. For more
10909 * information about how to add tools to a ListToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
10910 *
10911 * @example
10912 * // Example of a ListToolGroup
10913 * var toolFactory = new OO.ui.ToolFactory();
10914 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
10915 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
10916 *
10917 * // Configure and register two tools
10918 * function SettingsTool() {
10919 * SettingsTool.parent.apply( this, arguments );
10920 * }
10921 * OO.inheritClass( SettingsTool, OO.ui.Tool );
10922 * SettingsTool.static.name = 'settings';
10923 * SettingsTool.static.icon = 'settings';
10924 * SettingsTool.static.title = 'Change settings';
10925 * SettingsTool.prototype.onSelect = function () {
10926 * this.setActive( false );
10927 * };
10928 * toolFactory.register( SettingsTool );
10929 * // Register two more tools, nothing interesting here
10930 * function StuffTool() {
10931 * StuffTool.parent.apply( this, arguments );
10932 * }
10933 * OO.inheritClass( StuffTool, OO.ui.Tool );
10934 * StuffTool.static.name = 'stuff';
10935 * StuffTool.static.icon = 'ellipsis';
10936 * StuffTool.static.title = 'Change the world';
10937 * StuffTool.prototype.onSelect = function () {
10938 * this.setActive( false );
10939 * };
10940 * toolFactory.register( StuffTool );
10941 * toolbar.setup( [
10942 * {
10943 * // Configurations for list toolgroup.
10944 * type: 'list',
10945 * label: 'ListToolGroup',
10946 * indicator: 'down',
10947 * icon: 'picture',
10948 * title: 'This is the title, displayed when user moves the mouse over the list toolgroup',
10949 * header: 'This is the header',
10950 * include: [ 'settings', 'stuff' ],
10951 * allowCollapse: ['stuff']
10952 * }
10953 * ] );
10954 *
10955 * // Create some UI around the toolbar and place it in the document
10956 * var frame = new OO.ui.PanelLayout( {
10957 * expanded: false,
10958 * framed: true
10959 * } );
10960 * frame.$element.append(
10961 * toolbar.$element
10962 * );
10963 * $( 'body' ).append( frame.$element );
10964 * // Build the toolbar. This must be done after the toolbar has been appended to the document.
10965 * toolbar.initialize();
10966 *
10967 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
10968 *
10969 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
10970 *
10971 * @class
10972 * @extends OO.ui.PopupToolGroup
10973 *
10974 * @constructor
10975 * @param {OO.ui.Toolbar} toolbar
10976 * @param {Object} [config] Configuration options
10977 * @cfg {Array} [allowCollapse] Allow the specified tools to be collapsed. By default, collapsible tools
10978 * will only be displayed if users click the ‘More’ option displayed at the bottom of the list. If
10979 * the list is expanded, a ‘Fewer’ option permits users to collapse the list again. Any tools that
10980 * are included in the toolgroup, but are not designated as collapsible, will always be displayed.
10981 * To open a collapsible list in its expanded state, set #expanded to 'true'.
10982 * @cfg {Array} [forceExpand] Expand the specified tools. All other tools will be designated as collapsible.
10983 * Unless #expanded is set to true, the collapsible tools will be collapsed when the list is first opened.
10984 * @cfg {boolean} [expanded=false] Expand collapsible tools. This config is only relevant if tools have
10985 * been designated as collapsible. When expanded is set to true, all tools in the group will be displayed
10986 * when the list is first opened. Users can collapse the list with a ‘Fewer’ option at the bottom.
10987 */
10988 OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
10989 // Allow passing positional parameters inside the config object
10990 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
10991 config = toolbar;
10992 toolbar = config.toolbar;
10993 }
10994
10995 // Configuration initialization
10996 config = config || {};
10997
10998 // Properties (must be set before parent constructor, which calls #populate)
10999 this.allowCollapse = config.allowCollapse;
11000 this.forceExpand = config.forceExpand;
11001 this.expanded = config.expanded !== undefined ? config.expanded : false;
11002 this.collapsibleTools = [];
11003
11004 // Parent constructor
11005 OO.ui.ListToolGroup.parent.call( this, toolbar, config );
11006
11007 // Initialization
11008 this.$element.addClass( 'oo-ui-listToolGroup' );
11009 };
11010
11011 /* Setup */
11012
11013 OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup );
11014
11015 /* Static Properties */
11016
11017 OO.ui.ListToolGroup.static.name = 'list';
11018
11019 /* Methods */
11020
11021 /**
11022 * @inheritdoc
11023 */
11024 OO.ui.ListToolGroup.prototype.populate = function () {
11025 var i, len, allowCollapse = [];
11026
11027 OO.ui.ListToolGroup.parent.prototype.populate.call( this );
11028
11029 // Update the list of collapsible tools
11030 if ( this.allowCollapse !== undefined ) {
11031 allowCollapse = this.allowCollapse;
11032 } else if ( this.forceExpand !== undefined ) {
11033 allowCollapse = OO.simpleArrayDifference( Object.keys( this.tools ), this.forceExpand );
11034 }
11035
11036 this.collapsibleTools = [];
11037 for ( i = 0, len = allowCollapse.length; i < len; i++ ) {
11038 if ( this.tools[ allowCollapse[ i ] ] !== undefined ) {
11039 this.collapsibleTools.push( this.tools[ allowCollapse[ i ] ] );
11040 }
11041 }
11042
11043 // Keep at the end, even when tools are added
11044 this.$group.append( this.getExpandCollapseTool().$element );
11045
11046 this.getExpandCollapseTool().toggle( this.collapsibleTools.length !== 0 );
11047 this.updateCollapsibleState();
11048 };
11049
11050 OO.ui.ListToolGroup.prototype.getExpandCollapseTool = function () {
11051 if ( this.expandCollapseTool === undefined ) {
11052 var ExpandCollapseTool = function () {
11053 ExpandCollapseTool.parent.apply( this, arguments );
11054 };
11055
11056 OO.inheritClass( ExpandCollapseTool, OO.ui.Tool );
11057
11058 ExpandCollapseTool.prototype.onSelect = function () {
11059 this.toolGroup.expanded = !this.toolGroup.expanded;
11060 this.toolGroup.updateCollapsibleState();
11061 this.setActive( false );
11062 };
11063 ExpandCollapseTool.prototype.onUpdateState = function () {
11064 // Do nothing. Tool interface requires an implementation of this function.
11065 };
11066
11067 ExpandCollapseTool.static.name = 'more-fewer';
11068
11069 this.expandCollapseTool = new ExpandCollapseTool( this );
11070 }
11071 return this.expandCollapseTool;
11072 };
11073
11074 /**
11075 * @inheritdoc
11076 */
11077 OO.ui.ListToolGroup.prototype.onMouseKeyUp = function ( e ) {
11078 // Do not close the popup when the user wants to show more/fewer tools
11079 if (
11080 $( e.target ).closest( '.oo-ui-tool-name-more-fewer' ).length &&
11081 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
11082 ) {
11083 // HACK: Prevent the popup list from being hidden. Skip the PopupToolGroup implementation (which
11084 // hides the popup list when a tool is selected) and call ToolGroup's implementation directly.
11085 return OO.ui.ListToolGroup.parent.parent.prototype.onMouseKeyUp.call( this, e );
11086 } else {
11087 return OO.ui.ListToolGroup.parent.prototype.onMouseKeyUp.call( this, e );
11088 }
11089 };
11090
11091 OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () {
11092 var i, len;
11093
11094 this.getExpandCollapseTool()
11095 .setIcon( this.expanded ? 'collapse' : 'expand' )
11096 .setTitle( OO.ui.msg( this.expanded ? 'ooui-toolgroup-collapse' : 'ooui-toolgroup-expand' ) );
11097
11098 for ( i = 0, len = this.collapsibleTools.length; i < len; i++ ) {
11099 this.collapsibleTools[ i ].toggle( this.expanded );
11100 }
11101 };
11102
11103 /**
11104 * MenuToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
11105 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.BarToolGroup BarToolGroup}
11106 * and {@link OO.ui.ListToolGroup ListToolGroup}). MenuToolGroups contain selectable {@link OO.ui.Tool tools},
11107 * which are displayed by label in a dropdown menu. The tool's title is used as the label text, and the
11108 * menu label is updated to reflect which tool or tools are currently selected. If no tools are selected,
11109 * the menu label is empty. The menu can be configured with an indicator, icon, title, and/or header.
11110 *
11111 * MenuToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar
11112 * is set up. Note that all tools must define an {@link OO.ui.Tool#onUpdateState onUpdateState} method if
11113 * a MenuToolGroup is used.
11114 *
11115 * @example
11116 * // Example of a MenuToolGroup
11117 * var toolFactory = new OO.ui.ToolFactory();
11118 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
11119 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
11120 *
11121 * // We will be placing status text in this element when tools are used
11122 * var $area = $( '<p>' ).text( 'An example of a MenuToolGroup. Select a tool from the dropdown menu.' );
11123 *
11124 * // Define the tools that we're going to place in our toolbar
11125 *
11126 * function SettingsTool() {
11127 * SettingsTool.parent.apply( this, arguments );
11128 * this.reallyActive = false;
11129 * }
11130 * OO.inheritClass( SettingsTool, OO.ui.Tool );
11131 * SettingsTool.static.name = 'settings';
11132 * SettingsTool.static.icon = 'settings';
11133 * SettingsTool.static.title = 'Change settings';
11134 * SettingsTool.prototype.onSelect = function () {
11135 * $area.text( 'Settings tool clicked!' );
11136 * // Toggle the active state on each click
11137 * this.reallyActive = !this.reallyActive;
11138 * this.setActive( this.reallyActive );
11139 * // To update the menu label
11140 * this.toolbar.emit( 'updateState' );
11141 * };
11142 * SettingsTool.prototype.onUpdateState = function () {
11143 * };
11144 * toolFactory.register( SettingsTool );
11145 *
11146 * function StuffTool() {
11147 * StuffTool.parent.apply( this, arguments );
11148 * this.reallyActive = false;
11149 * }
11150 * OO.inheritClass( StuffTool, OO.ui.Tool );
11151 * StuffTool.static.name = 'stuff';
11152 * StuffTool.static.icon = 'ellipsis';
11153 * StuffTool.static.title = 'More stuff';
11154 * StuffTool.prototype.onSelect = function () {
11155 * $area.text( 'More stuff tool clicked!' );
11156 * // Toggle the active state on each click
11157 * this.reallyActive = !this.reallyActive;
11158 * this.setActive( this.reallyActive );
11159 * // To update the menu label
11160 * this.toolbar.emit( 'updateState' );
11161 * };
11162 * StuffTool.prototype.onUpdateState = function () {
11163 * };
11164 * toolFactory.register( StuffTool );
11165 *
11166 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
11167 * // used once (but not all defined tools must be used).
11168 * toolbar.setup( [
11169 * {
11170 * type: 'menu',
11171 * header: 'This is the (optional) header',
11172 * title: 'This is the (optional) title',
11173 * indicator: 'down',
11174 * include: [ 'settings', 'stuff' ]
11175 * }
11176 * ] );
11177 *
11178 * // Create some UI around the toolbar and place it in the document
11179 * var frame = new OO.ui.PanelLayout( {
11180 * expanded: false,
11181 * framed: true
11182 * } );
11183 * var contentFrame = new OO.ui.PanelLayout( {
11184 * expanded: false,
11185 * padded: true
11186 * } );
11187 * frame.$element.append(
11188 * toolbar.$element,
11189 * contentFrame.$element.append( $area )
11190 * );
11191 * $( 'body' ).append( frame.$element );
11192 *
11193 * // Here is where the toolbar is actually built. This must be done after inserting it into the
11194 * // document.
11195 * toolbar.initialize();
11196 * toolbar.emit( 'updateState' );
11197 *
11198 * For more information about how to add tools to a MenuToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
11199 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki] [1].
11200 *
11201 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
11202 *
11203 * @class
11204 * @extends OO.ui.PopupToolGroup
11205 *
11206 * @constructor
11207 * @param {OO.ui.Toolbar} toolbar
11208 * @param {Object} [config] Configuration options
11209 */
11210 OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
11211 // Allow passing positional parameters inside the config object
11212 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
11213 config = toolbar;
11214 toolbar = config.toolbar;
11215 }
11216
11217 // Configuration initialization
11218 config = config || {};
11219
11220 // Parent constructor
11221 OO.ui.MenuToolGroup.parent.call( this, toolbar, config );
11222
11223 // Events
11224 this.toolbar.connect( this, { updateState: 'onUpdateState' } );
11225
11226 // Initialization
11227 this.$element.addClass( 'oo-ui-menuToolGroup' );
11228 };
11229
11230 /* Setup */
11231
11232 OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup );
11233
11234 /* Static Properties */
11235
11236 OO.ui.MenuToolGroup.static.name = 'menu';
11237
11238 /* Methods */
11239
11240 /**
11241 * Handle the toolbar state being updated.
11242 *
11243 * When the state changes, the title of each active item in the menu will be joined together and
11244 * used as a label for the group. The label will be empty if none of the items are active.
11245 *
11246 * @private
11247 */
11248 OO.ui.MenuToolGroup.prototype.onUpdateState = function () {
11249 var name,
11250 labelTexts = [];
11251
11252 for ( name in this.tools ) {
11253 if ( this.tools[ name ].isActive() ) {
11254 labelTexts.push( this.tools[ name ].getTitle() );
11255 }
11256 }
11257
11258 this.setLabel( labelTexts.join( ', ' ) || ' ' );
11259 };
11260
11261 /**
11262 * Popup tools open a popup window when they are selected from the {@link OO.ui.Toolbar toolbar}. Each popup tool is configured
11263 * 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
11264 * an #onSelect or #onUpdateState method, as these methods have been implemented already.
11265 *
11266 * // Example of a popup tool. When selected, a popup tool displays
11267 * // a popup window.
11268 * function HelpTool( toolGroup, config ) {
11269 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
11270 * padded: true,
11271 * label: 'Help',
11272 * head: true
11273 * } }, config ) );
11274 * this.popup.$body.append( '<p>I am helpful!</p>' );
11275 * };
11276 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
11277 * HelpTool.static.name = 'help';
11278 * HelpTool.static.icon = 'help';
11279 * HelpTool.static.title = 'Help';
11280 * toolFactory.register( HelpTool );
11281 *
11282 * For an example of a toolbar that contains a popup tool, see {@link OO.ui.Toolbar toolbars}. For more information about
11283 * toolbars in genreral, please see the [OOjs UI documentation on MediaWiki][1].
11284 *
11285 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
11286 *
11287 * @abstract
11288 * @class
11289 * @extends OO.ui.Tool
11290 * @mixins OO.ui.mixin.PopupElement
11291 *
11292 * @constructor
11293 * @param {OO.ui.ToolGroup} toolGroup
11294 * @param {Object} [config] Configuration options
11295 */
11296 OO.ui.PopupTool = function OoUiPopupTool( toolGroup, config ) {
11297 // Allow passing positional parameters inside the config object
11298 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
11299 config = toolGroup;
11300 toolGroup = config.toolGroup;
11301 }
11302
11303 // Parent constructor
11304 OO.ui.PopupTool.parent.call( this, toolGroup, config );
11305
11306 // Mixin constructors
11307 OO.ui.mixin.PopupElement.call( this, config );
11308
11309 // Initialization
11310 this.$element
11311 .addClass( 'oo-ui-popupTool' )
11312 .append( this.popup.$element );
11313 };
11314
11315 /* Setup */
11316
11317 OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool );
11318 OO.mixinClass( OO.ui.PopupTool, OO.ui.mixin.PopupElement );
11319
11320 /* Methods */
11321
11322 /**
11323 * Handle the tool being selected.
11324 *
11325 * @inheritdoc
11326 */
11327 OO.ui.PopupTool.prototype.onSelect = function () {
11328 if ( !this.isDisabled() ) {
11329 this.popup.toggle();
11330 }
11331 this.setActive( false );
11332 return false;
11333 };
11334
11335 /**
11336 * Handle the toolbar state being updated.
11337 *
11338 * @inheritdoc
11339 */
11340 OO.ui.PopupTool.prototype.onUpdateState = function () {
11341 this.setActive( false );
11342 };
11343
11344 /**
11345 * A ToolGroupTool is a special sort of tool that can contain other {@link OO.ui.Tool tools}
11346 * and {@link OO.ui.ToolGroup toolgroups}. The ToolGroupTool was specifically designed to be used
11347 * inside a {@link OO.ui.BarToolGroup bar} toolgroup to provide access to additional tools from
11348 * the bar item. Included tools will be displayed in a dropdown {@link OO.ui.ListToolGroup list}
11349 * when the ToolGroupTool is selected.
11350 *
11351 * // Example: ToolGroupTool with two nested tools, 'setting1' and 'setting2', defined elsewhere.
11352 *
11353 * function SettingsTool() {
11354 * SettingsTool.parent.apply( this, arguments );
11355 * };
11356 * OO.inheritClass( SettingsTool, OO.ui.ToolGroupTool );
11357 * SettingsTool.static.name = 'settings';
11358 * SettingsTool.static.title = 'Change settings';
11359 * SettingsTool.static.groupConfig = {
11360 * icon: 'settings',
11361 * label: 'ToolGroupTool',
11362 * include: [ 'setting1', 'setting2' ]
11363 * };
11364 * toolFactory.register( SettingsTool );
11365 *
11366 * For more information, please see the [OOjs UI documentation on MediaWiki][1].
11367 *
11368 * Please note that this implementation is subject to change per [T74159] [2].
11369 *
11370 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars#ToolGroupTool
11371 * [2]: https://phabricator.wikimedia.org/T74159
11372 *
11373 * @abstract
11374 * @class
11375 * @extends OO.ui.Tool
11376 *
11377 * @constructor
11378 * @param {OO.ui.ToolGroup} toolGroup
11379 * @param {Object} [config] Configuration options
11380 */
11381 OO.ui.ToolGroupTool = function OoUiToolGroupTool( toolGroup, config ) {
11382 // Allow passing positional parameters inside the config object
11383 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
11384 config = toolGroup;
11385 toolGroup = config.toolGroup;
11386 }
11387
11388 // Parent constructor
11389 OO.ui.ToolGroupTool.parent.call( this, toolGroup, config );
11390
11391 // Properties
11392 this.innerToolGroup = this.createGroup( this.constructor.static.groupConfig );
11393
11394 // Events
11395 this.innerToolGroup.connect( this, { disable: 'onToolGroupDisable' } );
11396
11397 // Initialization
11398 this.$link.remove();
11399 this.$element
11400 .addClass( 'oo-ui-toolGroupTool' )
11401 .append( this.innerToolGroup.$element );
11402 };
11403
11404 /* Setup */
11405
11406 OO.inheritClass( OO.ui.ToolGroupTool, OO.ui.Tool );
11407
11408 /* Static Properties */
11409
11410 /**
11411 * Toolgroup configuration.
11412 *
11413 * The toolgroup configuration consists of the tools to include, as well as an icon and label
11414 * to use for the bar item. Tools can be included by symbolic name, group, or with the
11415 * wildcard selector. Please see {@link OO.ui.ToolGroup toolgroup} for more information.
11416 *
11417 * @property {Object.<string,Array>}
11418 */
11419 OO.ui.ToolGroupTool.static.groupConfig = {};
11420
11421 /* Methods */
11422
11423 /**
11424 * Handle the tool being selected.
11425 *
11426 * @inheritdoc
11427 */
11428 OO.ui.ToolGroupTool.prototype.onSelect = function () {
11429 this.innerToolGroup.setActive( !this.innerToolGroup.active );
11430 return false;
11431 };
11432
11433 /**
11434 * Synchronize disabledness state of the tool with the inner toolgroup.
11435 *
11436 * @private
11437 * @param {boolean} disabled Element is disabled
11438 */
11439 OO.ui.ToolGroupTool.prototype.onToolGroupDisable = function ( disabled ) {
11440 this.setDisabled( disabled );
11441 };
11442
11443 /**
11444 * Handle the toolbar state being updated.
11445 *
11446 * @inheritdoc
11447 */
11448 OO.ui.ToolGroupTool.prototype.onUpdateState = function () {
11449 this.setActive( false );
11450 };
11451
11452 /**
11453 * Build a {@link OO.ui.ToolGroup toolgroup} from the specified configuration.
11454 *
11455 * @param {Object.<string,Array>} group Toolgroup configuration. Please see {@link OO.ui.ToolGroup toolgroup} for
11456 * more information.
11457 * @return {OO.ui.ListToolGroup}
11458 */
11459 OO.ui.ToolGroupTool.prototype.createGroup = function ( group ) {
11460 if ( group.include === '*' ) {
11461 // Apply defaults to catch-all groups
11462 if ( group.label === undefined ) {
11463 group.label = OO.ui.msg( 'ooui-toolbar-more' );
11464 }
11465 }
11466
11467 return this.toolbar.getToolGroupFactory().create( 'list', this.toolbar, group );
11468 };
11469
11470 /**
11471 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
11472 *
11473 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
11474 *
11475 * @private
11476 * @abstract
11477 * @class
11478 * @extends OO.ui.mixin.GroupElement
11479 *
11480 * @constructor
11481 * @param {Object} [config] Configuration options
11482 */
11483 OO.ui.mixin.GroupWidget = function OoUiMixinGroupWidget( config ) {
11484 // Parent constructor
11485 OO.ui.mixin.GroupWidget.parent.call( this, config );
11486 };
11487
11488 /* Setup */
11489
11490 OO.inheritClass( OO.ui.mixin.GroupWidget, OO.ui.mixin.GroupElement );
11491
11492 /* Methods */
11493
11494 /**
11495 * Set the disabled state of the widget.
11496 *
11497 * This will also update the disabled state of child widgets.
11498 *
11499 * @param {boolean} disabled Disable widget
11500 * @chainable
11501 */
11502 OO.ui.mixin.GroupWidget.prototype.setDisabled = function ( disabled ) {
11503 var i, len;
11504
11505 // Parent method
11506 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
11507 OO.ui.Widget.prototype.setDisabled.call( this, disabled );
11508
11509 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
11510 if ( this.items ) {
11511 for ( i = 0, len = this.items.length; i < len; i++ ) {
11512 this.items[ i ].updateDisabled();
11513 }
11514 }
11515
11516 return this;
11517 };
11518
11519 /**
11520 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
11521 *
11522 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
11523 * allows bidirectional communication.
11524 *
11525 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
11526 *
11527 * @private
11528 * @abstract
11529 * @class
11530 *
11531 * @constructor
11532 */
11533 OO.ui.mixin.ItemWidget = function OoUiMixinItemWidget() {
11534 //
11535 };
11536
11537 /* Methods */
11538
11539 /**
11540 * Check if widget is disabled.
11541 *
11542 * Checks parent if present, making disabled state inheritable.
11543 *
11544 * @return {boolean} Widget is disabled
11545 */
11546 OO.ui.mixin.ItemWidget.prototype.isDisabled = function () {
11547 return this.disabled ||
11548 ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() );
11549 };
11550
11551 /**
11552 * Set group element is in.
11553 *
11554 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
11555 * @chainable
11556 */
11557 OO.ui.mixin.ItemWidget.prototype.setElementGroup = function ( group ) {
11558 // Parent method
11559 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
11560 OO.ui.Element.prototype.setElementGroup.call( this, group );
11561
11562 // Initialize item disabled states
11563 this.updateDisabled();
11564
11565 return this;
11566 };
11567
11568 /**
11569 * OutlineControlsWidget is a set of controls for an {@link OO.ui.OutlineSelectWidget outline select widget}.
11570 * Controls include moving items up and down, removing items, and adding different kinds of items.
11571 *
11572 * **Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.**
11573 *
11574 * @class
11575 * @extends OO.ui.Widget
11576 * @mixins OO.ui.mixin.GroupElement
11577 * @mixins OO.ui.mixin.IconElement
11578 *
11579 * @constructor
11580 * @param {OO.ui.OutlineSelectWidget} outline Outline to control
11581 * @param {Object} [config] Configuration options
11582 * @cfg {Object} [abilities] List of abilties
11583 * @cfg {boolean} [abilities.move=true] Allow moving movable items
11584 * @cfg {boolean} [abilities.remove=true] Allow removing removable items
11585 */
11586 OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
11587 // Allow passing positional parameters inside the config object
11588 if ( OO.isPlainObject( outline ) && config === undefined ) {
11589 config = outline;
11590 outline = config.outline;
11591 }
11592
11593 // Configuration initialization
11594 config = $.extend( { icon: 'add' }, config );
11595
11596 // Parent constructor
11597 OO.ui.OutlineControlsWidget.parent.call( this, config );
11598
11599 // Mixin constructors
11600 OO.ui.mixin.GroupElement.call( this, config );
11601 OO.ui.mixin.IconElement.call( this, config );
11602
11603 // Properties
11604 this.outline = outline;
11605 this.$movers = $( '<div>' );
11606 this.upButton = new OO.ui.ButtonWidget( {
11607 framed: false,
11608 icon: 'collapse',
11609 title: OO.ui.msg( 'ooui-outline-control-move-up' )
11610 } );
11611 this.downButton = new OO.ui.ButtonWidget( {
11612 framed: false,
11613 icon: 'expand',
11614 title: OO.ui.msg( 'ooui-outline-control-move-down' )
11615 } );
11616 this.removeButton = new OO.ui.ButtonWidget( {
11617 framed: false,
11618 icon: 'remove',
11619 title: OO.ui.msg( 'ooui-outline-control-remove' )
11620 } );
11621 this.abilities = { move: true, remove: true };
11622
11623 // Events
11624 outline.connect( this, {
11625 select: 'onOutlineChange',
11626 add: 'onOutlineChange',
11627 remove: 'onOutlineChange'
11628 } );
11629 this.upButton.connect( this, { click: [ 'emit', 'move', -1 ] } );
11630 this.downButton.connect( this, { click: [ 'emit', 'move', 1 ] } );
11631 this.removeButton.connect( this, { click: [ 'emit', 'remove' ] } );
11632
11633 // Initialization
11634 this.$element.addClass( 'oo-ui-outlineControlsWidget' );
11635 this.$group.addClass( 'oo-ui-outlineControlsWidget-items' );
11636 this.$movers
11637 .addClass( 'oo-ui-outlineControlsWidget-movers' )
11638 .append( this.removeButton.$element, this.upButton.$element, this.downButton.$element );
11639 this.$element.append( this.$icon, this.$group, this.$movers );
11640 this.setAbilities( config.abilities || {} );
11641 };
11642
11643 /* Setup */
11644
11645 OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget );
11646 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.mixin.GroupElement );
11647 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.mixin.IconElement );
11648
11649 /* Events */
11650
11651 /**
11652 * @event move
11653 * @param {number} places Number of places to move
11654 */
11655
11656 /**
11657 * @event remove
11658 */
11659
11660 /* Methods */
11661
11662 /**
11663 * Set abilities.
11664 *
11665 * @param {Object} abilities List of abilties
11666 * @param {boolean} [abilities.move] Allow moving movable items
11667 * @param {boolean} [abilities.remove] Allow removing removable items
11668 */
11669 OO.ui.OutlineControlsWidget.prototype.setAbilities = function ( abilities ) {
11670 var ability;
11671
11672 for ( ability in this.abilities ) {
11673 if ( abilities[ability] !== undefined ) {
11674 this.abilities[ability] = !!abilities[ability];
11675 }
11676 }
11677
11678 this.onOutlineChange();
11679 };
11680
11681 /**
11682 *
11683 * @private
11684 * Handle outline change events.
11685 */
11686 OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () {
11687 var i, len, firstMovable, lastMovable,
11688 items = this.outline.getItems(),
11689 selectedItem = this.outline.getSelectedItem(),
11690 movable = this.abilities.move && selectedItem && selectedItem.isMovable(),
11691 removable = this.abilities.remove && selectedItem && selectedItem.isRemovable();
11692
11693 if ( movable ) {
11694 i = -1;
11695 len = items.length;
11696 while ( ++i < len ) {
11697 if ( items[ i ].isMovable() ) {
11698 firstMovable = items[ i ];
11699 break;
11700 }
11701 }
11702 i = len;
11703 while ( i-- ) {
11704 if ( items[ i ].isMovable() ) {
11705 lastMovable = items[ i ];
11706 break;
11707 }
11708 }
11709 }
11710 this.upButton.setDisabled( !movable || selectedItem === firstMovable );
11711 this.downButton.setDisabled( !movable || selectedItem === lastMovable );
11712 this.removeButton.setDisabled( !removable );
11713 };
11714
11715 /**
11716 * ToggleWidget implements basic behavior of widgets with an on/off state.
11717 * Please see OO.ui.ToggleButtonWidget and OO.ui.ToggleSwitchWidget for examples.
11718 *
11719 * @abstract
11720 * @class
11721 * @extends OO.ui.Widget
11722 *
11723 * @constructor
11724 * @param {Object} [config] Configuration options
11725 * @cfg {boolean} [value=false] The toggle’s initial on/off state.
11726 * By default, the toggle is in the 'off' state.
11727 */
11728 OO.ui.ToggleWidget = function OoUiToggleWidget( config ) {
11729 // Configuration initialization
11730 config = config || {};
11731
11732 // Parent constructor
11733 OO.ui.ToggleWidget.parent.call( this, config );
11734
11735 // Properties
11736 this.value = null;
11737
11738 // Initialization
11739 this.$element.addClass( 'oo-ui-toggleWidget' );
11740 this.setValue( !!config.value );
11741 };
11742
11743 /* Setup */
11744
11745 OO.inheritClass( OO.ui.ToggleWidget, OO.ui.Widget );
11746
11747 /* Events */
11748
11749 /**
11750 * @event change
11751 *
11752 * A change event is emitted when the on/off state of the toggle changes.
11753 *
11754 * @param {boolean} value Value representing the new state of the toggle
11755 */
11756
11757 /* Methods */
11758
11759 /**
11760 * Get the value representing the toggle’s state.
11761 *
11762 * @return {boolean} The on/off state of the toggle
11763 */
11764 OO.ui.ToggleWidget.prototype.getValue = function () {
11765 return this.value;
11766 };
11767
11768 /**
11769 * Set the state of the toggle: `true` for 'on', `false' for 'off'.
11770 *
11771 * @param {boolean} value The state of the toggle
11772 * @fires change
11773 * @chainable
11774 */
11775 OO.ui.ToggleWidget.prototype.setValue = function ( value ) {
11776 value = !!value;
11777 if ( this.value !== value ) {
11778 this.value = value;
11779 this.emit( 'change', value );
11780 this.$element.toggleClass( 'oo-ui-toggleWidget-on', value );
11781 this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value );
11782 this.$element.attr( 'aria-checked', value.toString() );
11783 }
11784 return this;
11785 };
11786
11787 /**
11788 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
11789 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
11790 * removed, and cleared from the group.
11791 *
11792 * @example
11793 * // Example: A ButtonGroupWidget with two buttons
11794 * var button1 = new OO.ui.PopupButtonWidget( {
11795 * label: 'Select a category',
11796 * icon: 'menu',
11797 * popup: {
11798 * $content: $( '<p>List of categories...</p>' ),
11799 * padded: true,
11800 * align: 'left'
11801 * }
11802 * } );
11803 * var button2 = new OO.ui.ButtonWidget( {
11804 * label: 'Add item'
11805 * });
11806 * var buttonGroup = new OO.ui.ButtonGroupWidget( {
11807 * items: [button1, button2]
11808 * } );
11809 * $( 'body' ).append( buttonGroup.$element );
11810 *
11811 * @class
11812 * @extends OO.ui.Widget
11813 * @mixins OO.ui.mixin.GroupElement
11814 *
11815 * @constructor
11816 * @param {Object} [config] Configuration options
11817 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
11818 */
11819 OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
11820 // Configuration initialization
11821 config = config || {};
11822
11823 // Parent constructor
11824 OO.ui.ButtonGroupWidget.parent.call( this, config );
11825
11826 // Mixin constructors
11827 OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
11828
11829 // Initialization
11830 this.$element.addClass( 'oo-ui-buttonGroupWidget' );
11831 if ( Array.isArray( config.items ) ) {
11832 this.addItems( config.items );
11833 }
11834 };
11835
11836 /* Setup */
11837
11838 OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget );
11839 OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.mixin.GroupElement );
11840
11841 /**
11842 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
11843 * feels, and functionality can be customized via the class’s configuration options
11844 * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
11845 * and examples.
11846 *
11847 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
11848 *
11849 * @example
11850 * // A button widget
11851 * var button = new OO.ui.ButtonWidget( {
11852 * label: 'Button with Icon',
11853 * icon: 'remove',
11854 * iconTitle: 'Remove'
11855 * } );
11856 * $( 'body' ).append( button.$element );
11857 *
11858 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
11859 *
11860 * @class
11861 * @extends OO.ui.Widget
11862 * @mixins OO.ui.mixin.ButtonElement
11863 * @mixins OO.ui.mixin.IconElement
11864 * @mixins OO.ui.mixin.IndicatorElement
11865 * @mixins OO.ui.mixin.LabelElement
11866 * @mixins OO.ui.mixin.TitledElement
11867 * @mixins OO.ui.mixin.FlaggedElement
11868 * @mixins OO.ui.mixin.TabIndexedElement
11869 *
11870 * @constructor
11871 * @param {Object} [config] Configuration options
11872 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
11873 * @cfg {string} [target] The frame or window in which to open the hyperlink.
11874 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
11875 */
11876 OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
11877 // Configuration initialization
11878 config = config || {};
11879
11880 // Parent constructor
11881 OO.ui.ButtonWidget.parent.call( this, config );
11882
11883 // Mixin constructors
11884 OO.ui.mixin.ButtonElement.call( this, config );
11885 OO.ui.mixin.IconElement.call( this, config );
11886 OO.ui.mixin.IndicatorElement.call( this, config );
11887 OO.ui.mixin.LabelElement.call( this, config );
11888 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
11889 OO.ui.mixin.FlaggedElement.call( this, config );
11890 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
11891
11892 // Properties
11893 this.href = null;
11894 this.target = null;
11895 this.noFollow = false;
11896
11897 // Events
11898 this.connect( this, { disable: 'onDisable' } );
11899
11900 // Initialization
11901 this.$button.append( this.$icon, this.$label, this.$indicator );
11902 this.$element
11903 .addClass( 'oo-ui-buttonWidget' )
11904 .append( this.$button );
11905 this.setHref( config.href );
11906 this.setTarget( config.target );
11907 this.setNoFollow( config.noFollow );
11908 };
11909
11910 /* Setup */
11911
11912 OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget );
11913 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.ButtonElement );
11914 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.IconElement );
11915 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.IndicatorElement );
11916 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.LabelElement );
11917 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.TitledElement );
11918 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.FlaggedElement );
11919 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.TabIndexedElement );
11920
11921 /* Methods */
11922
11923 /**
11924 * @inheritdoc
11925 */
11926 OO.ui.ButtonWidget.prototype.onMouseDown = function ( e ) {
11927 if ( !this.isDisabled() ) {
11928 // Remove the tab-index while the button is down to prevent the button from stealing focus
11929 this.$button.removeAttr( 'tabindex' );
11930 }
11931
11932 return OO.ui.mixin.ButtonElement.prototype.onMouseDown.call( this, e );
11933 };
11934
11935 /**
11936 * @inheritdoc
11937 */
11938 OO.ui.ButtonWidget.prototype.onMouseUp = function ( e ) {
11939 if ( !this.isDisabled() ) {
11940 // Restore the tab-index after the button is up to restore the button's accessibility
11941 this.$button.attr( 'tabindex', this.tabIndex );
11942 }
11943
11944 return OO.ui.mixin.ButtonElement.prototype.onMouseUp.call( this, e );
11945 };
11946
11947 /**
11948 * Get hyperlink location.
11949 *
11950 * @return {string} Hyperlink location
11951 */
11952 OO.ui.ButtonWidget.prototype.getHref = function () {
11953 return this.href;
11954 };
11955
11956 /**
11957 * Get hyperlink target.
11958 *
11959 * @return {string} Hyperlink target
11960 */
11961 OO.ui.ButtonWidget.prototype.getTarget = function () {
11962 return this.target;
11963 };
11964
11965 /**
11966 * Get search engine traversal hint.
11967 *
11968 * @return {boolean} Whether search engines should avoid traversing this hyperlink
11969 */
11970 OO.ui.ButtonWidget.prototype.getNoFollow = function () {
11971 return this.noFollow;
11972 };
11973
11974 /**
11975 * Set hyperlink location.
11976 *
11977 * @param {string|null} href Hyperlink location, null to remove
11978 */
11979 OO.ui.ButtonWidget.prototype.setHref = function ( href ) {
11980 href = typeof href === 'string' ? href : null;
11981
11982 if ( href !== this.href ) {
11983 this.href = href;
11984 this.updateHref();
11985 }
11986
11987 return this;
11988 };
11989
11990 /**
11991 * Update the `href` attribute, in case of changes to href or
11992 * disabled state.
11993 *
11994 * @private
11995 * @chainable
11996 */
11997 OO.ui.ButtonWidget.prototype.updateHref = function () {
11998 if ( this.href !== null && !this.isDisabled() ) {
11999 this.$button.attr( 'href', this.href );
12000 } else {
12001 this.$button.removeAttr( 'href' );
12002 }
12003
12004 return this;
12005 };
12006
12007 /**
12008 * Handle disable events.
12009 *
12010 * @private
12011 * @param {boolean} disabled Element is disabled
12012 */
12013 OO.ui.ButtonWidget.prototype.onDisable = function () {
12014 this.updateHref();
12015 };
12016
12017 /**
12018 * Set hyperlink target.
12019 *
12020 * @param {string|null} target Hyperlink target, null to remove
12021 */
12022 OO.ui.ButtonWidget.prototype.setTarget = function ( target ) {
12023 target = typeof target === 'string' ? target : null;
12024
12025 if ( target !== this.target ) {
12026 this.target = target;
12027 if ( target !== null ) {
12028 this.$button.attr( 'target', target );
12029 } else {
12030 this.$button.removeAttr( 'target' );
12031 }
12032 }
12033
12034 return this;
12035 };
12036
12037 /**
12038 * Set search engine traversal hint.
12039 *
12040 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
12041 */
12042 OO.ui.ButtonWidget.prototype.setNoFollow = function ( noFollow ) {
12043 noFollow = typeof noFollow === 'boolean' ? noFollow : true;
12044
12045 if ( noFollow !== this.noFollow ) {
12046 this.noFollow = noFollow;
12047 if ( noFollow ) {
12048 this.$button.attr( 'rel', 'nofollow' );
12049 } else {
12050 this.$button.removeAttr( 'rel' );
12051 }
12052 }
12053
12054 return this;
12055 };
12056
12057 /**
12058 * An ActionWidget is a {@link OO.ui.ButtonWidget button widget} that executes an action.
12059 * Action widgets are used with OO.ui.ActionSet, which manages the behavior and availability
12060 * of the actions.
12061 *
12062 * Both actions and action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
12063 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information
12064 * and examples.
12065 *
12066 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
12067 *
12068 * @class
12069 * @extends OO.ui.ButtonWidget
12070 * @mixins OO.ui.mixin.PendingElement
12071 *
12072 * @constructor
12073 * @param {Object} [config] Configuration options
12074 * @cfg {string} [action] Symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
12075 * @cfg {string[]} [modes] Symbolic names of the modes (e.g., ‘edit’ or ‘read’) in which the action
12076 * should be made available. See the action set's {@link OO.ui.ActionSet#setMode setMode} method
12077 * for more information about setting modes.
12078 * @cfg {boolean} [framed=false] Render the action button with a frame
12079 */
12080 OO.ui.ActionWidget = function OoUiActionWidget( config ) {
12081 // Configuration initialization
12082 config = $.extend( { framed: false }, config );
12083
12084 // Parent constructor
12085 OO.ui.ActionWidget.parent.call( this, config );
12086
12087 // Mixin constructors
12088 OO.ui.mixin.PendingElement.call( this, config );
12089
12090 // Properties
12091 this.action = config.action || '';
12092 this.modes = config.modes || [];
12093 this.width = 0;
12094 this.height = 0;
12095
12096 // Initialization
12097 this.$element.addClass( 'oo-ui-actionWidget' );
12098 };
12099
12100 /* Setup */
12101
12102 OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget );
12103 OO.mixinClass( OO.ui.ActionWidget, OO.ui.mixin.PendingElement );
12104
12105 /* Events */
12106
12107 /**
12108 * A resize event is emitted when the size of the widget changes.
12109 *
12110 * @event resize
12111 */
12112
12113 /* Methods */
12114
12115 /**
12116 * Check if the action is configured to be available in the specified `mode`.
12117 *
12118 * @param {string} mode Name of mode
12119 * @return {boolean} The action is configured with the mode
12120 */
12121 OO.ui.ActionWidget.prototype.hasMode = function ( mode ) {
12122 return this.modes.indexOf( mode ) !== -1;
12123 };
12124
12125 /**
12126 * Get the symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
12127 *
12128 * @return {string}
12129 */
12130 OO.ui.ActionWidget.prototype.getAction = function () {
12131 return this.action;
12132 };
12133
12134 /**
12135 * Get the symbolic name of the mode or modes for which the action is configured to be available.
12136 *
12137 * The current mode is set with the action set's {@link OO.ui.ActionSet#setMode setMode} method.
12138 * Only actions that are configured to be avaiable in the current mode will be visible. All other actions
12139 * are hidden.
12140 *
12141 * @return {string[]}
12142 */
12143 OO.ui.ActionWidget.prototype.getModes = function () {
12144 return this.modes.slice();
12145 };
12146
12147 /**
12148 * Emit a resize event if the size has changed.
12149 *
12150 * @private
12151 * @chainable
12152 */
12153 OO.ui.ActionWidget.prototype.propagateResize = function () {
12154 var width, height;
12155
12156 if ( this.isElementAttached() ) {
12157 width = this.$element.width();
12158 height = this.$element.height();
12159
12160 if ( width !== this.width || height !== this.height ) {
12161 this.width = width;
12162 this.height = height;
12163 this.emit( 'resize' );
12164 }
12165 }
12166
12167 return this;
12168 };
12169
12170 /**
12171 * @inheritdoc
12172 */
12173 OO.ui.ActionWidget.prototype.setIcon = function () {
12174 // Mixin method
12175 OO.ui.mixin.IconElement.prototype.setIcon.apply( this, arguments );
12176 this.propagateResize();
12177
12178 return this;
12179 };
12180
12181 /**
12182 * @inheritdoc
12183 */
12184 OO.ui.ActionWidget.prototype.setLabel = function () {
12185 // Mixin method
12186 OO.ui.mixin.LabelElement.prototype.setLabel.apply( this, arguments );
12187 this.propagateResize();
12188
12189 return this;
12190 };
12191
12192 /**
12193 * @inheritdoc
12194 */
12195 OO.ui.ActionWidget.prototype.setFlags = function () {
12196 // Mixin method
12197 OO.ui.mixin.FlaggedElement.prototype.setFlags.apply( this, arguments );
12198 this.propagateResize();
12199
12200 return this;
12201 };
12202
12203 /**
12204 * @inheritdoc
12205 */
12206 OO.ui.ActionWidget.prototype.clearFlags = function () {
12207 // Mixin method
12208 OO.ui.mixin.FlaggedElement.prototype.clearFlags.apply( this, arguments );
12209 this.propagateResize();
12210
12211 return this;
12212 };
12213
12214 /**
12215 * Toggle the visibility of the action button.
12216 *
12217 * @param {boolean} [show] Show button, omit to toggle visibility
12218 * @chainable
12219 */
12220 OO.ui.ActionWidget.prototype.toggle = function () {
12221 // Parent method
12222 OO.ui.ActionWidget.parent.prototype.toggle.apply( this, arguments );
12223 this.propagateResize();
12224
12225 return this;
12226 };
12227
12228 /**
12229 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
12230 * which is used to display additional information or options.
12231 *
12232 * @example
12233 * // Example of a popup button.
12234 * var popupButton = new OO.ui.PopupButtonWidget( {
12235 * label: 'Popup button with options',
12236 * icon: 'menu',
12237 * popup: {
12238 * $content: $( '<p>Additional options here.</p>' ),
12239 * padded: true,
12240 * align: 'force-left'
12241 * }
12242 * } );
12243 * // Append the button to the DOM.
12244 * $( 'body' ).append( popupButton.$element );
12245 *
12246 * @class
12247 * @extends OO.ui.ButtonWidget
12248 * @mixins OO.ui.mixin.PopupElement
12249 *
12250 * @constructor
12251 * @param {Object} [config] Configuration options
12252 */
12253 OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
12254 // Parent constructor
12255 OO.ui.PopupButtonWidget.parent.call( this, config );
12256
12257 // Mixin constructors
12258 OO.ui.mixin.PopupElement.call( this, config );
12259
12260 // Events
12261 this.connect( this, { click: 'onAction' } );
12262
12263 // Initialization
12264 this.$element
12265 .addClass( 'oo-ui-popupButtonWidget' )
12266 .attr( 'aria-haspopup', 'true' )
12267 .append( this.popup.$element );
12268 };
12269
12270 /* Setup */
12271
12272 OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
12273 OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.mixin.PopupElement );
12274
12275 /* Methods */
12276
12277 /**
12278 * Handle the button action being triggered.
12279 *
12280 * @private
12281 */
12282 OO.ui.PopupButtonWidget.prototype.onAction = function () {
12283 this.popup.toggle();
12284 };
12285
12286 /**
12287 * ToggleButtons are buttons that have a state (‘on’ or ‘off’) that is represented by a
12288 * Boolean value. Like other {@link OO.ui.ButtonWidget buttons}, toggle buttons can be
12289 * configured with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators},
12290 * {@link OO.ui.mixin.TitledElement titles}, {@link OO.ui.mixin.FlaggedElement styling flags},
12291 * and {@link OO.ui.mixin.LabelElement labels}. Please see
12292 * the [OOjs UI documentation][1] on MediaWiki for more information.
12293 *
12294 * @example
12295 * // Toggle buttons in the 'off' and 'on' state.
12296 * var toggleButton1 = new OO.ui.ToggleButtonWidget( {
12297 * label: 'Toggle Button off'
12298 * } );
12299 * var toggleButton2 = new OO.ui.ToggleButtonWidget( {
12300 * label: 'Toggle Button on',
12301 * value: true
12302 * } );
12303 * // Append the buttons to the DOM.
12304 * $( 'body' ).append( toggleButton1.$element, toggleButton2.$element );
12305 *
12306 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Toggle_buttons
12307 *
12308 * @class
12309 * @extends OO.ui.ToggleWidget
12310 * @mixins OO.ui.mixin.ButtonElement
12311 * @mixins OO.ui.mixin.IconElement
12312 * @mixins OO.ui.mixin.IndicatorElement
12313 * @mixins OO.ui.mixin.LabelElement
12314 * @mixins OO.ui.mixin.TitledElement
12315 * @mixins OO.ui.mixin.FlaggedElement
12316 * @mixins OO.ui.mixin.TabIndexedElement
12317 *
12318 * @constructor
12319 * @param {Object} [config] Configuration options
12320 * @cfg {boolean} [value=false] The toggle button’s initial on/off
12321 * state. By default, the button is in the 'off' state.
12322 */
12323 OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) {
12324 // Configuration initialization
12325 config = config || {};
12326
12327 // Parent constructor
12328 OO.ui.ToggleButtonWidget.parent.call( this, config );
12329
12330 // Mixin constructors
12331 OO.ui.mixin.ButtonElement.call( this, config );
12332 OO.ui.mixin.IconElement.call( this, config );
12333 OO.ui.mixin.IndicatorElement.call( this, config );
12334 OO.ui.mixin.LabelElement.call( this, config );
12335 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
12336 OO.ui.mixin.FlaggedElement.call( this, config );
12337 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
12338
12339 // Events
12340 this.connect( this, { click: 'onAction' } );
12341
12342 // Initialization
12343 this.$button.append( this.$icon, this.$label, this.$indicator );
12344 this.$element
12345 .addClass( 'oo-ui-toggleButtonWidget' )
12346 .append( this.$button );
12347 };
12348
12349 /* Setup */
12350
12351 OO.inheritClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget );
12352 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.ButtonElement );
12353 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.IconElement );
12354 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.IndicatorElement );
12355 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.LabelElement );
12356 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.TitledElement );
12357 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.FlaggedElement );
12358 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.TabIndexedElement );
12359
12360 /* Methods */
12361
12362 /**
12363 * Handle the button action being triggered.
12364 *
12365 * @private
12366 */
12367 OO.ui.ToggleButtonWidget.prototype.onAction = function () {
12368 this.setValue( !this.value );
12369 };
12370
12371 /**
12372 * @inheritdoc
12373 */
12374 OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
12375 value = !!value;
12376 if ( value !== this.value ) {
12377 // Might be called from parent constructor before ButtonElement constructor
12378 if ( this.$button ) {
12379 this.$button.attr( 'aria-pressed', value.toString() );
12380 }
12381 this.setActive( value );
12382 }
12383
12384 // Parent method
12385 OO.ui.ToggleButtonWidget.parent.prototype.setValue.call( this, value );
12386
12387 return this;
12388 };
12389
12390 /**
12391 * @inheritdoc
12392 */
12393 OO.ui.ToggleButtonWidget.prototype.setButtonElement = function ( $button ) {
12394 if ( this.$button ) {
12395 this.$button.removeAttr( 'aria-pressed' );
12396 }
12397 OO.ui.mixin.ButtonElement.prototype.setButtonElement.call( this, $button );
12398 this.$button.attr( 'aria-pressed', this.value.toString() );
12399 };
12400
12401 /**
12402 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
12403 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
12404 * users can interact with it.
12405 *
12406 * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
12407 * OO.ui.DropdownInputWidget instead.
12408 *
12409 * @example
12410 * // Example: A DropdownWidget with a menu that contains three options
12411 * var dropDown = new OO.ui.DropdownWidget( {
12412 * label: 'Dropdown menu: Select a menu option',
12413 * menu: {
12414 * items: [
12415 * new OO.ui.MenuOptionWidget( {
12416 * data: 'a',
12417 * label: 'First'
12418 * } ),
12419 * new OO.ui.MenuOptionWidget( {
12420 * data: 'b',
12421 * label: 'Second'
12422 * } ),
12423 * new OO.ui.MenuOptionWidget( {
12424 * data: 'c',
12425 * label: 'Third'
12426 * } )
12427 * ]
12428 * }
12429 * } );
12430 *
12431 * $( 'body' ).append( dropDown.$element );
12432 *
12433 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
12434 *
12435 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
12436 *
12437 * @class
12438 * @extends OO.ui.Widget
12439 * @mixins OO.ui.mixin.IconElement
12440 * @mixins OO.ui.mixin.IndicatorElement
12441 * @mixins OO.ui.mixin.LabelElement
12442 * @mixins OO.ui.mixin.TitledElement
12443 * @mixins OO.ui.mixin.TabIndexedElement
12444 *
12445 * @constructor
12446 * @param {Object} [config] Configuration options
12447 * @cfg {Object} [menu] Configuration options to pass to menu widget
12448 */
12449 OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
12450 // Configuration initialization
12451 config = $.extend( { indicator: 'down' }, config );
12452
12453 // Parent constructor
12454 OO.ui.DropdownWidget.parent.call( this, config );
12455
12456 // Properties (must be set before TabIndexedElement constructor call)
12457 this.$handle = this.$( '<span>' );
12458
12459 // Mixin constructors
12460 OO.ui.mixin.IconElement.call( this, config );
12461 OO.ui.mixin.IndicatorElement.call( this, config );
12462 OO.ui.mixin.LabelElement.call( this, config );
12463 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
12464 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
12465
12466 // Properties
12467 this.menu = new OO.ui.MenuSelectWidget( $.extend( { widget: this }, config.menu ) );
12468
12469 // Events
12470 this.$handle.on( {
12471 click: this.onClick.bind( this ),
12472 keypress: this.onKeyPress.bind( this )
12473 } );
12474 this.menu.connect( this, { select: 'onMenuSelect' } );
12475
12476 // Initialization
12477 this.$handle
12478 .addClass( 'oo-ui-dropdownWidget-handle' )
12479 .append( this.$icon, this.$label, this.$indicator );
12480 this.$element
12481 .addClass( 'oo-ui-dropdownWidget' )
12482 .append( this.$handle, this.menu.$element );
12483 };
12484
12485 /* Setup */
12486
12487 OO.inheritClass( OO.ui.DropdownWidget, OO.ui.Widget );
12488 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.IconElement );
12489 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.IndicatorElement );
12490 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.LabelElement );
12491 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.TitledElement );
12492 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.TabIndexedElement );
12493
12494 /* Methods */
12495
12496 /**
12497 * Get the menu.
12498 *
12499 * @return {OO.ui.MenuSelectWidget} Menu of widget
12500 */
12501 OO.ui.DropdownWidget.prototype.getMenu = function () {
12502 return this.menu;
12503 };
12504
12505 /**
12506 * Handles menu select events.
12507 *
12508 * @private
12509 * @param {OO.ui.MenuOptionWidget} item Selected menu item
12510 */
12511 OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
12512 var selectedLabel;
12513
12514 if ( !item ) {
12515 this.setLabel( null );
12516 return;
12517 }
12518
12519 selectedLabel = item.getLabel();
12520
12521 // If the label is a DOM element, clone it, because setLabel will append() it
12522 if ( selectedLabel instanceof jQuery ) {
12523 selectedLabel = selectedLabel.clone();
12524 }
12525
12526 this.setLabel( selectedLabel );
12527 };
12528
12529 /**
12530 * Handle mouse click events.
12531 *
12532 * @private
12533 * @param {jQuery.Event} e Mouse click event
12534 */
12535 OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
12536 if ( !this.isDisabled() && e.which === 1 ) {
12537 this.menu.toggle();
12538 }
12539 return false;
12540 };
12541
12542 /**
12543 * Handle key press events.
12544 *
12545 * @private
12546 * @param {jQuery.Event} e Key press event
12547 */
12548 OO.ui.DropdownWidget.prototype.onKeyPress = function ( e ) {
12549 if ( !this.isDisabled() &&
12550 ( ( e.which === OO.ui.Keys.SPACE && !this.menu.isVisible() ) || e.which === OO.ui.Keys.ENTER )
12551 ) {
12552 this.menu.toggle();
12553 return false;
12554 }
12555 };
12556
12557 /**
12558 * SelectFileWidgets allow for selecting files, using the HTML5 File API. These
12559 * widgets can be configured with {@link OO.ui.mixin.IconElement icons} and {@link
12560 * OO.ui.mixin.IndicatorElement indicators}.
12561 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
12562 *
12563 * @example
12564 * // Example of a file select widget
12565 * var selectFile = new OO.ui.SelectFileWidget();
12566 * $( 'body' ).append( selectFile.$element );
12567 *
12568 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets
12569 *
12570 * @class
12571 * @extends OO.ui.Widget
12572 * @mixins OO.ui.mixin.IconElement
12573 * @mixins OO.ui.mixin.IndicatorElement
12574 * @mixins OO.ui.mixin.PendingElement
12575 * @mixins OO.ui.mixin.LabelElement
12576 * @mixins OO.ui.mixin.TabIndexedElement
12577 *
12578 * @constructor
12579 * @param {Object} [config] Configuration options
12580 * @cfg {string[]|null} [accept=null] MIME types to accept. null accepts all types.
12581 * @cfg {string} [placeholder] Text to display when no file is selected.
12582 * @cfg {string} [notsupported] Text to display when file support is missing in the browser.
12583 * @cfg {boolean} [droppable=true] Whether to accept files by drag and drop.
12584 */
12585 OO.ui.SelectFileWidget = function OoUiSelectFileWidget( config ) {
12586 var dragHandler;
12587
12588 // Configuration initialization
12589 config = $.extend( {
12590 accept: null,
12591 placeholder: OO.ui.msg( 'ooui-selectfile-placeholder' ),
12592 notsupported: OO.ui.msg( 'ooui-selectfile-not-supported' ),
12593 droppable: true
12594 }, config );
12595
12596 // Parent constructor
12597 OO.ui.SelectFileWidget.parent.call( this, config );
12598
12599 // Properties (must be set before TabIndexedElement constructor call)
12600 this.$handle = $( '<span>' );
12601
12602 // Mixin constructors
12603 OO.ui.mixin.IconElement.call( this, config );
12604 OO.ui.mixin.IndicatorElement.call( this, config );
12605 OO.ui.mixin.PendingElement.call( this, $.extend( {}, config, { $pending: this.$handle } ) );
12606 OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, { autoFitLabel: true } ) );
12607 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
12608
12609 // Properties
12610 this.isSupported = this.constructor.static.isSupported();
12611 this.currentFile = null;
12612 if ( Array.isArray( config.accept ) ) {
12613 this.accept = config.accept;
12614 } else {
12615 this.accept = null;
12616 }
12617 this.placeholder = config.placeholder;
12618 this.notsupported = config.notsupported;
12619 this.onFileSelectedHandler = this.onFileSelected.bind( this );
12620
12621 this.clearButton = new OO.ui.ButtonWidget( {
12622 classes: [ 'oo-ui-selectFileWidget-clearButton' ],
12623 framed: false,
12624 icon: 'remove',
12625 disabled: this.disabled
12626 } );
12627
12628 // Events
12629 this.$handle.on( {
12630 keypress: this.onKeyPress.bind( this )
12631 } );
12632 this.clearButton.connect( this, {
12633 click: 'onClearClick'
12634 } );
12635 if ( config.droppable ) {
12636 dragHandler = this.onDragEnterOrOver.bind( this );
12637 this.$handle.on( {
12638 dragenter: dragHandler,
12639 dragover: dragHandler,
12640 dragleave: this.onDragLeave.bind( this ),
12641 drop: this.onDrop.bind( this )
12642 } );
12643 }
12644
12645 // Initialization
12646 this.addInput();
12647 this.updateUI();
12648 this.$label.addClass( 'oo-ui-selectFileWidget-label' );
12649 this.$handle
12650 .addClass( 'oo-ui-selectFileWidget-handle' )
12651 .append( this.$icon, this.$label, this.clearButton.$element, this.$indicator );
12652 this.$element
12653 .addClass( 'oo-ui-selectFileWidget' )
12654 .append( this.$handle );
12655 if ( config.droppable ) {
12656 this.$element.addClass( 'oo-ui-selectFileWidget-droppable' );
12657 }
12658 };
12659
12660 /* Setup */
12661
12662 OO.inheritClass( OO.ui.SelectFileWidget, OO.ui.Widget );
12663 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.IconElement );
12664 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.IndicatorElement );
12665 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.PendingElement );
12666 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.LabelElement );
12667 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.TabIndexedElement );
12668
12669 /* Static Properties */
12670
12671 /**
12672 * Check if this widget is supported
12673 *
12674 * @static
12675 * @return {boolean}
12676 */
12677 OO.ui.SelectFileWidget.static.isSupported = function () {
12678 var $input;
12679 if ( OO.ui.SelectFileWidget.static.isSupportedCache === null ) {
12680 $input = $( '<input type="file">' );
12681 OO.ui.SelectFileWidget.static.isSupportedCache = $input[0].files !== undefined;
12682 }
12683 return OO.ui.SelectFileWidget.static.isSupportedCache;
12684 };
12685
12686 OO.ui.SelectFileWidget.static.isSupportedCache = null;
12687
12688 /* Events */
12689
12690 /**
12691 * @event change
12692 *
12693 * A change event is emitted when the on/off state of the toggle changes.
12694 *
12695 * @param {File|null} value New value
12696 */
12697
12698 /* Methods */
12699
12700 /**
12701 * Get the current value of the field
12702 *
12703 * @return {File|null}
12704 */
12705 OO.ui.SelectFileWidget.prototype.getValue = function () {
12706 return this.currentFile;
12707 };
12708
12709 /**
12710 * Set the current value of the field
12711 *
12712 * @param {File|null} file File to select
12713 */
12714 OO.ui.SelectFileWidget.prototype.setValue = function ( file ) {
12715 if ( this.currentFile !== file ) {
12716 this.currentFile = file;
12717 this.updateUI();
12718 this.emit( 'change', this.currentFile );
12719 }
12720 };
12721
12722 /**
12723 * Update the user interface when a file is selected or unselected
12724 *
12725 * @protected
12726 */
12727 OO.ui.SelectFileWidget.prototype.updateUI = function () {
12728 if ( !this.isSupported ) {
12729 this.$element.addClass( 'oo-ui-selectFileWidget-notsupported' );
12730 this.$element.removeClass( 'oo-ui-selectFileWidget-empty' );
12731 this.setLabel( this.notsupported );
12732 } else if ( this.currentFile ) {
12733 this.$element.removeClass( 'oo-ui-selectFileWidget-empty' );
12734 this.setLabel( this.currentFile.name +
12735 ( this.currentFile.type !== '' ? OO.ui.msg( 'ooui-semicolon-separator' ) + this.currentFile.type : '' )
12736 );
12737 } else {
12738 this.$element.addClass( 'oo-ui-selectFileWidget-empty' );
12739 this.setLabel( this.placeholder );
12740 }
12741
12742 if ( this.$input ) {
12743 this.$input.attr( 'title', this.getLabel() );
12744 }
12745 };
12746
12747 /**
12748 * Add the input to the handle
12749 *
12750 * @private
12751 */
12752 OO.ui.SelectFileWidget.prototype.addInput = function () {
12753 if ( this.$input ) {
12754 this.$input.remove();
12755 }
12756
12757 if ( !this.isSupported ) {
12758 this.$input = null;
12759 return;
12760 }
12761
12762 this.$input = $( '<input type="file">' );
12763 this.$input.on( 'change', this.onFileSelectedHandler );
12764 this.$input.attr( {
12765 tabindex: -1,
12766 title: this.getLabel()
12767 } );
12768 if ( this.accept ) {
12769 this.$input.attr( 'accept', this.accept.join( ', ' ) );
12770 }
12771 this.$handle.append( this.$input );
12772 };
12773
12774 /**
12775 * Determine if we should accept this file
12776 *
12777 * @private
12778 * @param {File} file
12779 * @return {boolean}
12780 */
12781 OO.ui.SelectFileWidget.prototype.isFileAcceptable = function ( file ) {
12782 var i, mime, mimeTest;
12783
12784 if ( !this.accept || file.type === '' ) {
12785 return true;
12786 }
12787
12788 mime = file.type;
12789 for ( i = 0; i < this.accept.length; i++ ) {
12790 mimeTest = this.accept[i];
12791 if ( mimeTest === mime ) {
12792 return true;
12793 } else if ( mimeTest.substr( -2 ) === '/*' ) {
12794 mimeTest = mimeTest.substr( 0, mimeTest.length - 1 );
12795 if ( mime.substr( 0, mimeTest.length ) === mimeTest ) {
12796 return true;
12797 }
12798 }
12799 }
12800
12801 return false;
12802 };
12803
12804 /**
12805 * Handle file selection from the input
12806 *
12807 * @private
12808 * @param {jQuery.Event} e
12809 */
12810 OO.ui.SelectFileWidget.prototype.onFileSelected = function ( e ) {
12811 var file = null;
12812
12813 if ( e.target.files && e.target.files[0] ) {
12814 file = e.target.files[0];
12815 if ( !this.isFileAcceptable( file ) ) {
12816 file = null;
12817 }
12818 }
12819
12820 this.setValue( file );
12821 this.addInput();
12822 };
12823
12824 /**
12825 * Handle clear button click events.
12826 *
12827 * @private
12828 */
12829 OO.ui.SelectFileWidget.prototype.onClearClick = function () {
12830 this.setValue( null );
12831 return false;
12832 };
12833
12834 /**
12835 * Handle key press events.
12836 *
12837 * @private
12838 * @param {jQuery.Event} e Key press event
12839 */
12840 OO.ui.SelectFileWidget.prototype.onKeyPress = function ( e ) {
12841 if ( this.isSupported && !this.isDisabled() && this.$input &&
12842 ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
12843 ) {
12844 this.$input.click();
12845 return false;
12846 }
12847 };
12848
12849 /**
12850 * Handle drag enter and over events
12851 *
12852 * @private
12853 * @param {jQuery.Event} e Drag event
12854 */
12855 OO.ui.SelectFileWidget.prototype.onDragEnterOrOver = function ( e ) {
12856 var file = null,
12857 dt = e.originalEvent.dataTransfer;
12858
12859 e.preventDefault();
12860 e.stopPropagation();
12861
12862 if ( this.isDisabled() || !this.isSupported ) {
12863 this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
12864 dt.dropEffect = 'none';
12865 return false;
12866 }
12867
12868 if ( dt && dt.files && dt.files[0] ) {
12869 file = dt.files[0];
12870 if ( !this.isFileAcceptable( file ) ) {
12871 file = null;
12872 }
12873 } else if ( dt && dt.types && $.inArray( 'Files', dt.types ) ) {
12874 // We know we have files so set 'file' to something truthy, we just
12875 // can't know any details about them.
12876 // * https://bugzilla.mozilla.org/show_bug.cgi?id=640534
12877 file = 'Files exist, but details are unknown';
12878 }
12879 if ( file ) {
12880 this.$element.addClass( 'oo-ui-selectFileWidget-canDrop' );
12881 } else {
12882 this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
12883 dt.dropEffect = 'none';
12884 }
12885
12886 return false;
12887 };
12888
12889 /**
12890 * Handle drag leave events
12891 *
12892 * @private
12893 * @param {jQuery.Event} e Drag event
12894 */
12895 OO.ui.SelectFileWidget.prototype.onDragLeave = function () {
12896 this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
12897 };
12898
12899 /**
12900 * Handle drop events
12901 *
12902 * @private
12903 * @param {jQuery.Event} e Drop event
12904 */
12905 OO.ui.SelectFileWidget.prototype.onDrop = function ( e ) {
12906 var file = null,
12907 dt = e.originalEvent.dataTransfer;
12908
12909 e.preventDefault();
12910 e.stopPropagation();
12911 this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
12912
12913 if ( this.isDisabled() || !this.isSupported ) {
12914 return false;
12915 }
12916
12917 if ( dt && dt.files && dt.files[0] ) {
12918 file = dt.files[0];
12919 if ( !this.isFileAcceptable( file ) ) {
12920 file = null;
12921 }
12922 }
12923 if ( file ) {
12924 this.setValue( file );
12925 }
12926
12927 return false;
12928 };
12929
12930 /**
12931 * @inheritdoc
12932 */
12933 OO.ui.SelectFileWidget.prototype.setDisabled = function ( state ) {
12934 OO.ui.SelectFileWidget.parent.prototype.setDisabled.call( this, state );
12935 if ( this.clearButton ) {
12936 this.clearButton.setDisabled( state );
12937 }
12938 return this;
12939 };
12940
12941 /**
12942 * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
12943 * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
12944 * for a list of icons included in the library.
12945 *
12946 * @example
12947 * // An icon widget with a label
12948 * var myIcon = new OO.ui.IconWidget( {
12949 * icon: 'help',
12950 * iconTitle: 'Help'
12951 * } );
12952 * // Create a label.
12953 * var iconLabel = new OO.ui.LabelWidget( {
12954 * label: 'Help'
12955 * } );
12956 * $( 'body' ).append( myIcon.$element, iconLabel.$element );
12957 *
12958 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
12959 *
12960 * @class
12961 * @extends OO.ui.Widget
12962 * @mixins OO.ui.mixin.IconElement
12963 * @mixins OO.ui.mixin.TitledElement
12964 * @mixins OO.ui.mixin.FlaggedElement
12965 *
12966 * @constructor
12967 * @param {Object} [config] Configuration options
12968 */
12969 OO.ui.IconWidget = function OoUiIconWidget( config ) {
12970 // Configuration initialization
12971 config = config || {};
12972
12973 // Parent constructor
12974 OO.ui.IconWidget.parent.call( this, config );
12975
12976 // Mixin constructors
12977 OO.ui.mixin.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) );
12978 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
12979 OO.ui.mixin.FlaggedElement.call( this, $.extend( {}, config, { $flagged: this.$element } ) );
12980
12981 // Initialization
12982 this.$element.addClass( 'oo-ui-iconWidget' );
12983 };
12984
12985 /* Setup */
12986
12987 OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
12988 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.IconElement );
12989 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.TitledElement );
12990 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.FlaggedElement );
12991
12992 /* Static Properties */
12993
12994 OO.ui.IconWidget.static.tagName = 'span';
12995
12996 /**
12997 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
12998 * attention to the status of an item or to clarify the function of a control. For a list of
12999 * indicators included in the library, please see the [OOjs UI documentation on MediaWiki][1].
13000 *
13001 * @example
13002 * // Example of an indicator widget
13003 * var indicator1 = new OO.ui.IndicatorWidget( {
13004 * indicator: 'alert'
13005 * } );
13006 *
13007 * // Create a fieldset layout to add a label
13008 * var fieldset = new OO.ui.FieldsetLayout();
13009 * fieldset.addItems( [
13010 * new OO.ui.FieldLayout( indicator1, { label: 'An alert indicator:' } )
13011 * ] );
13012 * $( 'body' ).append( fieldset.$element );
13013 *
13014 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
13015 *
13016 * @class
13017 * @extends OO.ui.Widget
13018 * @mixins OO.ui.mixin.IndicatorElement
13019 * @mixins OO.ui.mixin.TitledElement
13020 *
13021 * @constructor
13022 * @param {Object} [config] Configuration options
13023 */
13024 OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
13025 // Configuration initialization
13026 config = config || {};
13027
13028 // Parent constructor
13029 OO.ui.IndicatorWidget.parent.call( this, config );
13030
13031 // Mixin constructors
13032 OO.ui.mixin.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) );
13033 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
13034
13035 // Initialization
13036 this.$element.addClass( 'oo-ui-indicatorWidget' );
13037 };
13038
13039 /* Setup */
13040
13041 OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
13042 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.mixin.IndicatorElement );
13043 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.mixin.TitledElement );
13044
13045 /* Static Properties */
13046
13047 OO.ui.IndicatorWidget.static.tagName = 'span';
13048
13049 /**
13050 * InputWidget is the base class for all input widgets, which
13051 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
13052 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
13053 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
13054 *
13055 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
13056 *
13057 * @abstract
13058 * @class
13059 * @extends OO.ui.Widget
13060 * @mixins OO.ui.mixin.FlaggedElement
13061 * @mixins OO.ui.mixin.TabIndexedElement
13062 *
13063 * @constructor
13064 * @param {Object} [config] Configuration options
13065 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
13066 * @cfg {string} [value=''] The value of the input.
13067 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
13068 * before it is accepted.
13069 */
13070 OO.ui.InputWidget = function OoUiInputWidget( config ) {
13071 // Configuration initialization
13072 config = config || {};
13073
13074 // Parent constructor
13075 OO.ui.InputWidget.parent.call( this, config );
13076
13077 // Properties
13078 this.$input = this.getInputElement( config );
13079 this.value = '';
13080 this.inputFilter = config.inputFilter;
13081
13082 // Mixin constructors
13083 OO.ui.mixin.FlaggedElement.call( this, config );
13084 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$input } ) );
13085
13086 // Events
13087 this.$input.on( 'keydown mouseup cut paste change input select', this.onEdit.bind( this ) );
13088
13089 // Initialization
13090 this.$input
13091 .attr( 'name', config.name )
13092 .prop( 'disabled', this.isDisabled() );
13093 this.$element
13094 .addClass( 'oo-ui-inputWidget' )
13095 .append( this.$input );
13096 this.setValue( config.value );
13097 };
13098
13099 /* Setup */
13100
13101 OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
13102 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.FlaggedElement );
13103 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.TabIndexedElement );
13104
13105 /* Static Properties */
13106
13107 OO.ui.InputWidget.static.supportsSimpleLabel = true;
13108
13109 /* Events */
13110
13111 /**
13112 * @event change
13113 *
13114 * A change event is emitted when the value of the input changes.
13115 *
13116 * @param {string} value
13117 */
13118
13119 /* Methods */
13120
13121 /**
13122 * Get input element.
13123 *
13124 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
13125 * different circumstances. The element must have a `value` property (like form elements).
13126 *
13127 * @protected
13128 * @param {Object} config Configuration options
13129 * @return {jQuery} Input element
13130 */
13131 OO.ui.InputWidget.prototype.getInputElement = function () {
13132 return $( '<input>' );
13133 };
13134
13135 /**
13136 * Handle potentially value-changing events.
13137 *
13138 * @private
13139 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
13140 */
13141 OO.ui.InputWidget.prototype.onEdit = function () {
13142 var widget = this;
13143 if ( !this.isDisabled() ) {
13144 // Allow the stack to clear so the value will be updated
13145 setTimeout( function () {
13146 widget.setValue( widget.$input.val() );
13147 } );
13148 }
13149 };
13150
13151 /**
13152 * Get the value of the input.
13153 *
13154 * @return {string} Input value
13155 */
13156 OO.ui.InputWidget.prototype.getValue = function () {
13157 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
13158 // it, and we won't know unless they're kind enough to trigger a 'change' event.
13159 var value = this.$input.val();
13160 if ( this.value !== value ) {
13161 this.setValue( value );
13162 }
13163 return this.value;
13164 };
13165
13166 /**
13167 * Set the direction of the input, either RTL (right-to-left) or LTR (left-to-right).
13168 *
13169 * @param {boolean} isRTL
13170 * Direction is right-to-left
13171 */
13172 OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
13173 this.$input.prop( 'dir', isRTL ? 'rtl' : 'ltr' );
13174 };
13175
13176 /**
13177 * Set the value of the input.
13178 *
13179 * @param {string} value New value
13180 * @fires change
13181 * @chainable
13182 */
13183 OO.ui.InputWidget.prototype.setValue = function ( value ) {
13184 value = this.cleanUpValue( value );
13185 // Update the DOM if it has changed. Note that with cleanUpValue, it
13186 // is possible for the DOM value to change without this.value changing.
13187 if ( this.$input.val() !== value ) {
13188 this.$input.val( value );
13189 }
13190 if ( this.value !== value ) {
13191 this.value = value;
13192 this.emit( 'change', this.value );
13193 }
13194 return this;
13195 };
13196
13197 /**
13198 * Clean up incoming value.
13199 *
13200 * Ensures value is a string, and converts undefined and null to empty string.
13201 *
13202 * @private
13203 * @param {string} value Original value
13204 * @return {string} Cleaned up value
13205 */
13206 OO.ui.InputWidget.prototype.cleanUpValue = function ( value ) {
13207 if ( value === undefined || value === null ) {
13208 return '';
13209 } else if ( this.inputFilter ) {
13210 return this.inputFilter( String( value ) );
13211 } else {
13212 return String( value );
13213 }
13214 };
13215
13216 /**
13217 * Simulate the behavior of clicking on a label bound to this input. This method is only called by
13218 * {@link OO.ui.LabelWidget LabelWidget} and {@link OO.ui.FieldLayout FieldLayout}. It should not be
13219 * called directly.
13220 */
13221 OO.ui.InputWidget.prototype.simulateLabelClick = function () {
13222 if ( !this.isDisabled() ) {
13223 if ( this.$input.is( ':checkbox, :radio' ) ) {
13224 this.$input.click();
13225 }
13226 if ( this.$input.is( ':input' ) ) {
13227 this.$input[ 0 ].focus();
13228 }
13229 }
13230 };
13231
13232 /**
13233 * @inheritdoc
13234 */
13235 OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
13236 OO.ui.InputWidget.parent.prototype.setDisabled.call( this, state );
13237 if ( this.$input ) {
13238 this.$input.prop( 'disabled', this.isDisabled() );
13239 }
13240 return this;
13241 };
13242
13243 /**
13244 * Focus the input.
13245 *
13246 * @chainable
13247 */
13248 OO.ui.InputWidget.prototype.focus = function () {
13249 this.$input[ 0 ].focus();
13250 return this;
13251 };
13252
13253 /**
13254 * Blur the input.
13255 *
13256 * @chainable
13257 */
13258 OO.ui.InputWidget.prototype.blur = function () {
13259 this.$input[ 0 ].blur();
13260 return this;
13261 };
13262
13263 /**
13264 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
13265 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
13266 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
13267 * HTML `<button/>` (the default) or an HTML `<input/>` tags. See the
13268 * [OOjs UI documentation on MediaWiki] [1] for more information.
13269 *
13270 * @example
13271 * // A ButtonInputWidget rendered as an HTML button, the default.
13272 * var button = new OO.ui.ButtonInputWidget( {
13273 * label: 'Input button',
13274 * icon: 'check',
13275 * value: 'check'
13276 * } );
13277 * $( 'body' ).append( button.$element );
13278 *
13279 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
13280 *
13281 * @class
13282 * @extends OO.ui.InputWidget
13283 * @mixins OO.ui.mixin.ButtonElement
13284 * @mixins OO.ui.mixin.IconElement
13285 * @mixins OO.ui.mixin.IndicatorElement
13286 * @mixins OO.ui.mixin.LabelElement
13287 * @mixins OO.ui.mixin.TitledElement
13288 *
13289 * @constructor
13290 * @param {Object} [config] Configuration options
13291 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
13292 * @cfg {boolean} [useInputTag=false] Use an `<input/>` tag instead of a `<button/>` tag, the default.
13293 * Widgets configured to be an `<input/>` do not support {@link #icon icons} and {@link #indicator indicators},
13294 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
13295 * be set to `true` when there’s need to support IE6 in a form with multiple buttons.
13296 */
13297 OO.ui.ButtonInputWidget = function OoUiButtonInputWidget( config ) {
13298 // Configuration initialization
13299 config = $.extend( { type: 'button', useInputTag: false }, config );
13300
13301 // Properties (must be set before parent constructor, which calls #setValue)
13302 this.useInputTag = config.useInputTag;
13303
13304 // Parent constructor
13305 OO.ui.ButtonInputWidget.parent.call( this, config );
13306
13307 // Mixin constructors
13308 OO.ui.mixin.ButtonElement.call( this, $.extend( {}, config, { $button: this.$input } ) );
13309 OO.ui.mixin.IconElement.call( this, config );
13310 OO.ui.mixin.IndicatorElement.call( this, config );
13311 OO.ui.mixin.LabelElement.call( this, config );
13312 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
13313
13314 // Initialization
13315 if ( !config.useInputTag ) {
13316 this.$input.append( this.$icon, this.$label, this.$indicator );
13317 }
13318 this.$element.addClass( 'oo-ui-buttonInputWidget' );
13319 };
13320
13321 /* Setup */
13322
13323 OO.inheritClass( OO.ui.ButtonInputWidget, OO.ui.InputWidget );
13324 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.ButtonElement );
13325 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IconElement );
13326 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IndicatorElement );
13327 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.LabelElement );
13328 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.TitledElement );
13329
13330 /* Static Properties */
13331
13332 /**
13333 * Disable generating `<label>` elements for buttons. One would very rarely need additional label
13334 * for a button, and it's already a big clickable target, and it causes unexpected rendering.
13335 */
13336 OO.ui.ButtonInputWidget.static.supportsSimpleLabel = false;
13337
13338 /* Methods */
13339
13340 /**
13341 * @inheritdoc
13342 * @protected
13343 */
13344 OO.ui.ButtonInputWidget.prototype.getInputElement = function ( config ) {
13345 var type = [ 'button', 'submit', 'reset' ].indexOf( config.type ) !== -1 ?
13346 config.type :
13347 'button';
13348 return $( '<' + ( config.useInputTag ? 'input' : 'button' ) + ' type="' + type + '">' );
13349 };
13350
13351 /**
13352 * Set label value.
13353 *
13354 * If #useInputTag is `true`, the label is set as the `value` of the `<input/>` tag.
13355 *
13356 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
13357 * text, or `null` for no label
13358 * @chainable
13359 */
13360 OO.ui.ButtonInputWidget.prototype.setLabel = function ( label ) {
13361 OO.ui.mixin.LabelElement.prototype.setLabel.call( this, label );
13362
13363 if ( this.useInputTag ) {
13364 if ( typeof label === 'function' ) {
13365 label = OO.ui.resolveMsg( label );
13366 }
13367 if ( label instanceof jQuery ) {
13368 label = label.text();
13369 }
13370 if ( !label ) {
13371 label = '';
13372 }
13373 this.$input.val( label );
13374 }
13375
13376 return this;
13377 };
13378
13379 /**
13380 * Set the value of the input.
13381 *
13382 * This method is disabled for button inputs configured as {@link #useInputTag <input/> tags}, as
13383 * they do not support {@link #value values}.
13384 *
13385 * @param {string} value New value
13386 * @chainable
13387 */
13388 OO.ui.ButtonInputWidget.prototype.setValue = function ( value ) {
13389 if ( !this.useInputTag ) {
13390 OO.ui.ButtonInputWidget.parent.prototype.setValue.call( this, value );
13391 }
13392 return this;
13393 };
13394
13395 /**
13396 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
13397 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
13398 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
13399 * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
13400 *
13401 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
13402 *
13403 * @example
13404 * // An example of selected, unselected, and disabled checkbox inputs
13405 * var checkbox1=new OO.ui.CheckboxInputWidget( {
13406 * value: 'a',
13407 * selected: true
13408 * } );
13409 * var checkbox2=new OO.ui.CheckboxInputWidget( {
13410 * value: 'b'
13411 * } );
13412 * var checkbox3=new OO.ui.CheckboxInputWidget( {
13413 * value:'c',
13414 * disabled: true
13415 * } );
13416 * // Create a fieldset layout with fields for each checkbox.
13417 * var fieldset = new OO.ui.FieldsetLayout( {
13418 * label: 'Checkboxes'
13419 * } );
13420 * fieldset.addItems( [
13421 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
13422 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
13423 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
13424 * ] );
13425 * $( 'body' ).append( fieldset.$element );
13426 *
13427 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
13428 *
13429 * @class
13430 * @extends OO.ui.InputWidget
13431 *
13432 * @constructor
13433 * @param {Object} [config] Configuration options
13434 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
13435 */
13436 OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
13437 // Configuration initialization
13438 config = config || {};
13439
13440 // Parent constructor
13441 OO.ui.CheckboxInputWidget.parent.call( this, config );
13442
13443 // Initialization
13444 this.$element
13445 .addClass( 'oo-ui-checkboxInputWidget' )
13446 // Required for pretty styling in MediaWiki theme
13447 .append( $( '<span>' ) );
13448 this.setSelected( config.selected !== undefined ? config.selected : false );
13449 };
13450
13451 /* Setup */
13452
13453 OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
13454
13455 /* Methods */
13456
13457 /**
13458 * @inheritdoc
13459 * @protected
13460 */
13461 OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
13462 return $( '<input type="checkbox" />' );
13463 };
13464
13465 /**
13466 * @inheritdoc
13467 */
13468 OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
13469 var widget = this;
13470 if ( !this.isDisabled() ) {
13471 // Allow the stack to clear so the value will be updated
13472 setTimeout( function () {
13473 widget.setSelected( widget.$input.prop( 'checked' ) );
13474 } );
13475 }
13476 };
13477
13478 /**
13479 * Set selection state of this checkbox.
13480 *
13481 * @param {boolean} state `true` for selected
13482 * @chainable
13483 */
13484 OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state ) {
13485 state = !!state;
13486 if ( this.selected !== state ) {
13487 this.selected = state;
13488 this.$input.prop( 'checked', this.selected );
13489 this.emit( 'change', this.selected );
13490 }
13491 return this;
13492 };
13493
13494 /**
13495 * Check if this checkbox is selected.
13496 *
13497 * @return {boolean} Checkbox is selected
13498 */
13499 OO.ui.CheckboxInputWidget.prototype.isSelected = function () {
13500 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
13501 // it, and we won't know unless they're kind enough to trigger a 'change' event.
13502 var selected = this.$input.prop( 'checked' );
13503 if ( this.selected !== selected ) {
13504 this.setSelected( selected );
13505 }
13506 return this.selected;
13507 };
13508
13509 /**
13510 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
13511 * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
13512 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
13513 * more information about input widgets.
13514 *
13515 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
13516 * are no options. If no `value` configuration option is provided, the first option is selected.
13517 * If you need a state representing no value (no option being selected), use a DropdownWidget.
13518 *
13519 * This and OO.ui.RadioSelectInputWidget support the same configuration options.
13520 *
13521 * @example
13522 * // Example: A DropdownInputWidget with three options
13523 * var dropdownInput = new OO.ui.DropdownInputWidget( {
13524 * options: [
13525 * { data: 'a', label: 'First' },
13526 * { data: 'b', label: 'Second'},
13527 * { data: 'c', label: 'Third' }
13528 * ]
13529 * } );
13530 * $( 'body' ).append( dropdownInput.$element );
13531 *
13532 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
13533 *
13534 * @class
13535 * @extends OO.ui.InputWidget
13536 *
13537 * @constructor
13538 * @param {Object} [config] Configuration options
13539 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
13540 */
13541 OO.ui.DropdownInputWidget = function OoUiDropdownInputWidget( config ) {
13542 // Configuration initialization
13543 config = config || {};
13544
13545 // Properties (must be done before parent constructor which calls #setDisabled)
13546 this.dropdownWidget = new OO.ui.DropdownWidget();
13547
13548 // Parent constructor
13549 OO.ui.DropdownInputWidget.parent.call( this, config );
13550
13551 // Events
13552 this.dropdownWidget.getMenu().connect( this, { select: 'onMenuSelect' } );
13553
13554 // Initialization
13555 this.setOptions( config.options || [] );
13556 this.$element
13557 .addClass( 'oo-ui-dropdownInputWidget' )
13558 .append( this.dropdownWidget.$element );
13559 };
13560
13561 /* Setup */
13562
13563 OO.inheritClass( OO.ui.DropdownInputWidget, OO.ui.InputWidget );
13564
13565 /* Methods */
13566
13567 /**
13568 * @inheritdoc
13569 * @protected
13570 */
13571 OO.ui.DropdownInputWidget.prototype.getInputElement = function () {
13572 return $( '<input type="hidden">' );
13573 };
13574
13575 /**
13576 * Handles menu select events.
13577 *
13578 * @private
13579 * @param {OO.ui.MenuOptionWidget} item Selected menu item
13580 */
13581 OO.ui.DropdownInputWidget.prototype.onMenuSelect = function ( item ) {
13582 this.setValue( item.getData() );
13583 };
13584
13585 /**
13586 * @inheritdoc
13587 */
13588 OO.ui.DropdownInputWidget.prototype.setValue = function ( value ) {
13589 value = this.cleanUpValue( value );
13590 this.dropdownWidget.getMenu().selectItemByData( value );
13591 OO.ui.DropdownInputWidget.parent.prototype.setValue.call( this, value );
13592 return this;
13593 };
13594
13595 /**
13596 * @inheritdoc
13597 */
13598 OO.ui.DropdownInputWidget.prototype.setDisabled = function ( state ) {
13599 this.dropdownWidget.setDisabled( state );
13600 OO.ui.DropdownInputWidget.parent.prototype.setDisabled.call( this, state );
13601 return this;
13602 };
13603
13604 /**
13605 * Set the options available for this input.
13606 *
13607 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
13608 * @chainable
13609 */
13610 OO.ui.DropdownInputWidget.prototype.setOptions = function ( options ) {
13611 var
13612 value = this.getValue(),
13613 widget = this;
13614
13615 // Rebuild the dropdown menu
13616 this.dropdownWidget.getMenu()
13617 .clearItems()
13618 .addItems( options.map( function ( opt ) {
13619 var optValue = widget.cleanUpValue( opt.data );
13620 return new OO.ui.MenuOptionWidget( {
13621 data: optValue,
13622 label: opt.label !== undefined ? opt.label : optValue
13623 } );
13624 } ) );
13625
13626 // Restore the previous value, or reset to something sensible
13627 if ( this.dropdownWidget.getMenu().getItemFromData( value ) ) {
13628 // Previous value is still available, ensure consistency with the dropdown
13629 this.setValue( value );
13630 } else {
13631 // No longer valid, reset
13632 if ( options.length ) {
13633 this.setValue( options[ 0 ].data );
13634 }
13635 }
13636
13637 return this;
13638 };
13639
13640 /**
13641 * @inheritdoc
13642 */
13643 OO.ui.DropdownInputWidget.prototype.focus = function () {
13644 this.dropdownWidget.getMenu().toggle( true );
13645 return this;
13646 };
13647
13648 /**
13649 * @inheritdoc
13650 */
13651 OO.ui.DropdownInputWidget.prototype.blur = function () {
13652 this.dropdownWidget.getMenu().toggle( false );
13653 return this;
13654 };
13655
13656 /**
13657 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
13658 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
13659 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
13660 * please see the [OOjs UI documentation on MediaWiki][1].
13661 *
13662 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
13663 *
13664 * @example
13665 * // An example of selected, unselected, and disabled radio inputs
13666 * var radio1 = new OO.ui.RadioInputWidget( {
13667 * value: 'a',
13668 * selected: true
13669 * } );
13670 * var radio2 = new OO.ui.RadioInputWidget( {
13671 * value: 'b'
13672 * } );
13673 * var radio3 = new OO.ui.RadioInputWidget( {
13674 * value: 'c',
13675 * disabled: true
13676 * } );
13677 * // Create a fieldset layout with fields for each radio button.
13678 * var fieldset = new OO.ui.FieldsetLayout( {
13679 * label: 'Radio inputs'
13680 * } );
13681 * fieldset.addItems( [
13682 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
13683 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
13684 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
13685 * ] );
13686 * $( 'body' ).append( fieldset.$element );
13687 *
13688 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
13689 *
13690 * @class
13691 * @extends OO.ui.InputWidget
13692 *
13693 * @constructor
13694 * @param {Object} [config] Configuration options
13695 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
13696 */
13697 OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) {
13698 // Configuration initialization
13699 config = config || {};
13700
13701 // Parent constructor
13702 OO.ui.RadioInputWidget.parent.call( this, config );
13703
13704 // Initialization
13705 this.$element
13706 .addClass( 'oo-ui-radioInputWidget' )
13707 // Required for pretty styling in MediaWiki theme
13708 .append( $( '<span>' ) );
13709 this.setSelected( config.selected !== undefined ? config.selected : false );
13710 };
13711
13712 /* Setup */
13713
13714 OO.inheritClass( OO.ui.RadioInputWidget, OO.ui.InputWidget );
13715
13716 /* Methods */
13717
13718 /**
13719 * @inheritdoc
13720 * @protected
13721 */
13722 OO.ui.RadioInputWidget.prototype.getInputElement = function () {
13723 return $( '<input type="radio" />' );
13724 };
13725
13726 /**
13727 * @inheritdoc
13728 */
13729 OO.ui.RadioInputWidget.prototype.onEdit = function () {
13730 // RadioInputWidget doesn't track its state.
13731 };
13732
13733 /**
13734 * Set selection state of this radio button.
13735 *
13736 * @param {boolean} state `true` for selected
13737 * @chainable
13738 */
13739 OO.ui.RadioInputWidget.prototype.setSelected = function ( state ) {
13740 // RadioInputWidget doesn't track its state.
13741 this.$input.prop( 'checked', state );
13742 return this;
13743 };
13744
13745 /**
13746 * Check if this radio button is selected.
13747 *
13748 * @return {boolean} Radio is selected
13749 */
13750 OO.ui.RadioInputWidget.prototype.isSelected = function () {
13751 return this.$input.prop( 'checked' );
13752 };
13753
13754 /**
13755 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
13756 * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
13757 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
13758 * more information about input widgets.
13759 *
13760 * This and OO.ui.DropdownInputWidget support the same configuration options.
13761 *
13762 * @example
13763 * // Example: A RadioSelectInputWidget with three options
13764 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
13765 * options: [
13766 * { data: 'a', label: 'First' },
13767 * { data: 'b', label: 'Second'},
13768 * { data: 'c', label: 'Third' }
13769 * ]
13770 * } );
13771 * $( 'body' ).append( radioSelectInput.$element );
13772 *
13773 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
13774 *
13775 * @class
13776 * @extends OO.ui.InputWidget
13777 *
13778 * @constructor
13779 * @param {Object} [config] Configuration options
13780 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
13781 */
13782 OO.ui.RadioSelectInputWidget = function OoUiRadioSelectInputWidget( config ) {
13783 // Configuration initialization
13784 config = config || {};
13785
13786 // Properties (must be done before parent constructor which calls #setDisabled)
13787 this.radioSelectWidget = new OO.ui.RadioSelectWidget();
13788
13789 // Parent constructor
13790 OO.ui.RadioSelectInputWidget.parent.call( this, config );
13791
13792 // Events
13793 this.radioSelectWidget.connect( this, { select: 'onMenuSelect' } );
13794
13795 // Initialization
13796 this.setOptions( config.options || [] );
13797 this.$element
13798 .addClass( 'oo-ui-radioSelectInputWidget' )
13799 .append( this.radioSelectWidget.$element );
13800 };
13801
13802 /* Setup */
13803
13804 OO.inheritClass( OO.ui.RadioSelectInputWidget, OO.ui.InputWidget );
13805
13806 /* Static Properties */
13807
13808 OO.ui.RadioSelectInputWidget.static.supportsSimpleLabel = false;
13809
13810 /* Methods */
13811
13812 /**
13813 * @inheritdoc
13814 * @protected
13815 */
13816 OO.ui.RadioSelectInputWidget.prototype.getInputElement = function () {
13817 return $( '<input type="hidden">' );
13818 };
13819
13820 /**
13821 * Handles menu select events.
13822 *
13823 * @private
13824 * @param {OO.ui.RadioOptionWidget} item Selected menu item
13825 */
13826 OO.ui.RadioSelectInputWidget.prototype.onMenuSelect = function ( item ) {
13827 this.setValue( item.getData() );
13828 };
13829
13830 /**
13831 * @inheritdoc
13832 */
13833 OO.ui.RadioSelectInputWidget.prototype.setValue = function ( value ) {
13834 value = this.cleanUpValue( value );
13835 this.radioSelectWidget.selectItemByData( value );
13836 OO.ui.RadioSelectInputWidget.parent.prototype.setValue.call( this, value );
13837 return this;
13838 };
13839
13840 /**
13841 * @inheritdoc
13842 */
13843 OO.ui.RadioSelectInputWidget.prototype.setDisabled = function ( state ) {
13844 this.radioSelectWidget.setDisabled( state );
13845 OO.ui.RadioSelectInputWidget.parent.prototype.setDisabled.call( this, state );
13846 return this;
13847 };
13848
13849 /**
13850 * Set the options available for this input.
13851 *
13852 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
13853 * @chainable
13854 */
13855 OO.ui.RadioSelectInputWidget.prototype.setOptions = function ( options ) {
13856 var
13857 value = this.getValue(),
13858 widget = this;
13859
13860 // Rebuild the radioSelect menu
13861 this.radioSelectWidget
13862 .clearItems()
13863 .addItems( options.map( function ( opt ) {
13864 var optValue = widget.cleanUpValue( opt.data );
13865 return new OO.ui.RadioOptionWidget( {
13866 data: optValue,
13867 label: opt.label !== undefined ? opt.label : optValue
13868 } );
13869 } ) );
13870
13871 // Restore the previous value, or reset to something sensible
13872 if ( this.radioSelectWidget.getItemFromData( value ) ) {
13873 // Previous value is still available, ensure consistency with the radioSelect
13874 this.setValue( value );
13875 } else {
13876 // No longer valid, reset
13877 if ( options.length ) {
13878 this.setValue( options[ 0 ].data );
13879 }
13880 }
13881
13882 return this;
13883 };
13884
13885 /**
13886 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
13887 * size of the field as well as its presentation. In addition, these widgets can be configured
13888 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
13889 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
13890 * which modifies incoming values rather than validating them.
13891 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
13892 *
13893 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
13894 *
13895 * @example
13896 * // Example of a text input widget
13897 * var textInput = new OO.ui.TextInputWidget( {
13898 * value: 'Text input'
13899 * } )
13900 * $( 'body' ).append( textInput.$element );
13901 *
13902 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
13903 *
13904 * @class
13905 * @extends OO.ui.InputWidget
13906 * @mixins OO.ui.mixin.IconElement
13907 * @mixins OO.ui.mixin.IndicatorElement
13908 * @mixins OO.ui.mixin.PendingElement
13909 * @mixins OO.ui.mixin.LabelElement
13910 *
13911 * @constructor
13912 * @param {Object} [config] Configuration options
13913 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password', 'search',
13914 * 'email' or 'url'. Ignored if `multiline` is true.
13915 *
13916 * Some values of `type` result in additional behaviors:
13917 *
13918 * - `search`: implies `icon: 'search'` and `indicator: 'clear'`; when clicked, the indicator
13919 * empties the text field
13920 * @cfg {string} [placeholder] Placeholder text
13921 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
13922 * instruct the browser to focus this widget.
13923 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
13924 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
13925 * @cfg {boolean} [multiline=false] Allow multiple lines of text
13926 * @cfg {number} [rows] If multiline, number of visible lines in textarea. If used with `autosize`,
13927 * specifies minimum number of rows to display.
13928 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
13929 * Use the #maxRows config to specify a maximum number of displayed rows.
13930 * @cfg {boolean} [maxRows] Maximum number of rows to display when #autosize is set to true.
13931 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
13932 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
13933 * the value or placeholder text: `'before'` or `'after'`
13934 * @cfg {boolean} [required=false] Mark the field as required. Implies `indicator: 'required'`.
13935 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
13936 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
13937 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
13938 * (the value must contain only numbers); when RegExp, a regular expression that must match the
13939 * value for it to be considered valid; when Function, a function receiving the value as parameter
13940 * that must return true, or promise resolving to true, for it to be considered valid.
13941 */
13942 OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
13943 // Configuration initialization
13944 config = $.extend( {
13945 type: 'text',
13946 labelPosition: 'after'
13947 }, config );
13948 if ( config.type === 'search' ) {
13949 if ( config.icon === undefined ) {
13950 config.icon = 'search';
13951 }
13952 // indicator: 'clear' is set dynamically later, depending on value
13953 }
13954 if ( config.required ) {
13955 if ( config.indicator === undefined ) {
13956 config.indicator = 'required';
13957 }
13958 }
13959
13960 // Parent constructor
13961 OO.ui.TextInputWidget.parent.call( this, config );
13962
13963 // Mixin constructors
13964 OO.ui.mixin.IconElement.call( this, config );
13965 OO.ui.mixin.IndicatorElement.call( this, config );
13966 OO.ui.mixin.PendingElement.call( this, $.extend( {}, config, { $pending: this.$input } ) );
13967 OO.ui.mixin.LabelElement.call( this, config );
13968
13969 // Properties
13970 this.type = this.getSaneType( config );
13971 this.readOnly = false;
13972 this.multiline = !!config.multiline;
13973 this.autosize = !!config.autosize;
13974 this.minRows = config.rows !== undefined ? config.rows : '';
13975 this.maxRows = config.maxRows || Math.max( 2 * ( this.minRows || 0 ), 10 );
13976 this.validate = null;
13977
13978 // Clone for resizing
13979 if ( this.autosize ) {
13980 this.$clone = this.$input
13981 .clone()
13982 .insertAfter( this.$input )
13983 .attr( 'aria-hidden', 'true' )
13984 .addClass( 'oo-ui-element-hidden' );
13985 }
13986
13987 this.setValidation( config.validate );
13988 this.setLabelPosition( config.labelPosition );
13989
13990 // Events
13991 this.$input.on( {
13992 keypress: this.onKeyPress.bind( this ),
13993 blur: this.onBlur.bind( this )
13994 } );
13995 this.$input.one( {
13996 focus: this.onElementAttach.bind( this )
13997 } );
13998 this.$icon.on( 'mousedown', this.onIconMouseDown.bind( this ) );
13999 this.$indicator.on( 'mousedown', this.onIndicatorMouseDown.bind( this ) );
14000 this.on( 'labelChange', this.updatePosition.bind( this ) );
14001 this.connect( this, {
14002 change: 'onChange',
14003 disable: 'onDisable'
14004 } );
14005
14006 // Initialization
14007 this.$element
14008 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type )
14009 .append( this.$icon, this.$indicator );
14010 this.setReadOnly( !!config.readOnly );
14011 this.updateSearchIndicator();
14012 if ( config.placeholder ) {
14013 this.$input.attr( 'placeholder', config.placeholder );
14014 }
14015 if ( config.maxLength !== undefined ) {
14016 this.$input.attr( 'maxlength', config.maxLength );
14017 }
14018 if ( config.autofocus ) {
14019 this.$input.attr( 'autofocus', 'autofocus' );
14020 }
14021 if ( config.required ) {
14022 this.$input.attr( 'required', 'required' );
14023 this.$input.attr( 'aria-required', 'true' );
14024 }
14025 if ( config.autocomplete === false ) {
14026 this.$input.attr( 'autocomplete', 'off' );
14027 }
14028 if ( this.multiline && config.rows ) {
14029 this.$input.attr( 'rows', config.rows );
14030 }
14031 if ( this.label || config.autosize ) {
14032 this.installParentChangeDetector();
14033 }
14034 };
14035
14036 /* Setup */
14037
14038 OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
14039 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IconElement );
14040 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IndicatorElement );
14041 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.PendingElement );
14042 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.LabelElement );
14043
14044 /* Static Properties */
14045
14046 OO.ui.TextInputWidget.static.validationPatterns = {
14047 'non-empty': /.+/,
14048 integer: /^\d+$/
14049 };
14050
14051 /* Events */
14052
14053 /**
14054 * An `enter` event is emitted when the user presses 'enter' inside the text box.
14055 *
14056 * Not emitted if the input is multiline.
14057 *
14058 * @event enter
14059 */
14060
14061 /* Methods */
14062
14063 /**
14064 * Handle icon mouse down events.
14065 *
14066 * @private
14067 * @param {jQuery.Event} e Mouse down event
14068 * @fires icon
14069 */
14070 OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
14071 if ( e.which === 1 ) {
14072 this.$input[ 0 ].focus();
14073 return false;
14074 }
14075 };
14076
14077 /**
14078 * Handle indicator mouse down events.
14079 *
14080 * @private
14081 * @param {jQuery.Event} e Mouse down event
14082 * @fires indicator
14083 */
14084 OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
14085 if ( e.which === 1 ) {
14086 if ( this.type === 'search' ) {
14087 // Clear the text field
14088 this.setValue( '' );
14089 }
14090 this.$input[ 0 ].focus();
14091 return false;
14092 }
14093 };
14094
14095 /**
14096 * Handle key press events.
14097 *
14098 * @private
14099 * @param {jQuery.Event} e Key press event
14100 * @fires enter If enter key is pressed and input is not multiline
14101 */
14102 OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
14103 if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) {
14104 this.emit( 'enter', e );
14105 }
14106 };
14107
14108 /**
14109 * Handle blur events.
14110 *
14111 * @private
14112 * @param {jQuery.Event} e Blur event
14113 */
14114 OO.ui.TextInputWidget.prototype.onBlur = function () {
14115 this.setValidityFlag();
14116 };
14117
14118 /**
14119 * Handle element attach events.
14120 *
14121 * @private
14122 * @param {jQuery.Event} e Element attach event
14123 */
14124 OO.ui.TextInputWidget.prototype.onElementAttach = function () {
14125 // Any previously calculated size is now probably invalid if we reattached elsewhere
14126 this.valCache = null;
14127 this.adjustSize();
14128 this.positionLabel();
14129 };
14130
14131 /**
14132 * Handle change events.
14133 *
14134 * @param {string} value
14135 * @private
14136 */
14137 OO.ui.TextInputWidget.prototype.onChange = function () {
14138 this.updateSearchIndicator();
14139 this.setValidityFlag();
14140 this.adjustSize();
14141 };
14142
14143 /**
14144 * Handle disable events.
14145 *
14146 * @param {boolean} disabled Element is disabled
14147 * @private
14148 */
14149 OO.ui.TextInputWidget.prototype.onDisable = function () {
14150 this.updateSearchIndicator();
14151 };
14152
14153 /**
14154 * Check if the input is {@link #readOnly read-only}.
14155 *
14156 * @return {boolean}
14157 */
14158 OO.ui.TextInputWidget.prototype.isReadOnly = function () {
14159 return this.readOnly;
14160 };
14161
14162 /**
14163 * Set the {@link #readOnly read-only} state of the input.
14164 *
14165 * @param {boolean} state Make input read-only
14166 * @chainable
14167 */
14168 OO.ui.TextInputWidget.prototype.setReadOnly = function ( state ) {
14169 this.readOnly = !!state;
14170 this.$input.prop( 'readOnly', this.readOnly );
14171 this.updateSearchIndicator();
14172 return this;
14173 };
14174
14175 /**
14176 * Support function for making #onElementAttach work across browsers.
14177 *
14178 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
14179 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
14180 *
14181 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
14182 * first time that the element gets attached to the documented.
14183 */
14184 OO.ui.TextInputWidget.prototype.installParentChangeDetector = function () {
14185 var mutationObserver, onRemove, topmostNode, fakeParentNode,
14186 MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
14187 widget = this;
14188
14189 if ( MutationObserver ) {
14190 // The new way. If only it wasn't so ugly.
14191
14192 if ( this.$element.closest( 'html' ).length ) {
14193 // Widget is attached already, do nothing. This breaks the functionality of this function when
14194 // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
14195 // would require observation of the whole document, which would hurt performance of other,
14196 // more important code.
14197 return;
14198 }
14199
14200 // Find topmost node in the tree
14201 topmostNode = this.$element[0];
14202 while ( topmostNode.parentNode ) {
14203 topmostNode = topmostNode.parentNode;
14204 }
14205
14206 // We have no way to detect the $element being attached somewhere without observing the entire
14207 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
14208 // parent node of $element, and instead detect when $element is removed from it (and thus
14209 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
14210 // doesn't get attached, we end up back here and create the parent.
14211
14212 mutationObserver = new MutationObserver( function ( mutations ) {
14213 var i, j, removedNodes;
14214 for ( i = 0; i < mutations.length; i++ ) {
14215 removedNodes = mutations[ i ].removedNodes;
14216 for ( j = 0; j < removedNodes.length; j++ ) {
14217 if ( removedNodes[ j ] === topmostNode ) {
14218 setTimeout( onRemove, 0 );
14219 return;
14220 }
14221 }
14222 }
14223 } );
14224
14225 onRemove = function () {
14226 // If the node was attached somewhere else, report it
14227 if ( widget.$element.closest( 'html' ).length ) {
14228 widget.onElementAttach();
14229 }
14230 mutationObserver.disconnect();
14231 widget.installParentChangeDetector();
14232 };
14233
14234 // Create a fake parent and observe it
14235 fakeParentNode = $( '<div>' ).append( topmostNode )[0];
14236 mutationObserver.observe( fakeParentNode, { childList: true } );
14237 } else {
14238 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
14239 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
14240 this.$element.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach.bind( this ) );
14241 }
14242 };
14243
14244 /**
14245 * Automatically adjust the size of the text input.
14246 *
14247 * This only affects #multiline inputs that are {@link #autosize autosized}.
14248 *
14249 * @chainable
14250 */
14251 OO.ui.TextInputWidget.prototype.adjustSize = function () {
14252 var scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError, idealHeight;
14253
14254 if ( this.multiline && this.autosize && this.$input.val() !== this.valCache ) {
14255 this.$clone
14256 .val( this.$input.val() )
14257 .attr( 'rows', this.minRows )
14258 // Set inline height property to 0 to measure scroll height
14259 .css( 'height', 0 );
14260
14261 this.$clone.removeClass( 'oo-ui-element-hidden' );
14262
14263 this.valCache = this.$input.val();
14264
14265 scrollHeight = this.$clone[ 0 ].scrollHeight;
14266
14267 // Remove inline height property to measure natural heights
14268 this.$clone.css( 'height', '' );
14269 innerHeight = this.$clone.innerHeight();
14270 outerHeight = this.$clone.outerHeight();
14271
14272 // Measure max rows height
14273 this.$clone
14274 .attr( 'rows', this.maxRows )
14275 .css( 'height', 'auto' )
14276 .val( '' );
14277 maxInnerHeight = this.$clone.innerHeight();
14278
14279 // Difference between reported innerHeight and scrollHeight with no scrollbars present
14280 // Equals 1 on Blink-based browsers and 0 everywhere else
14281 measurementError = maxInnerHeight - this.$clone[ 0 ].scrollHeight;
14282 idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
14283
14284 this.$clone.addClass( 'oo-ui-element-hidden' );
14285
14286 // Only apply inline height when expansion beyond natural height is needed
14287 if ( idealHeight > innerHeight ) {
14288 // Use the difference between the inner and outer height as a buffer
14289 this.$input.css( 'height', idealHeight + ( outerHeight - innerHeight ) );
14290 } else {
14291 this.$input.css( 'height', '' );
14292 }
14293 }
14294 return this;
14295 };
14296
14297 /**
14298 * @inheritdoc
14299 * @protected
14300 */
14301 OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
14302 return config.multiline ?
14303 $( '<textarea>' ) :
14304 $( '<input type="' + this.getSaneType( config ) + '" />' );
14305 };
14306
14307 /**
14308 * Get sanitized value for 'type' for given config.
14309 *
14310 * @param {Object} config Configuration options
14311 * @return {string|null}
14312 * @private
14313 */
14314 OO.ui.TextInputWidget.prototype.getSaneType = function ( config ) {
14315 var type = [ 'text', 'password', 'search', 'email', 'url' ].indexOf( config.type ) !== -1 ?
14316 config.type :
14317 'text';
14318 return config.multiline ? 'multiline' : type;
14319 };
14320
14321 /**
14322 * Check if the input supports multiple lines.
14323 *
14324 * @return {boolean}
14325 */
14326 OO.ui.TextInputWidget.prototype.isMultiline = function () {
14327 return !!this.multiline;
14328 };
14329
14330 /**
14331 * Check if the input automatically adjusts its size.
14332 *
14333 * @return {boolean}
14334 */
14335 OO.ui.TextInputWidget.prototype.isAutosizing = function () {
14336 return !!this.autosize;
14337 };
14338
14339 /**
14340 * Select the entire text of the input.
14341 *
14342 * @chainable
14343 */
14344 OO.ui.TextInputWidget.prototype.select = function () {
14345 this.$input.select();
14346 return this;
14347 };
14348
14349 /**
14350 * Set the validation pattern.
14351 *
14352 * The validation pattern is either a regular expression, a function, or the symbolic name of a
14353 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
14354 * value must contain only numbers).
14355 *
14356 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
14357 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
14358 */
14359 OO.ui.TextInputWidget.prototype.setValidation = function ( validate ) {
14360 if ( validate instanceof RegExp || validate instanceof Function ) {
14361 this.validate = validate;
14362 } else {
14363 this.validate = this.constructor.static.validationPatterns[ validate ] || /.*/;
14364 }
14365 };
14366
14367 /**
14368 * Sets the 'invalid' flag appropriately.
14369 *
14370 * @param {boolean} [isValid] Optionally override validation result
14371 */
14372 OO.ui.TextInputWidget.prototype.setValidityFlag = function ( isValid ) {
14373 var widget = this,
14374 setFlag = function ( valid ) {
14375 if ( !valid ) {
14376 widget.$input.attr( 'aria-invalid', 'true' );
14377 } else {
14378 widget.$input.removeAttr( 'aria-invalid' );
14379 }
14380 widget.setFlags( { invalid: !valid } );
14381 };
14382
14383 if ( isValid !== undefined ) {
14384 setFlag( isValid );
14385 } else {
14386 this.isValid().done( setFlag );
14387 }
14388 };
14389
14390 /**
14391 * Check if a value is valid.
14392 *
14393 * This method returns a promise that resolves with a boolean `true` if the current value is
14394 * considered valid according to the supplied {@link #validate validation pattern}.
14395 *
14396 * @return {jQuery.Promise} A promise that resolves to a boolean `true` if the value is valid.
14397 */
14398 OO.ui.TextInputWidget.prototype.isValid = function () {
14399 if ( this.validate instanceof Function ) {
14400 var result = this.validate( this.getValue() );
14401 if ( $.isFunction( result.promise ) ) {
14402 return result.promise();
14403 } else {
14404 return $.Deferred().resolve( !!result ).promise();
14405 }
14406 } else {
14407 return $.Deferred().resolve( !!this.getValue().match( this.validate ) ).promise();
14408 }
14409 };
14410
14411 /**
14412 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
14413 *
14414 * @param {string} labelPosition Label position, 'before' or 'after'
14415 * @chainable
14416 */
14417 OO.ui.TextInputWidget.prototype.setLabelPosition = function ( labelPosition ) {
14418 this.labelPosition = labelPosition;
14419 this.updatePosition();
14420 return this;
14421 };
14422
14423 /**
14424 * Deprecated alias of #setLabelPosition
14425 *
14426 * @deprecated Use setLabelPosition instead.
14427 */
14428 OO.ui.TextInputWidget.prototype.setPosition =
14429 OO.ui.TextInputWidget.prototype.setLabelPosition;
14430
14431 /**
14432 * Update the position of the inline label.
14433 *
14434 * This method is called by #setLabelPosition, and can also be called on its own if
14435 * something causes the label to be mispositioned.
14436 *
14437 * @chainable
14438 */
14439 OO.ui.TextInputWidget.prototype.updatePosition = function () {
14440 var after = this.labelPosition === 'after';
14441
14442 this.$element
14443 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label && after )
14444 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label && !after );
14445
14446 if ( this.label ) {
14447 this.positionLabel();
14448 }
14449
14450 return this;
14451 };
14452
14453 /**
14454 * Update the 'clear' indicator displayed on type: 'search' text fields, hiding it when the field is
14455 * already empty or when it's not editable.
14456 */
14457 OO.ui.TextInputWidget.prototype.updateSearchIndicator = function () {
14458 if ( this.type === 'search' ) {
14459 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
14460 this.setIndicator( null );
14461 } else {
14462 this.setIndicator( 'clear' );
14463 }
14464 }
14465 };
14466
14467 /**
14468 * Position the label by setting the correct padding on the input.
14469 *
14470 * @private
14471 * @chainable
14472 */
14473 OO.ui.TextInputWidget.prototype.positionLabel = function () {
14474 // Clear old values
14475 this.$input
14476 // Clear old values if present
14477 .css( {
14478 'padding-right': '',
14479 'padding-left': ''
14480 } );
14481
14482 if ( this.label ) {
14483 this.$element.append( this.$label );
14484 } else {
14485 this.$label.detach();
14486 return;
14487 }
14488
14489 var after = this.labelPosition === 'after',
14490 rtl = this.$element.css( 'direction' ) === 'rtl',
14491 property = after === rtl ? 'padding-left' : 'padding-right';
14492
14493 this.$input.css( property, this.$label.outerWidth( true ) );
14494
14495 return this;
14496 };
14497
14498 /**
14499 * ComboBoxWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
14500 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
14501 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
14502 *
14503 * - by typing a value in the text input field. If the value exactly matches the value of a menu
14504 * option, that option will appear to be selected.
14505 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
14506 * input field.
14507 *
14508 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
14509 *
14510 * @example
14511 * // Example: A ComboBoxWidget.
14512 * var comboBox = new OO.ui.ComboBoxWidget( {
14513 * label: 'ComboBoxWidget',
14514 * input: { value: 'Option One' },
14515 * menu: {
14516 * items: [
14517 * new OO.ui.MenuOptionWidget( {
14518 * data: 'Option 1',
14519 * label: 'Option One'
14520 * } ),
14521 * new OO.ui.MenuOptionWidget( {
14522 * data: 'Option 2',
14523 * label: 'Option Two'
14524 * } ),
14525 * new OO.ui.MenuOptionWidget( {
14526 * data: 'Option 3',
14527 * label: 'Option Three'
14528 * } ),
14529 * new OO.ui.MenuOptionWidget( {
14530 * data: 'Option 4',
14531 * label: 'Option Four'
14532 * } ),
14533 * new OO.ui.MenuOptionWidget( {
14534 * data: 'Option 5',
14535 * label: 'Option Five'
14536 * } )
14537 * ]
14538 * }
14539 * } );
14540 * $( 'body' ).append( comboBox.$element );
14541 *
14542 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
14543 *
14544 * @class
14545 * @extends OO.ui.Widget
14546 * @mixins OO.ui.mixin.TabIndexedElement
14547 *
14548 * @constructor
14549 * @param {Object} [config] Configuration options
14550 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}.
14551 * @cfg {Object} [input] Configuration options to pass to the {@link OO.ui.TextInputWidget text input widget}.
14552 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
14553 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
14554 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
14555 */
14556 OO.ui.ComboBoxWidget = function OoUiComboBoxWidget( config ) {
14557 // Configuration initialization
14558 config = config || {};
14559
14560 // Parent constructor
14561 OO.ui.ComboBoxWidget.parent.call( this, config );
14562
14563 // Properties (must be set before TabIndexedElement constructor call)
14564 this.$indicator = this.$( '<span>' );
14565
14566 // Mixin constructors
14567 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$indicator } ) );
14568
14569 // Properties
14570 this.$overlay = config.$overlay || this.$element;
14571 this.input = new OO.ui.TextInputWidget( $.extend(
14572 {
14573 indicator: 'down',
14574 $indicator: this.$indicator,
14575 disabled: this.isDisabled()
14576 },
14577 config.input
14578 ) );
14579 this.input.$input.eq( 0 ).attr( {
14580 role: 'combobox',
14581 'aria-autocomplete': 'list'
14582 } );
14583 this.menu = new OO.ui.TextInputMenuSelectWidget( this.input, $.extend(
14584 {
14585 widget: this,
14586 input: this.input,
14587 disabled: this.isDisabled()
14588 },
14589 config.menu
14590 ) );
14591
14592 // Events
14593 this.$indicator.on( {
14594 click: this.onClick.bind( this ),
14595 keypress: this.onKeyPress.bind( this )
14596 } );
14597 this.input.connect( this, {
14598 change: 'onInputChange',
14599 enter: 'onInputEnter'
14600 } );
14601 this.menu.connect( this, {
14602 choose: 'onMenuChoose',
14603 add: 'onMenuItemsChange',
14604 remove: 'onMenuItemsChange'
14605 } );
14606
14607 // Initialization
14608 this.$element.addClass( 'oo-ui-comboBoxWidget' ).append( this.input.$element );
14609 this.$overlay.append( this.menu.$element );
14610 this.onMenuItemsChange();
14611 };
14612
14613 /* Setup */
14614
14615 OO.inheritClass( OO.ui.ComboBoxWidget, OO.ui.Widget );
14616 OO.mixinClass( OO.ui.ComboBoxWidget, OO.ui.mixin.TabIndexedElement );
14617
14618 /* Methods */
14619
14620 /**
14621 * Get the combobox's menu.
14622 * @return {OO.ui.TextInputMenuSelectWidget} Menu widget
14623 */
14624 OO.ui.ComboBoxWidget.prototype.getMenu = function () {
14625 return this.menu;
14626 };
14627
14628 /**
14629 * Get the combobox's text input widget.
14630 * @return {OO.ui.TextInputWidget} Text input widget
14631 */
14632 OO.ui.ComboBoxWidget.prototype.getInput = function () {
14633 return this.input;
14634 };
14635
14636 /**
14637 * Handle input change events.
14638 *
14639 * @private
14640 * @param {string} value New value
14641 */
14642 OO.ui.ComboBoxWidget.prototype.onInputChange = function ( value ) {
14643 var match = this.menu.getItemFromData( value );
14644
14645 this.menu.selectItem( match );
14646 if ( this.menu.getHighlightedItem() ) {
14647 this.menu.highlightItem( match );
14648 }
14649
14650 if ( !this.isDisabled() ) {
14651 this.menu.toggle( true );
14652 }
14653 };
14654
14655 /**
14656 * Handle mouse click events.
14657 *
14658 *
14659 * @private
14660 * @param {jQuery.Event} e Mouse click event
14661 */
14662 OO.ui.ComboBoxWidget.prototype.onClick = function ( e ) {
14663 if ( !this.isDisabled() && e.which === 1 ) {
14664 this.menu.toggle();
14665 this.input.$input[ 0 ].focus();
14666 }
14667 return false;
14668 };
14669
14670 /**
14671 * Handle key press events.
14672 *
14673 *
14674 * @private
14675 * @param {jQuery.Event} e Key press event
14676 */
14677 OO.ui.ComboBoxWidget.prototype.onKeyPress = function ( e ) {
14678 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
14679 this.menu.toggle();
14680 this.input.$input[ 0 ].focus();
14681 return false;
14682 }
14683 };
14684
14685 /**
14686 * Handle input enter events.
14687 *
14688 * @private
14689 */
14690 OO.ui.ComboBoxWidget.prototype.onInputEnter = function () {
14691 if ( !this.isDisabled() ) {
14692 this.menu.toggle( false );
14693 }
14694 };
14695
14696 /**
14697 * Handle menu choose events.
14698 *
14699 * @private
14700 * @param {OO.ui.OptionWidget} item Chosen item
14701 */
14702 OO.ui.ComboBoxWidget.prototype.onMenuChoose = function ( item ) {
14703 this.input.setValue( item.getData() );
14704 };
14705
14706 /**
14707 * Handle menu item change events.
14708 *
14709 * @private
14710 */
14711 OO.ui.ComboBoxWidget.prototype.onMenuItemsChange = function () {
14712 var match = this.menu.getItemFromData( this.input.getValue() );
14713 this.menu.selectItem( match );
14714 if ( this.menu.getHighlightedItem() ) {
14715 this.menu.highlightItem( match );
14716 }
14717 this.$element.toggleClass( 'oo-ui-comboBoxWidget-empty', this.menu.isEmpty() );
14718 };
14719
14720 /**
14721 * @inheritdoc
14722 */
14723 OO.ui.ComboBoxWidget.prototype.setDisabled = function ( disabled ) {
14724 // Parent method
14725 OO.ui.ComboBoxWidget.parent.prototype.setDisabled.call( this, disabled );
14726
14727 if ( this.input ) {
14728 this.input.setDisabled( this.isDisabled() );
14729 }
14730 if ( this.menu ) {
14731 this.menu.setDisabled( this.isDisabled() );
14732 }
14733
14734 return this;
14735 };
14736
14737 /**
14738 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
14739 * be configured with a `label` option that is set to a string, a label node, or a function:
14740 *
14741 * - String: a plaintext string
14742 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
14743 * label that includes a link or special styling, such as a gray color or additional graphical elements.
14744 * - Function: a function that will produce a string in the future. Functions are used
14745 * in cases where the value of the label is not currently defined.
14746 *
14747 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
14748 * will come into focus when the label is clicked.
14749 *
14750 * @example
14751 * // Examples of LabelWidgets
14752 * var label1 = new OO.ui.LabelWidget( {
14753 * label: 'plaintext label'
14754 * } );
14755 * var label2 = new OO.ui.LabelWidget( {
14756 * label: $( '<a href="default.html">jQuery label</a>' )
14757 * } );
14758 * // Create a fieldset layout with fields for each example
14759 * var fieldset = new OO.ui.FieldsetLayout();
14760 * fieldset.addItems( [
14761 * new OO.ui.FieldLayout( label1 ),
14762 * new OO.ui.FieldLayout( label2 )
14763 * ] );
14764 * $( 'body' ).append( fieldset.$element );
14765 *
14766 *
14767 * @class
14768 * @extends OO.ui.Widget
14769 * @mixins OO.ui.mixin.LabelElement
14770 *
14771 * @constructor
14772 * @param {Object} [config] Configuration options
14773 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
14774 * Clicking the label will focus the specified input field.
14775 */
14776 OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
14777 // Configuration initialization
14778 config = config || {};
14779
14780 // Parent constructor
14781 OO.ui.LabelWidget.parent.call( this, config );
14782
14783 // Mixin constructors
14784 OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, { $label: this.$element } ) );
14785 OO.ui.mixin.TitledElement.call( this, config );
14786
14787 // Properties
14788 this.input = config.input;
14789
14790 // Events
14791 if ( this.input instanceof OO.ui.InputWidget ) {
14792 this.$element.on( 'click', this.onClick.bind( this ) );
14793 }
14794
14795 // Initialization
14796 this.$element.addClass( 'oo-ui-labelWidget' );
14797 };
14798
14799 /* Setup */
14800
14801 OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
14802 OO.mixinClass( OO.ui.LabelWidget, OO.ui.mixin.LabelElement );
14803 OO.mixinClass( OO.ui.LabelWidget, OO.ui.mixin.TitledElement );
14804
14805 /* Static Properties */
14806
14807 OO.ui.LabelWidget.static.tagName = 'span';
14808
14809 /* Methods */
14810
14811 /**
14812 * Handles label mouse click events.
14813 *
14814 * @private
14815 * @param {jQuery.Event} e Mouse click event
14816 */
14817 OO.ui.LabelWidget.prototype.onClick = function () {
14818 this.input.simulateLabelClick();
14819 return false;
14820 };
14821
14822 /**
14823 * OptionWidgets are special elements that can be selected and configured with data. The
14824 * data is often unique for each option, but it does not have to be. OptionWidgets are used
14825 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
14826 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
14827 *
14828 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
14829 *
14830 * @class
14831 * @extends OO.ui.Widget
14832 * @mixins OO.ui.mixin.LabelElement
14833 * @mixins OO.ui.mixin.FlaggedElement
14834 *
14835 * @constructor
14836 * @param {Object} [config] Configuration options
14837 */
14838 OO.ui.OptionWidget = function OoUiOptionWidget( config ) {
14839 // Configuration initialization
14840 config = config || {};
14841
14842 // Parent constructor
14843 OO.ui.OptionWidget.parent.call( this, config );
14844
14845 // Mixin constructors
14846 OO.ui.mixin.ItemWidget.call( this );
14847 OO.ui.mixin.LabelElement.call( this, config );
14848 OO.ui.mixin.FlaggedElement.call( this, config );
14849
14850 // Properties
14851 this.selected = false;
14852 this.highlighted = false;
14853 this.pressed = false;
14854
14855 // Initialization
14856 this.$element
14857 .data( 'oo-ui-optionWidget', this )
14858 .attr( 'role', 'option' )
14859 .attr( 'aria-selected', 'false' )
14860 .addClass( 'oo-ui-optionWidget' )
14861 .append( this.$label );
14862 };
14863
14864 /* Setup */
14865
14866 OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
14867 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.ItemWidget );
14868 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.LabelElement );
14869 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.FlaggedElement );
14870
14871 /* Static Properties */
14872
14873 OO.ui.OptionWidget.static.selectable = true;
14874
14875 OO.ui.OptionWidget.static.highlightable = true;
14876
14877 OO.ui.OptionWidget.static.pressable = true;
14878
14879 OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
14880
14881 /* Methods */
14882
14883 /**
14884 * Check if the option can be selected.
14885 *
14886 * @return {boolean} Item is selectable
14887 */
14888 OO.ui.OptionWidget.prototype.isSelectable = function () {
14889 return this.constructor.static.selectable && !this.isDisabled();
14890 };
14891
14892 /**
14893 * Check if the option can be highlighted. A highlight indicates that the option
14894 * may be selected when a user presses enter or clicks. Disabled items cannot
14895 * be highlighted.
14896 *
14897 * @return {boolean} Item is highlightable
14898 */
14899 OO.ui.OptionWidget.prototype.isHighlightable = function () {
14900 return this.constructor.static.highlightable && !this.isDisabled();
14901 };
14902
14903 /**
14904 * Check if the option can be pressed. The pressed state occurs when a user mouses
14905 * down on an item, but has not yet let go of the mouse.
14906 *
14907 * @return {boolean} Item is pressable
14908 */
14909 OO.ui.OptionWidget.prototype.isPressable = function () {
14910 return this.constructor.static.pressable && !this.isDisabled();
14911 };
14912
14913 /**
14914 * Check if the option is selected.
14915 *
14916 * @return {boolean} Item is selected
14917 */
14918 OO.ui.OptionWidget.prototype.isSelected = function () {
14919 return this.selected;
14920 };
14921
14922 /**
14923 * Check if the option is highlighted. A highlight indicates that the
14924 * item may be selected when a user presses enter or clicks.
14925 *
14926 * @return {boolean} Item is highlighted
14927 */
14928 OO.ui.OptionWidget.prototype.isHighlighted = function () {
14929 return this.highlighted;
14930 };
14931
14932 /**
14933 * Check if the option is pressed. The pressed state occurs when a user mouses
14934 * down on an item, but has not yet let go of the mouse. The item may appear
14935 * selected, but it will not be selected until the user releases the mouse.
14936 *
14937 * @return {boolean} Item is pressed
14938 */
14939 OO.ui.OptionWidget.prototype.isPressed = function () {
14940 return this.pressed;
14941 };
14942
14943 /**
14944 * Set the option’s selected state. In general, all modifications to the selection
14945 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
14946 * method instead of this method.
14947 *
14948 * @param {boolean} [state=false] Select option
14949 * @chainable
14950 */
14951 OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
14952 if ( this.constructor.static.selectable ) {
14953 this.selected = !!state;
14954 this.$element
14955 .toggleClass( 'oo-ui-optionWidget-selected', state )
14956 .attr( 'aria-selected', state.toString() );
14957 if ( state && this.constructor.static.scrollIntoViewOnSelect ) {
14958 this.scrollElementIntoView();
14959 }
14960 this.updateThemeClasses();
14961 }
14962 return this;
14963 };
14964
14965 /**
14966 * Set the option’s highlighted state. In general, all programmatic
14967 * modifications to the highlight should be handled by the
14968 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
14969 * method instead of this method.
14970 *
14971 * @param {boolean} [state=false] Highlight option
14972 * @chainable
14973 */
14974 OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
14975 if ( this.constructor.static.highlightable ) {
14976 this.highlighted = !!state;
14977 this.$element.toggleClass( 'oo-ui-optionWidget-highlighted', state );
14978 this.updateThemeClasses();
14979 }
14980 return this;
14981 };
14982
14983 /**
14984 * Set the option’s pressed state. In general, all
14985 * programmatic modifications to the pressed state should be handled by the
14986 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
14987 * method instead of this method.
14988 *
14989 * @param {boolean} [state=false] Press option
14990 * @chainable
14991 */
14992 OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
14993 if ( this.constructor.static.pressable ) {
14994 this.pressed = !!state;
14995 this.$element.toggleClass( 'oo-ui-optionWidget-pressed', state );
14996 this.updateThemeClasses();
14997 }
14998 return this;
14999 };
15000
15001 /**
15002 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
15003 * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
15004 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
15005 * options. For more information about options and selects, please see the
15006 * [OOjs UI documentation on MediaWiki][1].
15007 *
15008 * @example
15009 * // Decorated options in a select widget
15010 * var select = new OO.ui.SelectWidget( {
15011 * items: [
15012 * new OO.ui.DecoratedOptionWidget( {
15013 * data: 'a',
15014 * label: 'Option with icon',
15015 * icon: 'help'
15016 * } ),
15017 * new OO.ui.DecoratedOptionWidget( {
15018 * data: 'b',
15019 * label: 'Option with indicator',
15020 * indicator: 'next'
15021 * } )
15022 * ]
15023 * } );
15024 * $( 'body' ).append( select.$element );
15025 *
15026 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
15027 *
15028 * @class
15029 * @extends OO.ui.OptionWidget
15030 * @mixins OO.ui.mixin.IconElement
15031 * @mixins OO.ui.mixin.IndicatorElement
15032 *
15033 * @constructor
15034 * @param {Object} [config] Configuration options
15035 */
15036 OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( config ) {
15037 // Parent constructor
15038 OO.ui.DecoratedOptionWidget.parent.call( this, config );
15039
15040 // Mixin constructors
15041 OO.ui.mixin.IconElement.call( this, config );
15042 OO.ui.mixin.IndicatorElement.call( this, config );
15043
15044 // Initialization
15045 this.$element
15046 .addClass( 'oo-ui-decoratedOptionWidget' )
15047 .prepend( this.$icon )
15048 .append( this.$indicator );
15049 };
15050
15051 /* Setup */
15052
15053 OO.inheritClass( OO.ui.DecoratedOptionWidget, OO.ui.OptionWidget );
15054 OO.mixinClass( OO.ui.DecoratedOptionWidget, OO.ui.mixin.IconElement );
15055 OO.mixinClass( OO.ui.DecoratedOptionWidget, OO.ui.mixin.IndicatorElement );
15056
15057 /**
15058 * ButtonOptionWidget is a special type of {@link OO.ui.mixin.ButtonElement button element} that
15059 * can be selected and configured with data. The class is
15060 * used with OO.ui.ButtonSelectWidget to create a selection of button options. Please see the
15061 * [OOjs UI documentation on MediaWiki] [1] for more information.
15062 *
15063 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_options
15064 *
15065 * @class
15066 * @extends OO.ui.DecoratedOptionWidget
15067 * @mixins OO.ui.mixin.ButtonElement
15068 * @mixins OO.ui.mixin.TabIndexedElement
15069 *
15070 * @constructor
15071 * @param {Object} [config] Configuration options
15072 */
15073 OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( config ) {
15074 // Configuration initialization
15075 config = $.extend( { tabIndex: -1 }, config );
15076
15077 // Parent constructor
15078 OO.ui.ButtonOptionWidget.parent.call( this, config );
15079
15080 // Mixin constructors
15081 OO.ui.mixin.ButtonElement.call( this, config );
15082 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
15083
15084 // Initialization
15085 this.$element.addClass( 'oo-ui-buttonOptionWidget' );
15086 this.$button.append( this.$element.contents() );
15087 this.$element.append( this.$button );
15088 };
15089
15090 /* Setup */
15091
15092 OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.DecoratedOptionWidget );
15093 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.mixin.ButtonElement );
15094 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.mixin.TabIndexedElement );
15095
15096 /* Static Properties */
15097
15098 // Allow button mouse down events to pass through so they can be handled by the parent select widget
15099 OO.ui.ButtonOptionWidget.static.cancelButtonMouseDownEvents = false;
15100
15101 OO.ui.ButtonOptionWidget.static.highlightable = false;
15102
15103 /* Methods */
15104
15105 /**
15106 * @inheritdoc
15107 */
15108 OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) {
15109 OO.ui.ButtonOptionWidget.parent.prototype.setSelected.call( this, state );
15110
15111 if ( this.constructor.static.selectable ) {
15112 this.setActive( state );
15113 }
15114
15115 return this;
15116 };
15117
15118 /**
15119 * RadioOptionWidget is an option widget that looks like a radio button.
15120 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
15121 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
15122 *
15123 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
15124 *
15125 * @class
15126 * @extends OO.ui.OptionWidget
15127 *
15128 * @constructor
15129 * @param {Object} [config] Configuration options
15130 */
15131 OO.ui.RadioOptionWidget = function OoUiRadioOptionWidget( config ) {
15132 // Configuration initialization
15133 config = config || {};
15134
15135 // Properties (must be done before parent constructor which calls #setDisabled)
15136 this.radio = new OO.ui.RadioInputWidget( { value: config.data, tabIndex: -1 } );
15137
15138 // Parent constructor
15139 OO.ui.RadioOptionWidget.parent.call( this, config );
15140
15141 // Events
15142 this.radio.$input.on( 'focus', this.onInputFocus.bind( this ) );
15143
15144 // Initialization
15145 // Remove implicit role, we're handling it ourselves
15146 this.radio.$input.attr( 'role', 'presentation' );
15147 this.$element
15148 .addClass( 'oo-ui-radioOptionWidget' )
15149 .attr( 'role', 'radio' )
15150 .attr( 'aria-checked', 'false' )
15151 .removeAttr( 'aria-selected' )
15152 .prepend( this.radio.$element );
15153 };
15154
15155 /* Setup */
15156
15157 OO.inheritClass( OO.ui.RadioOptionWidget, OO.ui.OptionWidget );
15158
15159 /* Static Properties */
15160
15161 OO.ui.RadioOptionWidget.static.highlightable = false;
15162
15163 OO.ui.RadioOptionWidget.static.scrollIntoViewOnSelect = true;
15164
15165 OO.ui.RadioOptionWidget.static.pressable = false;
15166
15167 OO.ui.RadioOptionWidget.static.tagName = 'label';
15168
15169 /* Methods */
15170
15171 /**
15172 * @param {jQuery.Event} e Focus event
15173 * @private
15174 */
15175 OO.ui.RadioOptionWidget.prototype.onInputFocus = function () {
15176 this.radio.$input.blur();
15177 this.$element.parent().focus();
15178 };
15179
15180 /**
15181 * @inheritdoc
15182 */
15183 OO.ui.RadioOptionWidget.prototype.setSelected = function ( state ) {
15184 OO.ui.RadioOptionWidget.parent.prototype.setSelected.call( this, state );
15185
15186 this.radio.setSelected( state );
15187 this.$element
15188 .attr( 'aria-checked', state.toString() )
15189 .removeAttr( 'aria-selected' );
15190
15191 return this;
15192 };
15193
15194 /**
15195 * @inheritdoc
15196 */
15197 OO.ui.RadioOptionWidget.prototype.setDisabled = function ( disabled ) {
15198 OO.ui.RadioOptionWidget.parent.prototype.setDisabled.call( this, disabled );
15199
15200 this.radio.setDisabled( this.isDisabled() );
15201
15202 return this;
15203 };
15204
15205 /**
15206 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
15207 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
15208 * the [OOjs UI documentation on MediaWiki] [1] for more information.
15209 *
15210 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
15211 *
15212 * @class
15213 * @extends OO.ui.DecoratedOptionWidget
15214 *
15215 * @constructor
15216 * @param {Object} [config] Configuration options
15217 */
15218 OO.ui.MenuOptionWidget = function OoUiMenuOptionWidget( config ) {
15219 // Configuration initialization
15220 config = $.extend( { icon: 'check' }, config );
15221
15222 // Parent constructor
15223 OO.ui.MenuOptionWidget.parent.call( this, config );
15224
15225 // Initialization
15226 this.$element
15227 .attr( 'role', 'menuitem' )
15228 .addClass( 'oo-ui-menuOptionWidget' );
15229 };
15230
15231 /* Setup */
15232
15233 OO.inheritClass( OO.ui.MenuOptionWidget, OO.ui.DecoratedOptionWidget );
15234
15235 /* Static Properties */
15236
15237 OO.ui.MenuOptionWidget.static.scrollIntoViewOnSelect = true;
15238
15239 /**
15240 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
15241 * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
15242 *
15243 * @example
15244 * var myDropdown = new OO.ui.DropdownWidget( {
15245 * menu: {
15246 * items: [
15247 * new OO.ui.MenuSectionOptionWidget( {
15248 * label: 'Dogs'
15249 * } ),
15250 * new OO.ui.MenuOptionWidget( {
15251 * data: 'corgi',
15252 * label: 'Welsh Corgi'
15253 * } ),
15254 * new OO.ui.MenuOptionWidget( {
15255 * data: 'poodle',
15256 * label: 'Standard Poodle'
15257 * } ),
15258 * new OO.ui.MenuSectionOptionWidget( {
15259 * label: 'Cats'
15260 * } ),
15261 * new OO.ui.MenuOptionWidget( {
15262 * data: 'lion',
15263 * label: 'Lion'
15264 * } )
15265 * ]
15266 * }
15267 * } );
15268 * $( 'body' ).append( myDropdown.$element );
15269 *
15270 *
15271 * @class
15272 * @extends OO.ui.DecoratedOptionWidget
15273 *
15274 * @constructor
15275 * @param {Object} [config] Configuration options
15276 */
15277 OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( config ) {
15278 // Parent constructor
15279 OO.ui.MenuSectionOptionWidget.parent.call( this, config );
15280
15281 // Initialization
15282 this.$element.addClass( 'oo-ui-menuSectionOptionWidget' );
15283 };
15284
15285 /* Setup */
15286
15287 OO.inheritClass( OO.ui.MenuSectionOptionWidget, OO.ui.DecoratedOptionWidget );
15288
15289 /* Static Properties */
15290
15291 OO.ui.MenuSectionOptionWidget.static.selectable = false;
15292
15293 OO.ui.MenuSectionOptionWidget.static.highlightable = false;
15294
15295 /**
15296 * OutlineOptionWidget is an item in an {@link OO.ui.OutlineSelectWidget OutlineSelectWidget}.
15297 *
15298 * Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}, which contain
15299 * {@link OO.ui.PageLayout page layouts}. See {@link OO.ui.BookletLayout BookletLayout}
15300 * for an example.
15301 *
15302 * @class
15303 * @extends OO.ui.DecoratedOptionWidget
15304 *
15305 * @constructor
15306 * @param {Object} [config] Configuration options
15307 * @cfg {number} [level] Indentation level
15308 * @cfg {boolean} [movable] Allow modification from {@link OO.ui.OutlineControlsWidget outline controls}.
15309 */
15310 OO.ui.OutlineOptionWidget = function OoUiOutlineOptionWidget( config ) {
15311 // Configuration initialization
15312 config = config || {};
15313
15314 // Parent constructor
15315 OO.ui.OutlineOptionWidget.parent.call( this, config );
15316
15317 // Properties
15318 this.level = 0;
15319 this.movable = !!config.movable;
15320 this.removable = !!config.removable;
15321
15322 // Initialization
15323 this.$element.addClass( 'oo-ui-outlineOptionWidget' );
15324 this.setLevel( config.level );
15325 };
15326
15327 /* Setup */
15328
15329 OO.inheritClass( OO.ui.OutlineOptionWidget, OO.ui.DecoratedOptionWidget );
15330
15331 /* Static Properties */
15332
15333 OO.ui.OutlineOptionWidget.static.highlightable = false;
15334
15335 OO.ui.OutlineOptionWidget.static.scrollIntoViewOnSelect = true;
15336
15337 OO.ui.OutlineOptionWidget.static.levelClass = 'oo-ui-outlineOptionWidget-level-';
15338
15339 OO.ui.OutlineOptionWidget.static.levels = 3;
15340
15341 /* Methods */
15342
15343 /**
15344 * Check if item is movable.
15345 *
15346 * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
15347 *
15348 * @return {boolean} Item is movable
15349 */
15350 OO.ui.OutlineOptionWidget.prototype.isMovable = function () {
15351 return this.movable;
15352 };
15353
15354 /**
15355 * Check if item is removable.
15356 *
15357 * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
15358 *
15359 * @return {boolean} Item is removable
15360 */
15361 OO.ui.OutlineOptionWidget.prototype.isRemovable = function () {
15362 return this.removable;
15363 };
15364
15365 /**
15366 * Get indentation level.
15367 *
15368 * @return {number} Indentation level
15369 */
15370 OO.ui.OutlineOptionWidget.prototype.getLevel = function () {
15371 return this.level;
15372 };
15373
15374 /**
15375 * Set movability.
15376 *
15377 * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
15378 *
15379 * @param {boolean} movable Item is movable
15380 * @chainable
15381 */
15382 OO.ui.OutlineOptionWidget.prototype.setMovable = function ( movable ) {
15383 this.movable = !!movable;
15384 this.updateThemeClasses();
15385 return this;
15386 };
15387
15388 /**
15389 * Set removability.
15390 *
15391 * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
15392 *
15393 * @param {boolean} movable Item is removable
15394 * @chainable
15395 */
15396 OO.ui.OutlineOptionWidget.prototype.setRemovable = function ( removable ) {
15397 this.removable = !!removable;
15398 this.updateThemeClasses();
15399 return this;
15400 };
15401
15402 /**
15403 * Set indentation level.
15404 *
15405 * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
15406 * @chainable
15407 */
15408 OO.ui.OutlineOptionWidget.prototype.setLevel = function ( level ) {
15409 var levels = this.constructor.static.levels,
15410 levelClass = this.constructor.static.levelClass,
15411 i = levels;
15412
15413 this.level = level ? Math.max( 0, Math.min( levels - 1, level ) ) : 0;
15414 while ( i-- ) {
15415 if ( this.level === i ) {
15416 this.$element.addClass( levelClass + i );
15417 } else {
15418 this.$element.removeClass( levelClass + i );
15419 }
15420 }
15421 this.updateThemeClasses();
15422
15423 return this;
15424 };
15425
15426 /**
15427 * TabOptionWidget is an item in a {@link OO.ui.TabSelectWidget TabSelectWidget}.
15428 *
15429 * Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}, which contain
15430 * {@link OO.ui.CardLayout card layouts}. See {@link OO.ui.IndexLayout IndexLayout}
15431 * for an example.
15432 *
15433 * @class
15434 * @extends OO.ui.OptionWidget
15435 *
15436 * @constructor
15437 * @param {Object} [config] Configuration options
15438 */
15439 OO.ui.TabOptionWidget = function OoUiTabOptionWidget( config ) {
15440 // Configuration initialization
15441 config = config || {};
15442
15443 // Parent constructor
15444 OO.ui.TabOptionWidget.parent.call( this, config );
15445
15446 // Initialization
15447 this.$element.addClass( 'oo-ui-tabOptionWidget' );
15448 };
15449
15450 /* Setup */
15451
15452 OO.inheritClass( OO.ui.TabOptionWidget, OO.ui.OptionWidget );
15453
15454 /* Static Properties */
15455
15456 OO.ui.TabOptionWidget.static.highlightable = false;
15457
15458 /**
15459 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
15460 * By default, each popup has an anchor that points toward its origin.
15461 * Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples.
15462 *
15463 * @example
15464 * // A popup widget.
15465 * var popup = new OO.ui.PopupWidget( {
15466 * $content: $( '<p>Hi there!</p>' ),
15467 * padded: true,
15468 * width: 300
15469 * } );
15470 *
15471 * $( 'body' ).append( popup.$element );
15472 * // To display the popup, toggle the visibility to 'true'.
15473 * popup.toggle( true );
15474 *
15475 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups
15476 *
15477 * @class
15478 * @extends OO.ui.Widget
15479 * @mixins OO.ui.mixin.LabelElement
15480 *
15481 * @constructor
15482 * @param {Object} [config] Configuration options
15483 * @cfg {number} [width=320] Width of popup in pixels
15484 * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
15485 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
15486 * @cfg {string} [align='center'] Alignment of the popup: `center`, `force-left`, `force-right`, `backwards` or `forwards`.
15487 * If the popup is forced-left the popup body is leaning towards the left. For force-right alignment, the body of the
15488 * popup is leaning towards the right of the screen.
15489 * Using 'backwards' is a logical direction which will result in the popup leaning towards the beginning of the sentence
15490 * in the given language, which means it will flip to the correct positioning in right-to-left languages.
15491 * Using 'forward' will also result in a logical alignment where the body of the popup leans towards the end of the
15492 * sentence in the given language.
15493 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
15494 * See the [OOjs UI docs on MediaWiki][3] for an example.
15495 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
15496 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
15497 * @cfg {jQuery} [$content] Content to append to the popup's body
15498 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
15499 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
15500 * This config option is only relevant if #autoClose is set to `true`. See the [OOjs UI docs on MediaWiki][2]
15501 * for an example.
15502 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#autocloseExample
15503 * @cfg {boolean} [head] Show a popup header that contains a #label (if specified) and close
15504 * button.
15505 * @cfg {boolean} [padded] Add padding to the popup's body
15506 */
15507 OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
15508 // Configuration initialization
15509 config = config || {};
15510
15511 // Parent constructor
15512 OO.ui.PopupWidget.parent.call( this, config );
15513
15514 // Properties (must be set before ClippableElement constructor call)
15515 this.$body = $( '<div>' );
15516
15517 // Mixin constructors
15518 OO.ui.mixin.LabelElement.call( this, config );
15519 OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$body } ) );
15520
15521 // Properties
15522 this.$popup = $( '<div>' );
15523 this.$head = $( '<div>' );
15524 this.$anchor = $( '<div>' );
15525 // If undefined, will be computed lazily in updateDimensions()
15526 this.$container = config.$container;
15527 this.containerPadding = config.containerPadding !== undefined ? config.containerPadding : 10;
15528 this.autoClose = !!config.autoClose;
15529 this.$autoCloseIgnore = config.$autoCloseIgnore;
15530 this.transitionTimeout = null;
15531 this.anchor = null;
15532 this.width = config.width !== undefined ? config.width : 320;
15533 this.height = config.height !== undefined ? config.height : null;
15534 this.setAlignment( config.align );
15535 this.closeButton = new OO.ui.ButtonWidget( { framed: false, icon: 'close' } );
15536 this.onMouseDownHandler = this.onMouseDown.bind( this );
15537 this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
15538
15539 // Events
15540 this.closeButton.connect( this, { click: 'onCloseButtonClick' } );
15541
15542 // Initialization
15543 this.toggleAnchor( config.anchor === undefined || config.anchor );
15544 this.$body.addClass( 'oo-ui-popupWidget-body' );
15545 this.$anchor.addClass( 'oo-ui-popupWidget-anchor' );
15546 this.$head
15547 .addClass( 'oo-ui-popupWidget-head' )
15548 .append( this.$label, this.closeButton.$element );
15549 if ( !config.head ) {
15550 this.$head.addClass( 'oo-ui-element-hidden' );
15551 }
15552 this.$popup
15553 .addClass( 'oo-ui-popupWidget-popup' )
15554 .append( this.$head, this.$body );
15555 this.$element
15556 .addClass( 'oo-ui-popupWidget' )
15557 .append( this.$popup, this.$anchor );
15558 // Move content, which was added to #$element by OO.ui.Widget, to the body
15559 if ( config.$content instanceof jQuery ) {
15560 this.$body.append( config.$content );
15561 }
15562 if ( config.padded ) {
15563 this.$body.addClass( 'oo-ui-popupWidget-body-padded' );
15564 }
15565
15566 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
15567 // that reference properties not initialized at that time of parent class construction
15568 // TODO: Find a better way to handle post-constructor setup
15569 this.visible = false;
15570 this.$element.addClass( 'oo-ui-element-hidden' );
15571 };
15572
15573 /* Setup */
15574
15575 OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
15576 OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.LabelElement );
15577 OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.ClippableElement );
15578
15579 /* Methods */
15580
15581 /**
15582 * Handles mouse down events.
15583 *
15584 * @private
15585 * @param {MouseEvent} e Mouse down event
15586 */
15587 OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
15588 if (
15589 this.isVisible() &&
15590 !$.contains( this.$element[ 0 ], e.target ) &&
15591 ( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length )
15592 ) {
15593 this.toggle( false );
15594 }
15595 };
15596
15597 /**
15598 * Bind mouse down listener.
15599 *
15600 * @private
15601 */
15602 OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
15603 // Capture clicks outside popup
15604 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
15605 };
15606
15607 /**
15608 * Handles close button click events.
15609 *
15610 * @private
15611 */
15612 OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
15613 if ( this.isVisible() ) {
15614 this.toggle( false );
15615 }
15616 };
15617
15618 /**
15619 * Unbind mouse down listener.
15620 *
15621 * @private
15622 */
15623 OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
15624 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
15625 };
15626
15627 /**
15628 * Handles key down events.
15629 *
15630 * @private
15631 * @param {KeyboardEvent} e Key down event
15632 */
15633 OO.ui.PopupWidget.prototype.onDocumentKeyDown = function ( e ) {
15634 if (
15635 e.which === OO.ui.Keys.ESCAPE &&
15636 this.isVisible()
15637 ) {
15638 this.toggle( false );
15639 e.preventDefault();
15640 e.stopPropagation();
15641 }
15642 };
15643
15644 /**
15645 * Bind key down listener.
15646 *
15647 * @private
15648 */
15649 OO.ui.PopupWidget.prototype.bindKeyDownListener = function () {
15650 this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
15651 };
15652
15653 /**
15654 * Unbind key down listener.
15655 *
15656 * @private
15657 */
15658 OO.ui.PopupWidget.prototype.unbindKeyDownListener = function () {
15659 this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
15660 };
15661
15662 /**
15663 * Show, hide, or toggle the visibility of the anchor.
15664 *
15665 * @param {boolean} [show] Show anchor, omit to toggle
15666 */
15667 OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) {
15668 show = show === undefined ? !this.anchored : !!show;
15669
15670 if ( this.anchored !== show ) {
15671 if ( show ) {
15672 this.$element.addClass( 'oo-ui-popupWidget-anchored' );
15673 } else {
15674 this.$element.removeClass( 'oo-ui-popupWidget-anchored' );
15675 }
15676 this.anchored = show;
15677 }
15678 };
15679
15680 /**
15681 * Check if the anchor is visible.
15682 *
15683 * @return {boolean} Anchor is visible
15684 */
15685 OO.ui.PopupWidget.prototype.hasAnchor = function () {
15686 return this.anchor;
15687 };
15688
15689 /**
15690 * @inheritdoc
15691 */
15692 OO.ui.PopupWidget.prototype.toggle = function ( show ) {
15693 show = show === undefined ? !this.isVisible() : !!show;
15694
15695 var change = show !== this.isVisible();
15696
15697 // Parent method
15698 OO.ui.PopupWidget.parent.prototype.toggle.call( this, show );
15699
15700 if ( change ) {
15701 if ( show ) {
15702 if ( this.autoClose ) {
15703 this.bindMouseDownListener();
15704 this.bindKeyDownListener();
15705 }
15706 this.updateDimensions();
15707 this.toggleClipping( true );
15708 } else {
15709 this.toggleClipping( false );
15710 if ( this.autoClose ) {
15711 this.unbindMouseDownListener();
15712 this.unbindKeyDownListener();
15713 }
15714 }
15715 }
15716
15717 return this;
15718 };
15719
15720 /**
15721 * Set the size of the popup.
15722 *
15723 * Changing the size may also change the popup's position depending on the alignment.
15724 *
15725 * @param {number} width Width in pixels
15726 * @param {number} height Height in pixels
15727 * @param {boolean} [transition=false] Use a smooth transition
15728 * @chainable
15729 */
15730 OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) {
15731 this.width = width;
15732 this.height = height !== undefined ? height : null;
15733 if ( this.isVisible() ) {
15734 this.updateDimensions( transition );
15735 }
15736 };
15737
15738 /**
15739 * Update the size and position.
15740 *
15741 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
15742 * be called automatically.
15743 *
15744 * @param {boolean} [transition=false] Use a smooth transition
15745 * @chainable
15746 */
15747 OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
15748 var popupOffset, originOffset, containerLeft, containerWidth, containerRight,
15749 popupLeft, popupRight, overlapLeft, overlapRight, anchorWidth,
15750 align = this.align,
15751 widget = this;
15752
15753 if ( !this.$container ) {
15754 // Lazy-initialize $container if not specified in constructor
15755 this.$container = $( this.getClosestScrollableElementContainer() );
15756 }
15757
15758 // Set height and width before measuring things, since it might cause our measurements
15759 // to change (e.g. due to scrollbars appearing or disappearing)
15760 this.$popup.css( {
15761 width: this.width,
15762 height: this.height !== null ? this.height : 'auto'
15763 } );
15764
15765 // If we are in RTL, we need to flip the alignment, unless it is center
15766 if ( align === 'forwards' || align === 'backwards' ) {
15767 if ( this.$container.css( 'direction' ) === 'rtl' ) {
15768 align = ( { forwards: 'force-left', backwards: 'force-right' } )[ this.align ];
15769 } else {
15770 align = ( { forwards: 'force-right', backwards: 'force-left' } )[ this.align ];
15771 }
15772
15773 }
15774
15775 // Compute initial popupOffset based on alignment
15776 popupOffset = this.width * ( { 'force-left': -1, center: -0.5, 'force-right': 0 } )[ align ];
15777
15778 // Figure out if this will cause the popup to go beyond the edge of the container
15779 originOffset = this.$element.offset().left;
15780 containerLeft = this.$container.offset().left;
15781 containerWidth = this.$container.innerWidth();
15782 containerRight = containerLeft + containerWidth;
15783 popupLeft = popupOffset - this.containerPadding;
15784 popupRight = popupOffset + this.containerPadding + this.width + this.containerPadding;
15785 overlapLeft = ( originOffset + popupLeft ) - containerLeft;
15786 overlapRight = containerRight - ( originOffset + popupRight );
15787
15788 // Adjust offset to make the popup not go beyond the edge, if needed
15789 if ( overlapRight < 0 ) {
15790 popupOffset += overlapRight;
15791 } else if ( overlapLeft < 0 ) {
15792 popupOffset -= overlapLeft;
15793 }
15794
15795 // Adjust offset to avoid anchor being rendered too close to the edge
15796 // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
15797 // TODO: Find a measurement that works for CSS anchors and image anchors
15798 anchorWidth = this.$anchor[ 0 ].scrollWidth * 2;
15799 if ( popupOffset + this.width < anchorWidth ) {
15800 popupOffset = anchorWidth - this.width;
15801 } else if ( -popupOffset < anchorWidth ) {
15802 popupOffset = -anchorWidth;
15803 }
15804
15805 // Prevent transition from being interrupted
15806 clearTimeout( this.transitionTimeout );
15807 if ( transition ) {
15808 // Enable transition
15809 this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
15810 }
15811
15812 // Position body relative to anchor
15813 this.$popup.css( 'margin-left', popupOffset );
15814
15815 if ( transition ) {
15816 // Prevent transitioning after transition is complete
15817 this.transitionTimeout = setTimeout( function () {
15818 widget.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
15819 }, 200 );
15820 } else {
15821 // Prevent transitioning immediately
15822 this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
15823 }
15824
15825 // Reevaluate clipping state since we've relocated and resized the popup
15826 this.clip();
15827
15828 return this;
15829 };
15830
15831 /**
15832 * Set popup alignment
15833 * @param {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
15834 * `backwards` or `forwards`.
15835 */
15836 OO.ui.PopupWidget.prototype.setAlignment = function ( align ) {
15837 // Validate alignment and transform deprecated values
15838 if ( [ 'left', 'right', 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align ) > -1 ) {
15839 this.align = { left: 'force-right', right: 'force-left' }[ align ] || align;
15840 } else {
15841 this.align = 'center';
15842 }
15843 };
15844
15845 /**
15846 * Get popup alignment
15847 * @return {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
15848 * `backwards` or `forwards`.
15849 */
15850 OO.ui.PopupWidget.prototype.getAlignment = function () {
15851 return this.align;
15852 };
15853
15854 /**
15855 * Progress bars visually display the status of an operation, such as a download,
15856 * and can be either determinate or indeterminate:
15857 *
15858 * - **determinate** process bars show the percent of an operation that is complete.
15859 *
15860 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
15861 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
15862 * not use percentages.
15863 *
15864 * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
15865 *
15866 * @example
15867 * // Examples of determinate and indeterminate progress bars.
15868 * var progressBar1 = new OO.ui.ProgressBarWidget( {
15869 * progress: 33
15870 * } );
15871 * var progressBar2 = new OO.ui.ProgressBarWidget();
15872 *
15873 * // Create a FieldsetLayout to layout progress bars
15874 * var fieldset = new OO.ui.FieldsetLayout;
15875 * fieldset.addItems( [
15876 * new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
15877 * new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
15878 * ] );
15879 * $( 'body' ).append( fieldset.$element );
15880 *
15881 * @class
15882 * @extends OO.ui.Widget
15883 *
15884 * @constructor
15885 * @param {Object} [config] Configuration options
15886 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
15887 * To create a determinate progress bar, specify a number that reflects the initial percent complete.
15888 * By default, the progress bar is indeterminate.
15889 */
15890 OO.ui.ProgressBarWidget = function OoUiProgressBarWidget( config ) {
15891 // Configuration initialization
15892 config = config || {};
15893
15894 // Parent constructor
15895 OO.ui.ProgressBarWidget.parent.call( this, config );
15896
15897 // Properties
15898 this.$bar = $( '<div>' );
15899 this.progress = null;
15900
15901 // Initialization
15902 this.setProgress( config.progress !== undefined ? config.progress : false );
15903 this.$bar.addClass( 'oo-ui-progressBarWidget-bar' );
15904 this.$element
15905 .attr( {
15906 role: 'progressbar',
15907 'aria-valuemin': 0,
15908 'aria-valuemax': 100
15909 } )
15910 .addClass( 'oo-ui-progressBarWidget' )
15911 .append( this.$bar );
15912 };
15913
15914 /* Setup */
15915
15916 OO.inheritClass( OO.ui.ProgressBarWidget, OO.ui.Widget );
15917
15918 /* Static Properties */
15919
15920 OO.ui.ProgressBarWidget.static.tagName = 'div';
15921
15922 /* Methods */
15923
15924 /**
15925 * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
15926 *
15927 * @return {number|boolean} Progress percent
15928 */
15929 OO.ui.ProgressBarWidget.prototype.getProgress = function () {
15930 return this.progress;
15931 };
15932
15933 /**
15934 * Set the percent of the process completed or `false` for an indeterminate process.
15935 *
15936 * @param {number|boolean} progress Progress percent or `false` for indeterminate
15937 */
15938 OO.ui.ProgressBarWidget.prototype.setProgress = function ( progress ) {
15939 this.progress = progress;
15940
15941 if ( progress !== false ) {
15942 this.$bar.css( 'width', this.progress + '%' );
15943 this.$element.attr( 'aria-valuenow', this.progress );
15944 } else {
15945 this.$bar.css( 'width', '' );
15946 this.$element.removeAttr( 'aria-valuenow' );
15947 }
15948 this.$element.toggleClass( 'oo-ui-progressBarWidget-indeterminate', !progress );
15949 };
15950
15951 /**
15952 * SearchWidgets combine a {@link OO.ui.TextInputWidget text input field}, where users can type a search query,
15953 * and a {@link OO.ui.TextInputMenuSelectWidget menu} of search results, which is displayed beneath the query
15954 * field. Unlike {@link OO.ui.mixin.LookupElement lookup menus}, search result menus are always visible to the user.
15955 * Users can choose an item from the menu or type a query into the text field to search for a matching result item.
15956 * In general, search widgets are used inside a separate {@link OO.ui.Dialog dialog} window.
15957 *
15958 * Each time the query is changed, the search result menu is cleared and repopulated. Please see
15959 * the [OOjs UI demos][1] for an example.
15960 *
15961 * [1]: https://tools.wmflabs.org/oojs-ui/oojs-ui/demos/#dialogs-mediawiki-vector-ltr
15962 *
15963 * @class
15964 * @extends OO.ui.Widget
15965 *
15966 * @constructor
15967 * @param {Object} [config] Configuration options
15968 * @cfg {string|jQuery} [placeholder] Placeholder text for query input
15969 * @cfg {string} [value] Initial query value
15970 */
15971 OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
15972 // Configuration initialization
15973 config = config || {};
15974
15975 // Parent constructor
15976 OO.ui.SearchWidget.parent.call( this, config );
15977
15978 // Properties
15979 this.query = new OO.ui.TextInputWidget( {
15980 icon: 'search',
15981 placeholder: config.placeholder,
15982 value: config.value
15983 } );
15984 this.results = new OO.ui.SelectWidget();
15985 this.$query = $( '<div>' );
15986 this.$results = $( '<div>' );
15987
15988 // Events
15989 this.query.connect( this, {
15990 change: 'onQueryChange',
15991 enter: 'onQueryEnter'
15992 } );
15993 this.query.$input.on( 'keydown', this.onQueryKeydown.bind( this ) );
15994
15995 // Initialization
15996 this.$query
15997 .addClass( 'oo-ui-searchWidget-query' )
15998 .append( this.query.$element );
15999 this.$results
16000 .addClass( 'oo-ui-searchWidget-results' )
16001 .append( this.results.$element );
16002 this.$element
16003 .addClass( 'oo-ui-searchWidget' )
16004 .append( this.$results, this.$query );
16005 };
16006
16007 /* Setup */
16008
16009 OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget );
16010
16011 /* Methods */
16012
16013 /**
16014 * Handle query key down events.
16015 *
16016 * @private
16017 * @param {jQuery.Event} e Key down event
16018 */
16019 OO.ui.SearchWidget.prototype.onQueryKeydown = function ( e ) {
16020 var highlightedItem, nextItem,
16021 dir = e.which === OO.ui.Keys.DOWN ? 1 : ( e.which === OO.ui.Keys.UP ? -1 : 0 );
16022
16023 if ( dir ) {
16024 highlightedItem = this.results.getHighlightedItem();
16025 if ( !highlightedItem ) {
16026 highlightedItem = this.results.getSelectedItem();
16027 }
16028 nextItem = this.results.getRelativeSelectableItem( highlightedItem, dir );
16029 this.results.highlightItem( nextItem );
16030 nextItem.scrollElementIntoView();
16031 }
16032 };
16033
16034 /**
16035 * Handle select widget select events.
16036 *
16037 * Clears existing results. Subclasses should repopulate items according to new query.
16038 *
16039 * @private
16040 * @param {string} value New value
16041 */
16042 OO.ui.SearchWidget.prototype.onQueryChange = function () {
16043 // Reset
16044 this.results.clearItems();
16045 };
16046
16047 /**
16048 * Handle select widget enter key events.
16049 *
16050 * Chooses highlighted item.
16051 *
16052 * @private
16053 * @param {string} value New value
16054 */
16055 OO.ui.SearchWidget.prototype.onQueryEnter = function () {
16056 // Reset
16057 this.results.chooseItem( this.results.getHighlightedItem() );
16058 };
16059
16060 /**
16061 * Get the query input.
16062 *
16063 * @return {OO.ui.TextInputWidget} Query input
16064 */
16065 OO.ui.SearchWidget.prototype.getQuery = function () {
16066 return this.query;
16067 };
16068
16069 /**
16070 * Get the search results menu.
16071 *
16072 * @return {OO.ui.SelectWidget} Menu of search results
16073 */
16074 OO.ui.SearchWidget.prototype.getResults = function () {
16075 return this.results;
16076 };
16077
16078 /**
16079 * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
16080 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
16081 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
16082 * menu selects}.
16083 *
16084 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
16085 * information, please see the [OOjs UI documentation on MediaWiki][1].
16086 *
16087 * @example
16088 * // Example of a select widget with three options
16089 * var select = new OO.ui.SelectWidget( {
16090 * items: [
16091 * new OO.ui.OptionWidget( {
16092 * data: 'a',
16093 * label: 'Option One',
16094 * } ),
16095 * new OO.ui.OptionWidget( {
16096 * data: 'b',
16097 * label: 'Option Two',
16098 * } ),
16099 * new OO.ui.OptionWidget( {
16100 * data: 'c',
16101 * label: 'Option Three',
16102 * } )
16103 * ]
16104 * } );
16105 * $( 'body' ).append( select.$element );
16106 *
16107 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
16108 *
16109 * @abstract
16110 * @class
16111 * @extends OO.ui.Widget
16112 * @mixins OO.ui.mixin.GroupElement
16113 *
16114 * @constructor
16115 * @param {Object} [config] Configuration options
16116 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
16117 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
16118 * the [OOjs UI documentation on MediaWiki] [2] for examples.
16119 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
16120 */
16121 OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
16122 // Configuration initialization
16123 config = config || {};
16124
16125 // Parent constructor
16126 OO.ui.SelectWidget.parent.call( this, config );
16127
16128 // Mixin constructors
16129 OO.ui.mixin.GroupWidget.call( this, $.extend( {}, config, { $group: this.$element } ) );
16130
16131 // Properties
16132 this.pressed = false;
16133 this.selecting = null;
16134 this.onMouseUpHandler = this.onMouseUp.bind( this );
16135 this.onMouseMoveHandler = this.onMouseMove.bind( this );
16136 this.onKeyDownHandler = this.onKeyDown.bind( this );
16137 this.onKeyPressHandler = this.onKeyPress.bind( this );
16138 this.keyPressBuffer = '';
16139 this.keyPressBufferTimer = null;
16140
16141 // Events
16142 this.connect( this, {
16143 toggle: 'onToggle'
16144 } );
16145 this.$element.on( {
16146 mousedown: this.onMouseDown.bind( this ),
16147 mouseover: this.onMouseOver.bind( this ),
16148 mouseleave: this.onMouseLeave.bind( this )
16149 } );
16150
16151 // Initialization
16152 this.$element
16153 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
16154 .attr( 'role', 'listbox' );
16155 if ( Array.isArray( config.items ) ) {
16156 this.addItems( config.items );
16157 }
16158 };
16159
16160 /* Setup */
16161
16162 OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
16163
16164 // Need to mixin base class as well
16165 OO.mixinClass( OO.ui.SelectWidget, OO.ui.mixin.GroupElement );
16166 OO.mixinClass( OO.ui.SelectWidget, OO.ui.mixin.GroupWidget );
16167
16168 /* Static */
16169 OO.ui.SelectWidget.static.passAllFilter = function () {
16170 return true;
16171 };
16172
16173 /* Events */
16174
16175 /**
16176 * @event highlight
16177 *
16178 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
16179 *
16180 * @param {OO.ui.OptionWidget|null} item Highlighted item
16181 */
16182
16183 /**
16184 * @event press
16185 *
16186 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
16187 * pressed state of an option.
16188 *
16189 * @param {OO.ui.OptionWidget|null} item Pressed item
16190 */
16191
16192 /**
16193 * @event select
16194 *
16195 * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
16196 *
16197 * @param {OO.ui.OptionWidget|null} item Selected item
16198 */
16199
16200 /**
16201 * @event choose
16202 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
16203 * @param {OO.ui.OptionWidget} item Chosen item
16204 */
16205
16206 /**
16207 * @event add
16208 *
16209 * An `add` event is emitted when options are added to the select with the #addItems method.
16210 *
16211 * @param {OO.ui.OptionWidget[]} items Added items
16212 * @param {number} index Index of insertion point
16213 */
16214
16215 /**
16216 * @event remove
16217 *
16218 * A `remove` event is emitted when options are removed from the select with the #clearItems
16219 * or #removeItems methods.
16220 *
16221 * @param {OO.ui.OptionWidget[]} items Removed items
16222 */
16223
16224 /* Methods */
16225
16226 /**
16227 * Handle mouse down events.
16228 *
16229 * @private
16230 * @param {jQuery.Event} e Mouse down event
16231 */
16232 OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
16233 var item;
16234
16235 if ( !this.isDisabled() && e.which === 1 ) {
16236 this.togglePressed( true );
16237 item = this.getTargetItem( e );
16238 if ( item && item.isSelectable() ) {
16239 this.pressItem( item );
16240 this.selecting = item;
16241 this.getElementDocument().addEventListener(
16242 'mouseup',
16243 this.onMouseUpHandler,
16244 true
16245 );
16246 this.getElementDocument().addEventListener(
16247 'mousemove',
16248 this.onMouseMoveHandler,
16249 true
16250 );
16251 }
16252 }
16253 return false;
16254 };
16255
16256 /**
16257 * Handle mouse up events.
16258 *
16259 * @private
16260 * @param {jQuery.Event} e Mouse up event
16261 */
16262 OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
16263 var item;
16264
16265 this.togglePressed( false );
16266 if ( !this.selecting ) {
16267 item = this.getTargetItem( e );
16268 if ( item && item.isSelectable() ) {
16269 this.selecting = item;
16270 }
16271 }
16272 if ( !this.isDisabled() && e.which === 1 && this.selecting ) {
16273 this.pressItem( null );
16274 this.chooseItem( this.selecting );
16275 this.selecting = null;
16276 }
16277
16278 this.getElementDocument().removeEventListener(
16279 'mouseup',
16280 this.onMouseUpHandler,
16281 true
16282 );
16283 this.getElementDocument().removeEventListener(
16284 'mousemove',
16285 this.onMouseMoveHandler,
16286 true
16287 );
16288
16289 return false;
16290 };
16291
16292 /**
16293 * Handle mouse move events.
16294 *
16295 * @private
16296 * @param {jQuery.Event} e Mouse move event
16297 */
16298 OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
16299 var item;
16300
16301 if ( !this.isDisabled() && this.pressed ) {
16302 item = this.getTargetItem( e );
16303 if ( item && item !== this.selecting && item.isSelectable() ) {
16304 this.pressItem( item );
16305 this.selecting = item;
16306 }
16307 }
16308 return false;
16309 };
16310
16311 /**
16312 * Handle mouse over events.
16313 *
16314 * @private
16315 * @param {jQuery.Event} e Mouse over event
16316 */
16317 OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
16318 var item;
16319
16320 if ( !this.isDisabled() ) {
16321 item = this.getTargetItem( e );
16322 this.highlightItem( item && item.isHighlightable() ? item : null );
16323 }
16324 return false;
16325 };
16326
16327 /**
16328 * Handle mouse leave events.
16329 *
16330 * @private
16331 * @param {jQuery.Event} e Mouse over event
16332 */
16333 OO.ui.SelectWidget.prototype.onMouseLeave = function () {
16334 if ( !this.isDisabled() ) {
16335 this.highlightItem( null );
16336 }
16337 return false;
16338 };
16339
16340 /**
16341 * Handle key down events.
16342 *
16343 * @protected
16344 * @param {jQuery.Event} e Key down event
16345 */
16346 OO.ui.SelectWidget.prototype.onKeyDown = function ( e ) {
16347 var nextItem,
16348 handled = false,
16349 currentItem = this.getHighlightedItem() || this.getSelectedItem();
16350
16351 if ( !this.isDisabled() && this.isVisible() ) {
16352 switch ( e.keyCode ) {
16353 case OO.ui.Keys.ENTER:
16354 if ( currentItem && currentItem.constructor.static.highlightable ) {
16355 // Was only highlighted, now let's select it. No-op if already selected.
16356 this.chooseItem( currentItem );
16357 handled = true;
16358 }
16359 break;
16360 case OO.ui.Keys.UP:
16361 case OO.ui.Keys.LEFT:
16362 this.clearKeyPressBuffer();
16363 nextItem = this.getRelativeSelectableItem( currentItem, -1 );
16364 handled = true;
16365 break;
16366 case OO.ui.Keys.DOWN:
16367 case OO.ui.Keys.RIGHT:
16368 this.clearKeyPressBuffer();
16369 nextItem = this.getRelativeSelectableItem( currentItem, 1 );
16370 handled = true;
16371 break;
16372 case OO.ui.Keys.ESCAPE:
16373 case OO.ui.Keys.TAB:
16374 if ( currentItem && currentItem.constructor.static.highlightable ) {
16375 currentItem.setHighlighted( false );
16376 }
16377 this.unbindKeyDownListener();
16378 this.unbindKeyPressListener();
16379 // Don't prevent tabbing away / defocusing
16380 handled = false;
16381 break;
16382 }
16383
16384 if ( nextItem ) {
16385 if ( nextItem.constructor.static.highlightable ) {
16386 this.highlightItem( nextItem );
16387 } else {
16388 this.chooseItem( nextItem );
16389 }
16390 nextItem.scrollElementIntoView();
16391 }
16392
16393 if ( handled ) {
16394 // Can't just return false, because e is not always a jQuery event
16395 e.preventDefault();
16396 e.stopPropagation();
16397 }
16398 }
16399 };
16400
16401 /**
16402 * Bind key down listener.
16403 *
16404 * @protected
16405 */
16406 OO.ui.SelectWidget.prototype.bindKeyDownListener = function () {
16407 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
16408 };
16409
16410 /**
16411 * Unbind key down listener.
16412 *
16413 * @protected
16414 */
16415 OO.ui.SelectWidget.prototype.unbindKeyDownListener = function () {
16416 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
16417 };
16418
16419 /**
16420 * Clear the key-press buffer
16421 *
16422 * @protected
16423 */
16424 OO.ui.SelectWidget.prototype.clearKeyPressBuffer = function () {
16425 if ( this.keyPressBufferTimer ) {
16426 clearTimeout( this.keyPressBufferTimer );
16427 this.keyPressBufferTimer = null;
16428 }
16429 this.keyPressBuffer = '';
16430 };
16431
16432 /**
16433 * Handle key press events.
16434 *
16435 * @protected
16436 * @param {jQuery.Event} e Key press event
16437 */
16438 OO.ui.SelectWidget.prototype.onKeyPress = function ( e ) {
16439 var c, filter, item;
16440
16441 if ( !e.charCode ) {
16442 if ( e.keyCode === OO.ui.Keys.BACKSPACE && this.keyPressBuffer !== '' ) {
16443 this.keyPressBuffer = this.keyPressBuffer.substr( 0, this.keyPressBuffer.length - 1 );
16444 return false;
16445 }
16446 return;
16447 }
16448 if ( String.fromCodePoint ) {
16449 c = String.fromCodePoint( e.charCode );
16450 } else {
16451 c = String.fromCharCode( e.charCode );
16452 }
16453
16454 if ( this.keyPressBufferTimer ) {
16455 clearTimeout( this.keyPressBufferTimer );
16456 }
16457 this.keyPressBufferTimer = setTimeout( this.clearKeyPressBuffer.bind( this ), 1500 );
16458
16459 item = this.getHighlightedItem() || this.getSelectedItem();
16460
16461 if ( this.keyPressBuffer === c ) {
16462 // Common (if weird) special case: typing "xxxx" will cycle through all
16463 // the items beginning with "x".
16464 if ( item ) {
16465 item = this.getRelativeSelectableItem( item, 1 );
16466 }
16467 } else {
16468 this.keyPressBuffer += c;
16469 }
16470
16471 filter = this.getItemMatcher( this.keyPressBuffer );
16472 if ( !item || !filter( item ) ) {
16473 item = this.getRelativeSelectableItem( item, 1, filter );
16474 }
16475 if ( item ) {
16476 if ( item.constructor.static.highlightable ) {
16477 this.highlightItem( item );
16478 } else {
16479 this.chooseItem( item );
16480 }
16481 item.scrollElementIntoView();
16482 }
16483
16484 return false;
16485 };
16486
16487 /**
16488 * Get a matcher for the specific string
16489 *
16490 * @protected
16491 * @param {string} s String to match against items
16492 * @return {Function} function ( OO.ui.OptionItem ) => boolean
16493 */
16494 OO.ui.SelectWidget.prototype.getItemMatcher = function ( s ) {
16495 var re;
16496
16497 if ( s.normalize ) {
16498 s = s.normalize();
16499 }
16500 re = new RegExp( '^\\s*' + s.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' ), 'i' );
16501 return function ( item ) {
16502 var l = item.getLabel();
16503 if ( typeof l !== 'string' ) {
16504 l = item.$label.text();
16505 }
16506 if ( l.normalize ) {
16507 l = l.normalize();
16508 }
16509 return re.test( l );
16510 };
16511 };
16512
16513 /**
16514 * Bind key press listener.
16515 *
16516 * @protected
16517 */
16518 OO.ui.SelectWidget.prototype.bindKeyPressListener = function () {
16519 this.getElementWindow().addEventListener( 'keypress', this.onKeyPressHandler, true );
16520 };
16521
16522 /**
16523 * Unbind key down listener.
16524 *
16525 * If you override this, be sure to call this.clearKeyPressBuffer() from your
16526 * implementation.
16527 *
16528 * @protected
16529 */
16530 OO.ui.SelectWidget.prototype.unbindKeyPressListener = function () {
16531 this.getElementWindow().removeEventListener( 'keypress', this.onKeyPressHandler, true );
16532 this.clearKeyPressBuffer();
16533 };
16534
16535 /**
16536 * Visibility change handler
16537 *
16538 * @protected
16539 * @param {boolean} visible
16540 */
16541 OO.ui.SelectWidget.prototype.onToggle = function ( visible ) {
16542 if ( !visible ) {
16543 this.clearKeyPressBuffer();
16544 }
16545 };
16546
16547 /**
16548 * Get the closest item to a jQuery.Event.
16549 *
16550 * @private
16551 * @param {jQuery.Event} e
16552 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
16553 */
16554 OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) {
16555 return $( e.target ).closest( '.oo-ui-optionWidget' ).data( 'oo-ui-optionWidget' ) || null;
16556 };
16557
16558 /**
16559 * Get selected item.
16560 *
16561 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
16562 */
16563 OO.ui.SelectWidget.prototype.getSelectedItem = function () {
16564 var i, len;
16565
16566 for ( i = 0, len = this.items.length; i < len; i++ ) {
16567 if ( this.items[ i ].isSelected() ) {
16568 return this.items[ i ];
16569 }
16570 }
16571 return null;
16572 };
16573
16574 /**
16575 * Get highlighted item.
16576 *
16577 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
16578 */
16579 OO.ui.SelectWidget.prototype.getHighlightedItem = function () {
16580 var i, len;
16581
16582 for ( i = 0, len = this.items.length; i < len; i++ ) {
16583 if ( this.items[ i ].isHighlighted() ) {
16584 return this.items[ i ];
16585 }
16586 }
16587 return null;
16588 };
16589
16590 /**
16591 * Toggle pressed state.
16592 *
16593 * Press is a state that occurs when a user mouses down on an item, but
16594 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
16595 * until the user releases the mouse.
16596 *
16597 * @param {boolean} pressed An option is being pressed
16598 */
16599 OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) {
16600 if ( pressed === undefined ) {
16601 pressed = !this.pressed;
16602 }
16603 if ( pressed !== this.pressed ) {
16604 this.$element
16605 .toggleClass( 'oo-ui-selectWidget-pressed', pressed )
16606 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed );
16607 this.pressed = pressed;
16608 }
16609 };
16610
16611 /**
16612 * Highlight an option. If the `item` param is omitted, no options will be highlighted
16613 * and any existing highlight will be removed. The highlight is mutually exclusive.
16614 *
16615 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
16616 * @fires highlight
16617 * @chainable
16618 */
16619 OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
16620 var i, len, highlighted,
16621 changed = false;
16622
16623 for ( i = 0, len = this.items.length; i < len; i++ ) {
16624 highlighted = this.items[ i ] === item;
16625 if ( this.items[ i ].isHighlighted() !== highlighted ) {
16626 this.items[ i ].setHighlighted( highlighted );
16627 changed = true;
16628 }
16629 }
16630 if ( changed ) {
16631 this.emit( 'highlight', item );
16632 }
16633
16634 return this;
16635 };
16636
16637 /**
16638 * Programmatically select an option by its data. If the `data` parameter is omitted,
16639 * or if the item does not exist, all options will be deselected.
16640 *
16641 * @param {Object|string} [data] Value of the item to select, omit to deselect all
16642 * @fires select
16643 * @chainable
16644 */
16645 OO.ui.SelectWidget.prototype.selectItemByData = function ( data ) {
16646 var itemFromData = this.getItemFromData( data );
16647 if ( data === undefined || !itemFromData ) {
16648 return this.selectItem();
16649 }
16650 return this.selectItem( itemFromData );
16651 };
16652
16653 /**
16654 * Programmatically select an option by its reference. If the `item` parameter is omitted,
16655 * all options will be deselected.
16656 *
16657 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
16658 * @fires select
16659 * @chainable
16660 */
16661 OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
16662 var i, len, selected,
16663 changed = false;
16664
16665 for ( i = 0, len = this.items.length; i < len; i++ ) {
16666 selected = this.items[ i ] === item;
16667 if ( this.items[ i ].isSelected() !== selected ) {
16668 this.items[ i ].setSelected( selected );
16669 changed = true;
16670 }
16671 }
16672 if ( changed ) {
16673 this.emit( 'select', item );
16674 }
16675
16676 return this;
16677 };
16678
16679 /**
16680 * Press an item.
16681 *
16682 * Press is a state that occurs when a user mouses down on an item, but has not
16683 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
16684 * releases the mouse.
16685 *
16686 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
16687 * @fires press
16688 * @chainable
16689 */
16690 OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
16691 var i, len, pressed,
16692 changed = false;
16693
16694 for ( i = 0, len = this.items.length; i < len; i++ ) {
16695 pressed = this.items[ i ] === item;
16696 if ( this.items[ i ].isPressed() !== pressed ) {
16697 this.items[ i ].setPressed( pressed );
16698 changed = true;
16699 }
16700 }
16701 if ( changed ) {
16702 this.emit( 'press', item );
16703 }
16704
16705 return this;
16706 };
16707
16708 /**
16709 * Choose an item.
16710 *
16711 * Note that ‘choose’ should never be modified programmatically. A user can choose
16712 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
16713 * use the #selectItem method.
16714 *
16715 * This method is identical to #selectItem, but may vary in subclasses that take additional action
16716 * when users choose an item with the keyboard or mouse.
16717 *
16718 * @param {OO.ui.OptionWidget} item Item to choose
16719 * @fires choose
16720 * @chainable
16721 */
16722 OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
16723 this.selectItem( item );
16724 this.emit( 'choose', item );
16725
16726 return this;
16727 };
16728
16729 /**
16730 * Get an option by its position relative to the specified item (or to the start of the option array,
16731 * if item is `null`). The direction in which to search through the option array is specified with a
16732 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
16733 * `null` if there are no options in the array.
16734 *
16735 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
16736 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
16737 * @param {Function} filter Only consider items for which this function returns
16738 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
16739 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
16740 */
16741 OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction, filter ) {
16742 var currentIndex, nextIndex, i,
16743 increase = direction > 0 ? 1 : -1,
16744 len = this.items.length;
16745
16746 if ( !$.isFunction( filter ) ) {
16747 filter = OO.ui.SelectWidget.static.passAllFilter;
16748 }
16749
16750 if ( item instanceof OO.ui.OptionWidget ) {
16751 currentIndex = $.inArray( item, this.items );
16752 nextIndex = ( currentIndex + increase + len ) % len;
16753 } else {
16754 // If no item is selected and moving forward, start at the beginning.
16755 // If moving backward, start at the end.
16756 nextIndex = direction > 0 ? 0 : len - 1;
16757 }
16758
16759 for ( i = 0; i < len; i++ ) {
16760 item = this.items[ nextIndex ];
16761 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() && filter( item ) ) {
16762 return item;
16763 }
16764 nextIndex = ( nextIndex + increase + len ) % len;
16765 }
16766 return null;
16767 };
16768
16769 /**
16770 * Get the next selectable item or `null` if there are no selectable items.
16771 * Disabled options and menu-section markers and breaks are not selectable.
16772 *
16773 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
16774 */
16775 OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
16776 var i, len, item;
16777
16778 for ( i = 0, len = this.items.length; i < len; i++ ) {
16779 item = this.items[ i ];
16780 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
16781 return item;
16782 }
16783 }
16784
16785 return null;
16786 };
16787
16788 /**
16789 * Add an array of options to the select. Optionally, an index number can be used to
16790 * specify an insertion point.
16791 *
16792 * @param {OO.ui.OptionWidget[]} items Items to add
16793 * @param {number} [index] Index to insert items after
16794 * @fires add
16795 * @chainable
16796 */
16797 OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
16798 // Mixin method
16799 OO.ui.mixin.GroupWidget.prototype.addItems.call( this, items, index );
16800
16801 // Always provide an index, even if it was omitted
16802 this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index );
16803
16804 return this;
16805 };
16806
16807 /**
16808 * Remove the specified array of options from the select. Options will be detached
16809 * from the DOM, not removed, so they can be reused later. To remove all options from
16810 * the select, you may wish to use the #clearItems method instead.
16811 *
16812 * @param {OO.ui.OptionWidget[]} items Items to remove
16813 * @fires remove
16814 * @chainable
16815 */
16816 OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
16817 var i, len, item;
16818
16819 // Deselect items being removed
16820 for ( i = 0, len = items.length; i < len; i++ ) {
16821 item = items[ i ];
16822 if ( item.isSelected() ) {
16823 this.selectItem( null );
16824 }
16825 }
16826
16827 // Mixin method
16828 OO.ui.mixin.GroupWidget.prototype.removeItems.call( this, items );
16829
16830 this.emit( 'remove', items );
16831
16832 return this;
16833 };
16834
16835 /**
16836 * Clear all options from the select. Options will be detached from the DOM, not removed,
16837 * so that they can be reused later. To remove a subset of options from the select, use
16838 * the #removeItems method.
16839 *
16840 * @fires remove
16841 * @chainable
16842 */
16843 OO.ui.SelectWidget.prototype.clearItems = function () {
16844 var items = this.items.slice();
16845
16846 // Mixin method
16847 OO.ui.mixin.GroupWidget.prototype.clearItems.call( this );
16848
16849 // Clear selection
16850 this.selectItem( null );
16851
16852 this.emit( 'remove', items );
16853
16854 return this;
16855 };
16856
16857 /**
16858 * ButtonSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains
16859 * button options and is used together with
16860 * OO.ui.ButtonOptionWidget. The ButtonSelectWidget provides an interface for
16861 * highlighting, choosing, and selecting mutually exclusive options. Please see
16862 * the [OOjs UI documentation on MediaWiki] [1] for more information.
16863 *
16864 * @example
16865 * // Example: A ButtonSelectWidget that contains three ButtonOptionWidgets
16866 * var option1 = new OO.ui.ButtonOptionWidget( {
16867 * data: 1,
16868 * label: 'Option 1',
16869 * title: 'Button option 1'
16870 * } );
16871 *
16872 * var option2 = new OO.ui.ButtonOptionWidget( {
16873 * data: 2,
16874 * label: 'Option 2',
16875 * title: 'Button option 2'
16876 * } );
16877 *
16878 * var option3 = new OO.ui.ButtonOptionWidget( {
16879 * data: 3,
16880 * label: 'Option 3',
16881 * title: 'Button option 3'
16882 * } );
16883 *
16884 * var buttonSelect=new OO.ui.ButtonSelectWidget( {
16885 * items: [ option1, option2, option3 ]
16886 * } );
16887 * $( 'body' ).append( buttonSelect.$element );
16888 *
16889 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
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.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
16899 // Parent constructor
16900 OO.ui.ButtonSelectWidget.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.addClass( 'oo-ui-buttonSelectWidget' );
16913 };
16914
16915 /* Setup */
16916
16917 OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
16918 OO.mixinClass( OO.ui.ButtonSelectWidget, OO.ui.mixin.TabIndexedElement );
16919
16920 /**
16921 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
16922 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
16923 * an interface for adding, removing and selecting options.
16924 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
16925 *
16926 * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
16927 * OO.ui.RadioSelectInputWidget instead.
16928 *
16929 * @example
16930 * // A RadioSelectWidget with RadioOptions.
16931 * var option1 = new OO.ui.RadioOptionWidget( {
16932 * data: 'a',
16933 * label: 'Selected radio option'
16934 * } );
16935 *
16936 * var option2 = new OO.ui.RadioOptionWidget( {
16937 * data: 'b',
16938 * label: 'Unselected radio option'
16939 * } );
16940 *
16941 * var radioSelect=new OO.ui.RadioSelectWidget( {
16942 * items: [ option1, option2 ]
16943 * } );
16944 *
16945 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
16946 * radioSelect.selectItem( option1 );
16947 *
16948 * $( 'body' ).append( radioSelect.$element );
16949 *
16950 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
16951
16952 *
16953 * @class
16954 * @extends OO.ui.SelectWidget
16955 * @mixins OO.ui.mixin.TabIndexedElement
16956 *
16957 * @constructor
16958 * @param {Object} [config] Configuration options
16959 */
16960 OO.ui.RadioSelectWidget = function OoUiRadioSelectWidget( config ) {
16961 // Parent constructor
16962 OO.ui.RadioSelectWidget.parent.call( this, config );
16963
16964 // Mixin constructors
16965 OO.ui.mixin.TabIndexedElement.call( this, config );
16966
16967 // Events
16968 this.$element.on( {
16969 focus: this.bindKeyDownListener.bind( this ),
16970 blur: this.unbindKeyDownListener.bind( this )
16971 } );
16972
16973 // Initialization
16974 this.$element
16975 .addClass( 'oo-ui-radioSelectWidget' )
16976 .attr( 'role', 'radiogroup' );
16977 };
16978
16979 /* Setup */
16980
16981 OO.inheritClass( OO.ui.RadioSelectWidget, OO.ui.SelectWidget );
16982 OO.mixinClass( OO.ui.RadioSelectWidget, OO.ui.mixin.TabIndexedElement );
16983
16984 /**
16985 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
16986 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
16987 * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxWidget ComboBoxWidget},
16988 * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
16989 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
16990 * and customized to be opened, closed, and displayed as needed.
16991 *
16992 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
16993 * mouse outside the menu.
16994 *
16995 * Menus also have support for keyboard interaction:
16996 *
16997 * - Enter/Return key: choose and select a menu option
16998 * - Up-arrow key: highlight the previous menu option
16999 * - Down-arrow key: highlight the next menu option
17000 * - Esc key: hide the menu
17001 *
17002 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
17003 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
17004 *
17005 * @class
17006 * @extends OO.ui.SelectWidget
17007 * @mixins OO.ui.mixin.ClippableElement
17008 *
17009 * @constructor
17010 * @param {Object} [config] Configuration options
17011 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
17012 * the text the user types. This config is used by {@link OO.ui.ComboBoxWidget ComboBoxWidget}
17013 * and {@link OO.ui.mixin.LookupElement LookupElement}
17014 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks the mouse
17015 * anywhere on the page outside of this widget, the menu is hidden. For example, if there is a button
17016 * that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
17017 * that button, unless the button (or its parent widget) is passed in here.
17018 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
17019 */
17020 OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) {
17021 // Configuration initialization
17022 config = config || {};
17023
17024 // Parent constructor
17025 OO.ui.MenuSelectWidget.parent.call( this, config );
17026
17027 // Mixin constructors
17028 OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
17029
17030 // Properties
17031 this.newItems = null;
17032 this.autoHide = config.autoHide === undefined || !!config.autoHide;
17033 this.$input = config.input ? config.input.$input : null;
17034 this.$widget = config.widget ? config.widget.$element : null;
17035 this.onDocumentMouseDownHandler = this.onDocumentMouseDown.bind( this );
17036
17037 // Initialization
17038 this.$element
17039 .addClass( 'oo-ui-menuSelectWidget' )
17040 .attr( 'role', 'menu' );
17041
17042 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
17043 // that reference properties not initialized at that time of parent class construction
17044 // TODO: Find a better way to handle post-constructor setup
17045 this.visible = false;
17046 this.$element.addClass( 'oo-ui-element-hidden' );
17047 };
17048
17049 /* Setup */
17050
17051 OO.inheritClass( OO.ui.MenuSelectWidget, OO.ui.SelectWidget );
17052 OO.mixinClass( OO.ui.MenuSelectWidget, OO.ui.mixin.ClippableElement );
17053
17054 /* Methods */
17055
17056 /**
17057 * Handles document mouse down events.
17058 *
17059 * @protected
17060 * @param {jQuery.Event} e Key down event
17061 */
17062 OO.ui.MenuSelectWidget.prototype.onDocumentMouseDown = function ( e ) {
17063 if (
17064 !OO.ui.contains( this.$element[ 0 ], e.target, true ) &&
17065 ( !this.$widget || !OO.ui.contains( this.$widget[ 0 ], e.target, true ) )
17066 ) {
17067 this.toggle( false );
17068 }
17069 };
17070
17071 /**
17072 * @inheritdoc
17073 */
17074 OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) {
17075 var currentItem = this.getHighlightedItem() || this.getSelectedItem();
17076
17077 if ( !this.isDisabled() && this.isVisible() ) {
17078 switch ( e.keyCode ) {
17079 case OO.ui.Keys.LEFT:
17080 case OO.ui.Keys.RIGHT:
17081 // Do nothing if a text field is associated, arrow keys will be handled natively
17082 if ( !this.$input ) {
17083 OO.ui.MenuSelectWidget.parent.prototype.onKeyDown.call( this, e );
17084 }
17085 break;
17086 case OO.ui.Keys.ESCAPE:
17087 case OO.ui.Keys.TAB:
17088 if ( currentItem ) {
17089 currentItem.setHighlighted( false );
17090 }
17091 this.toggle( false );
17092 // Don't prevent tabbing away, prevent defocusing
17093 if ( e.keyCode === OO.ui.Keys.ESCAPE ) {
17094 e.preventDefault();
17095 e.stopPropagation();
17096 }
17097 break;
17098 default:
17099 OO.ui.MenuSelectWidget.parent.prototype.onKeyDown.call( this, e );
17100 return;
17101 }
17102 }
17103 };
17104
17105 /**
17106 * @inheritdoc
17107 */
17108 OO.ui.MenuSelectWidget.prototype.bindKeyDownListener = function () {
17109 if ( this.$input ) {
17110 this.$input.on( 'keydown', this.onKeyDownHandler );
17111 } else {
17112 OO.ui.MenuSelectWidget.parent.prototype.bindKeyDownListener.call( this );
17113 }
17114 };
17115
17116 /**
17117 * @inheritdoc
17118 */
17119 OO.ui.MenuSelectWidget.prototype.unbindKeyDownListener = function () {
17120 if ( this.$input ) {
17121 this.$input.off( 'keydown', this.onKeyDownHandler );
17122 } else {
17123 OO.ui.MenuSelectWidget.parent.prototype.unbindKeyDownListener.call( this );
17124 }
17125 };
17126
17127 /**
17128 * @inheritdoc
17129 */
17130 OO.ui.MenuSelectWidget.prototype.bindKeyPressListener = function () {
17131 if ( !this.$input ) {
17132 OO.ui.MenuSelectWidget.parent.prototype.bindKeyPressListener.call( this );
17133 }
17134 };
17135
17136 /**
17137 * @inheritdoc
17138 */
17139 OO.ui.MenuSelectWidget.prototype.unbindKeyPressListener = function () {
17140 if ( this.$input ) {
17141 this.clearKeyPressBuffer();
17142 } else {
17143 OO.ui.MenuSelectWidget.parent.prototype.unbindKeyPressListener.call( this );
17144 }
17145 };
17146
17147 /**
17148 * Choose an item.
17149 *
17150 * When a user chooses an item, the menu is closed.
17151 *
17152 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
17153 * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
17154 * @param {OO.ui.OptionWidget} item Item to choose
17155 * @chainable
17156 */
17157 OO.ui.MenuSelectWidget.prototype.chooseItem = function ( item ) {
17158 OO.ui.MenuSelectWidget.parent.prototype.chooseItem.call( this, item );
17159 this.toggle( false );
17160 return this;
17161 };
17162
17163 /**
17164 * @inheritdoc
17165 */
17166 OO.ui.MenuSelectWidget.prototype.addItems = function ( items, index ) {
17167 var i, len, item;
17168
17169 // Parent method
17170 OO.ui.MenuSelectWidget.parent.prototype.addItems.call( this, items, index );
17171
17172 // Auto-initialize
17173 if ( !this.newItems ) {
17174 this.newItems = [];
17175 }
17176
17177 for ( i = 0, len = items.length; i < len; i++ ) {
17178 item = items[ i ];
17179 if ( this.isVisible() ) {
17180 // Defer fitting label until item has been attached
17181 item.fitLabel();
17182 } else {
17183 this.newItems.push( item );
17184 }
17185 }
17186
17187 // Reevaluate clipping
17188 this.clip();
17189
17190 return this;
17191 };
17192
17193 /**
17194 * @inheritdoc
17195 */
17196 OO.ui.MenuSelectWidget.prototype.removeItems = function ( items ) {
17197 // Parent method
17198 OO.ui.MenuSelectWidget.parent.prototype.removeItems.call( this, items );
17199
17200 // Reevaluate clipping
17201 this.clip();
17202
17203 return this;
17204 };
17205
17206 /**
17207 * @inheritdoc
17208 */
17209 OO.ui.MenuSelectWidget.prototype.clearItems = function () {
17210 // Parent method
17211 OO.ui.MenuSelectWidget.parent.prototype.clearItems.call( this );
17212
17213 // Reevaluate clipping
17214 this.clip();
17215
17216 return this;
17217 };
17218
17219 /**
17220 * @inheritdoc
17221 */
17222 OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
17223 visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
17224
17225 var i, len,
17226 change = visible !== this.isVisible();
17227
17228 // Parent method
17229 OO.ui.MenuSelectWidget.parent.prototype.toggle.call( this, visible );
17230
17231 if ( change ) {
17232 if ( visible ) {
17233 this.bindKeyDownListener();
17234 this.bindKeyPressListener();
17235
17236 if ( this.newItems && this.newItems.length ) {
17237 for ( i = 0, len = this.newItems.length; i < len; i++ ) {
17238 this.newItems[ i ].fitLabel();
17239 }
17240 this.newItems = null;
17241 }
17242 this.toggleClipping( true );
17243
17244 // Auto-hide
17245 if ( this.autoHide ) {
17246 this.getElementDocument().addEventListener(
17247 'mousedown', this.onDocumentMouseDownHandler, true
17248 );
17249 }
17250 } else {
17251 this.unbindKeyDownListener();
17252 this.unbindKeyPressListener();
17253 this.getElementDocument().removeEventListener(
17254 'mousedown', this.onDocumentMouseDownHandler, true
17255 );
17256 this.toggleClipping( false );
17257 }
17258 }
17259
17260 return this;
17261 };
17262
17263 /**
17264 * TextInputMenuSelectWidget is a menu that is specially designed to be positioned beneath
17265 * a {@link OO.ui.TextInputWidget text input} field. The menu's position is automatically
17266 * calculated and maintained when the menu is toggled or the window is resized.
17267 * See OO.ui.ComboBoxWidget for an example of a widget that uses this class.
17268 *
17269 * @class
17270 * @extends OO.ui.MenuSelectWidget
17271 *
17272 * @constructor
17273 * @param {OO.ui.TextInputWidget} inputWidget Text input widget to provide menu for
17274 * @param {Object} [config] Configuration options
17275 * @cfg {jQuery} [$container=input.$element] Element to render menu under
17276 */
17277 OO.ui.TextInputMenuSelectWidget = function OoUiTextInputMenuSelectWidget( inputWidget, config ) {
17278 // Allow passing positional parameters inside the config object
17279 if ( OO.isPlainObject( inputWidget ) && config === undefined ) {
17280 config = inputWidget;
17281 inputWidget = config.inputWidget;
17282 }
17283
17284 // Configuration initialization
17285 config = config || {};
17286
17287 // Parent constructor
17288 OO.ui.TextInputMenuSelectWidget.parent.call( this, config );
17289
17290 // Properties
17291 this.inputWidget = inputWidget;
17292 this.$container = config.$container || this.inputWidget.$element;
17293 this.onWindowResizeHandler = this.onWindowResize.bind( this );
17294
17295 // Initialization
17296 this.$element.addClass( 'oo-ui-textInputMenuSelectWidget' );
17297 };
17298
17299 /* Setup */
17300
17301 OO.inheritClass( OO.ui.TextInputMenuSelectWidget, OO.ui.MenuSelectWidget );
17302
17303 /* Methods */
17304
17305 /**
17306 * Handle window resize event.
17307 *
17308 * @private
17309 * @param {jQuery.Event} e Window resize event
17310 */
17311 OO.ui.TextInputMenuSelectWidget.prototype.onWindowResize = function () {
17312 this.position();
17313 };
17314
17315 /**
17316 * @inheritdoc
17317 */
17318 OO.ui.TextInputMenuSelectWidget.prototype.toggle = function ( visible ) {
17319 visible = visible === undefined ? !this.isVisible() : !!visible;
17320
17321 var change = visible !== this.isVisible();
17322
17323 if ( change && visible ) {
17324 // Make sure the width is set before the parent method runs.
17325 // After this we have to call this.position(); again to actually
17326 // position ourselves correctly.
17327 this.position();
17328 }
17329
17330 // Parent method
17331 OO.ui.TextInputMenuSelectWidget.parent.prototype.toggle.call( this, visible );
17332
17333 if ( change ) {
17334 if ( this.isVisible() ) {
17335 this.position();
17336 $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
17337 } else {
17338 $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
17339 }
17340 }
17341
17342 return this;
17343 };
17344
17345 /**
17346 * Position the menu.
17347 *
17348 * @private
17349 * @chainable
17350 */
17351 OO.ui.TextInputMenuSelectWidget.prototype.position = function () {
17352 var $container = this.$container,
17353 pos = OO.ui.Element.static.getRelativePosition( $container, this.$element.offsetParent() );
17354
17355 // Position under input
17356 pos.top += $container.height();
17357 this.$element.css( pos );
17358
17359 // Set width
17360 this.setIdealSize( $container.width() );
17361 // We updated the position, so re-evaluate the clipping state
17362 this.clip();
17363
17364 return this;
17365 };
17366
17367 /**
17368 * OutlineSelectWidget is a structured list that contains {@link OO.ui.OutlineOptionWidget outline options}
17369 * A set of controls can be provided with an {@link OO.ui.OutlineControlsWidget outline controls} widget.
17370 *
17371 * **Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.**
17372 *
17373 * @class
17374 * @extends OO.ui.SelectWidget
17375 * @mixins OO.ui.mixin.TabIndexedElement
17376 *
17377 * @constructor
17378 * @param {Object} [config] Configuration options
17379 */
17380 OO.ui.OutlineSelectWidget = function OoUiOutlineSelectWidget( config ) {
17381 // Parent constructor
17382 OO.ui.OutlineSelectWidget.parent.call( this, config );
17383
17384 // Mixin constructors
17385 OO.ui.mixin.TabIndexedElement.call( this, config );
17386
17387 // Events
17388 this.$element.on( {
17389 focus: this.bindKeyDownListener.bind( this ),
17390 blur: this.unbindKeyDownListener.bind( this )
17391 } );
17392
17393 // Initialization
17394 this.$element.addClass( 'oo-ui-outlineSelectWidget' );
17395 };
17396
17397 /* Setup */
17398
17399 OO.inheritClass( OO.ui.OutlineSelectWidget, OO.ui.SelectWidget );
17400 OO.mixinClass( OO.ui.OutlineSelectWidget, OO.ui.mixin.TabIndexedElement );
17401
17402 /**
17403 * TabSelectWidget is a list that contains {@link OO.ui.TabOptionWidget tab options}
17404 *
17405 * **Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}.**
17406 *
17407 * @class
17408 * @extends OO.ui.SelectWidget
17409 * @mixins OO.ui.mixin.TabIndexedElement
17410 *
17411 * @constructor
17412 * @param {Object} [config] Configuration options
17413 */
17414 OO.ui.TabSelectWidget = function OoUiTabSelectWidget( config ) {
17415 // Parent constructor
17416 OO.ui.TabSelectWidget.parent.call( this, config );
17417
17418 // Mixin constructors
17419 OO.ui.mixin.TabIndexedElement.call( this, config );
17420
17421 // Events
17422 this.$element.on( {
17423 focus: this.bindKeyDownListener.bind( this ),
17424 blur: this.unbindKeyDownListener.bind( this )
17425 } );
17426
17427 // Initialization
17428 this.$element.addClass( 'oo-ui-tabSelectWidget' );
17429 };
17430
17431 /* Setup */
17432
17433 OO.inheritClass( OO.ui.TabSelectWidget, OO.ui.SelectWidget );
17434 OO.mixinClass( OO.ui.TabSelectWidget, OO.ui.mixin.TabIndexedElement );
17435
17436 /**
17437 * NumberInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
17438 * can be entered manually) and two {@link OO.ui.ButtonWidget button widgets}
17439 * (to adjust the value in increments) to allow the user to enter a number.
17440 *
17441 * @example
17442 * // Example: A NumberInputWidget.
17443 * var numberInput = new OO.ui.NumberInputWidget( {
17444 * label: 'NumberInputWidget',
17445 * input: { value: 5, min: 1, max: 10 }
17446 * } );
17447 * $( 'body' ).append( numberInput.$element );
17448 *
17449 * @class
17450 * @extends OO.ui.Widget
17451 *
17452 * @constructor
17453 * @param {Object} [config] Configuration options
17454 * @cfg {Object} [input] Configuration options to pass to the {@link OO.ui.TextInputWidget text input widget}.
17455 * @cfg {Object} [minusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget decrementing button widget}.
17456 * @cfg {Object} [plusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget incrementing button widget}.
17457 * @cfg {boolean} [isInteger=false] Whether the field accepts only integer values.
17458 * @cfg {number} [min=-Infinity] Minimum allowed value
17459 * @cfg {number} [max=Infinity] Maximum allowed value
17460 * @cfg {number} [step=1] Delta when using the buttons or up/down arrow keys
17461 * @cfg {number|null} [pageStep] Delta when using the page-up/page-down keys. Defaults to 10 times #step.
17462 */
17463 OO.ui.NumberInputWidget = function OoUiNumberInputWidget( config ) {
17464 // Configuration initialization
17465 config = $.extend( {
17466 isInteger: false,
17467 min: -Infinity,
17468 max: Infinity,
17469 step: 1,
17470 pageStep: null
17471 }, config );
17472
17473 // Parent constructor
17474 OO.ui.NumberInputWidget.parent.call( this, config );
17475
17476 // Properties
17477 this.input = new OO.ui.TextInputWidget( $.extend(
17478 {
17479 disabled: this.isDisabled()
17480 },
17481 config.input
17482 ) );
17483 this.minusButton = new OO.ui.ButtonWidget( $.extend(
17484 {
17485 disabled: this.isDisabled(),
17486 tabIndex: -1
17487 },
17488 config.minusButton,
17489 {
17490 classes: [ 'oo-ui-numberInputWidget-minusButton' ],
17491 label: '−'
17492 }
17493 ) );
17494 this.plusButton = new OO.ui.ButtonWidget( $.extend(
17495 {
17496 disabled: this.isDisabled(),
17497 tabIndex: -1
17498 },
17499 config.plusButton,
17500 {
17501 classes: [ 'oo-ui-numberInputWidget-plusButton' ],
17502 label: '+'
17503 }
17504 ) );
17505
17506 // Events
17507 this.input.connect( this, {
17508 change: this.emit.bind( this, 'change' ),
17509 enter: this.emit.bind( this, 'enter' )
17510 } );
17511 this.input.$input.on( {
17512 keydown: this.onKeyDown.bind( this ),
17513 'wheel mousewheel DOMMouseScroll': this.onWheel.bind( this )
17514 } );
17515 this.plusButton.connect( this, {
17516 click: [ 'onButtonClick', +1 ]
17517 } );
17518 this.minusButton.connect( this, {
17519 click: [ 'onButtonClick', -1 ]
17520 } );
17521
17522 // Initialization
17523 this.setIsInteger( !!config.isInteger );
17524 this.setRange( config.min, config.max );
17525 this.setStep( config.step, config.pageStep );
17526
17527 this.$field = $( '<div>' ).addClass( 'oo-ui-numberInputWidget-field' )
17528 .append(
17529 this.minusButton.$element,
17530 this.input.$element,
17531 this.plusButton.$element
17532 );
17533 this.$element.addClass( 'oo-ui-numberInputWidget' ).append( this.$field );
17534 this.input.setValidation( this.validateNumber.bind( this ) );
17535 };
17536
17537 /* Setup */
17538
17539 OO.inheritClass( OO.ui.NumberInputWidget, OO.ui.Widget );
17540
17541 /* Events */
17542
17543 /**
17544 * A `change` event is emitted when the value of the input changes.
17545 *
17546 * @event change
17547 */
17548
17549 /**
17550 * An `enter` event is emitted when the user presses 'enter' inside the text box.
17551 *
17552 * @event enter
17553 */
17554
17555 /* Methods */
17556
17557 /**
17558 * Set whether only integers are allowed
17559 * @param {boolean} flag
17560 */
17561 OO.ui.NumberInputWidget.prototype.setIsInteger = function ( flag ) {
17562 this.isInteger = !!flag;
17563 this.input.setValidityFlag();
17564 };
17565
17566 /**
17567 * Get whether only integers are allowed
17568 * @return {boolean} Flag value
17569 */
17570 OO.ui.NumberInputWidget.prototype.getIsInteger = function () {
17571 return this.isInteger;
17572 };
17573
17574 /**
17575 * Set the range of allowed values
17576 * @param {number} min Minimum allowed value
17577 * @param {number} max Maximum allowed value
17578 */
17579 OO.ui.NumberInputWidget.prototype.setRange = function ( min, max ) {
17580 if ( min > max ) {
17581 throw new Error( 'Minimum (' + min + ') must not be greater than maximum (' + max + ')' );
17582 }
17583 this.min = min;
17584 this.max = max;
17585 this.input.setValidityFlag();
17586 };
17587
17588 /**
17589 * Get the current range
17590 * @return {number[]} Minimum and maximum values
17591 */
17592 OO.ui.NumberInputWidget.prototype.getRange = function () {
17593 return [ this.min, this.max ];
17594 };
17595
17596 /**
17597 * Set the stepping deltas
17598 * @param {number} step Normal step
17599 * @param {number|null} pageStep Page step. If null, 10 * step will be used.
17600 */
17601 OO.ui.NumberInputWidget.prototype.setStep = function ( step, pageStep ) {
17602 if ( step <= 0 ) {
17603 throw new Error( 'Step value must be positive' );
17604 }
17605 if ( pageStep === null ) {
17606 pageStep = step * 10;
17607 } else if ( pageStep <= 0 ) {
17608 throw new Error( 'Page step value must be positive' );
17609 }
17610 this.step = step;
17611 this.pageStep = pageStep;
17612 };
17613
17614 /**
17615 * Get the current stepping values
17616 * @return {number[]} Step and page step
17617 */
17618 OO.ui.NumberInputWidget.prototype.getStep = function () {
17619 return [ this.step, this.pageStep ];
17620 };
17621
17622 /**
17623 * Get the current value of the widget
17624 * @return {string}
17625 */
17626 OO.ui.NumberInputWidget.prototype.getValue = function () {
17627 return this.input.getValue();
17628 };
17629
17630 /**
17631 * Get the current value of the widget as a number
17632 * @return {number} May be NaN, or an invalid number
17633 */
17634 OO.ui.NumberInputWidget.prototype.getNumericValue = function () {
17635 return +this.input.getValue();
17636 };
17637
17638 /**
17639 * Set the value of the widget
17640 * @param {string} value Invalid values are allowed
17641 */
17642 OO.ui.NumberInputWidget.prototype.setValue = function ( value ) {
17643 this.input.setValue( value );
17644 };
17645
17646 /**
17647 * Adjust the value of the widget
17648 * @param {number} delta Adjustment amount
17649 */
17650 OO.ui.NumberInputWidget.prototype.adjustValue = function ( delta ) {
17651 var n, v = this.getNumericValue();
17652
17653 delta = +delta;
17654 if ( isNaN( delta ) || !isFinite( delta ) ) {
17655 throw new Error( 'Delta must be a finite number' );
17656 }
17657
17658 if ( isNaN( v ) ) {
17659 n = 0;
17660 } else {
17661 n = v + delta;
17662 n = Math.max( Math.min( n, this.max ), this.min );
17663 if ( this.isInteger ) {
17664 n = Math.round( n );
17665 }
17666 }
17667
17668 if ( n !== v ) {
17669 this.setValue( n );
17670 }
17671 };
17672
17673 /**
17674 * Validate input
17675 * @private
17676 * @param {string} value Field value
17677 * @return {boolean}
17678 */
17679 OO.ui.NumberInputWidget.prototype.validateNumber = function ( value ) {
17680 var n = +value;
17681 if ( isNaN( n ) || !isFinite( n ) ) {
17682 return false;
17683 }
17684
17685 /*jshint bitwise: false */
17686 if ( this.isInteger && ( n | 0 ) !== n ) {
17687 return false;
17688 }
17689 /*jshint bitwise: true */
17690
17691 if ( n < this.min || n > this.max ) {
17692 return false;
17693 }
17694
17695 return true;
17696 };
17697
17698 /**
17699 * Handle mouse click events.
17700 *
17701 * @private
17702 * @param {number} dir +1 or -1
17703 */
17704 OO.ui.NumberInputWidget.prototype.onButtonClick = function ( dir ) {
17705 this.adjustValue( dir * this.step );
17706 };
17707
17708 /**
17709 * Handle mouse wheel events.
17710 *
17711 * @private
17712 * @param {jQuery.Event} event
17713 */
17714 OO.ui.NumberInputWidget.prototype.onWheel = function ( event ) {
17715 var delta = 0;
17716
17717 // Standard 'wheel' event
17718 if ( event.originalEvent.deltaMode !== undefined ) {
17719 this.sawWheelEvent = true;
17720 }
17721 if ( event.originalEvent.deltaY ) {
17722 delta = -event.originalEvent.deltaY;
17723 } else if ( event.originalEvent.deltaX ) {
17724 delta = event.originalEvent.deltaX;
17725 }
17726
17727 // Non-standard events
17728 if ( !this.sawWheelEvent ) {
17729 if ( event.originalEvent.wheelDeltaX ) {
17730 delta = -event.originalEvent.wheelDeltaX;
17731 } else if ( event.originalEvent.wheelDeltaY ) {
17732 delta = event.originalEvent.wheelDeltaY;
17733 } else if ( event.originalEvent.wheelDelta ) {
17734 delta = event.originalEvent.wheelDelta;
17735 } else if ( event.originalEvent.detail ) {
17736 delta = -event.originalEvent.detail;
17737 }
17738 }
17739
17740 if ( delta ) {
17741 delta = delta < 0 ? -1 : 1;
17742 this.adjustValue( delta * this.step );
17743 }
17744
17745 return false;
17746 };
17747
17748 /**
17749 * Handle key down events.
17750 *
17751 *
17752 * @private
17753 * @param {jQuery.Event} e Key down event
17754 */
17755 OO.ui.NumberInputWidget.prototype.onKeyDown = function ( e ) {
17756 if ( !this.isDisabled() ) {
17757 switch ( e.which ) {
17758 case OO.ui.Keys.UP:
17759 this.adjustValue( this.step );
17760 return false;
17761 case OO.ui.Keys.DOWN:
17762 this.adjustValue( -this.step );
17763 return false;
17764 case OO.ui.Keys.PAGEUP:
17765 this.adjustValue( this.pageStep );
17766 return false;
17767 case OO.ui.Keys.PAGEDOWN:
17768 this.adjustValue( -this.pageStep );
17769 return false;
17770 }
17771 }
17772 };
17773
17774 /**
17775 * @inheritdoc
17776 */
17777 OO.ui.NumberInputWidget.prototype.setDisabled = function ( disabled ) {
17778 // Parent method
17779 OO.ui.NumberInputWidget.parent.prototype.setDisabled.call( this, disabled );
17780
17781 if ( this.input ) {
17782 this.input.setDisabled( this.isDisabled() );
17783 }
17784 if ( this.minusButton ) {
17785 this.minusButton.setDisabled( this.isDisabled() );
17786 }
17787 if ( this.plusButton ) {
17788 this.plusButton.setDisabled( this.isDisabled() );
17789 }
17790
17791 return this;
17792 };
17793
17794 /**
17795 * ToggleSwitches are switches that slide on and off. Their state is represented by a Boolean
17796 * value (`true` for ‘on’, and `false` otherwise, the default). The ‘off’ state is represented
17797 * visually by a slider in the leftmost position.
17798 *
17799 * @example
17800 * // Toggle switches in the 'off' and 'on' position.
17801 * var toggleSwitch1 = new OO.ui.ToggleSwitchWidget();
17802 * var toggleSwitch2 = new OO.ui.ToggleSwitchWidget( {
17803 * value: true
17804 * } );
17805 *
17806 * // Create a FieldsetLayout to layout and label switches
17807 * var fieldset = new OO.ui.FieldsetLayout( {
17808 * label: 'Toggle switches'
17809 * } );
17810 * fieldset.addItems( [
17811 * new OO.ui.FieldLayout( toggleSwitch1, { label: 'Off', align: 'top' } ),
17812 * new OO.ui.FieldLayout( toggleSwitch2, { label: 'On', align: 'top' } )
17813 * ] );
17814 * $( 'body' ).append( fieldset.$element );
17815 *
17816 * @class
17817 * @extends OO.ui.ToggleWidget
17818 * @mixins OO.ui.mixin.TabIndexedElement
17819 *
17820 * @constructor
17821 * @param {Object} [config] Configuration options
17822 * @cfg {boolean} [value=false] The toggle switch’s initial on/off state.
17823 * By default, the toggle switch is in the 'off' position.
17824 */
17825 OO.ui.ToggleSwitchWidget = function OoUiToggleSwitchWidget( config ) {
17826 // Parent constructor
17827 OO.ui.ToggleSwitchWidget.parent.call( this, config );
17828
17829 // Mixin constructors
17830 OO.ui.mixin.TabIndexedElement.call( this, config );
17831
17832 // Properties
17833 this.dragging = false;
17834 this.dragStart = null;
17835 this.sliding = false;
17836 this.$glow = $( '<span>' );
17837 this.$grip = $( '<span>' );
17838
17839 // Events
17840 this.$element.on( {
17841 click: this.onClick.bind( this ),
17842 keypress: this.onKeyPress.bind( this )
17843 } );
17844
17845 // Initialization
17846 this.$glow.addClass( 'oo-ui-toggleSwitchWidget-glow' );
17847 this.$grip.addClass( 'oo-ui-toggleSwitchWidget-grip' );
17848 this.$element
17849 .addClass( 'oo-ui-toggleSwitchWidget' )
17850 .attr( 'role', 'checkbox' )
17851 .append( this.$glow, this.$grip );
17852 };
17853
17854 /* Setup */
17855
17856 OO.inheritClass( OO.ui.ToggleSwitchWidget, OO.ui.ToggleWidget );
17857 OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.mixin.TabIndexedElement );
17858
17859 /* Methods */
17860
17861 /**
17862 * Handle mouse click events.
17863 *
17864 * @private
17865 * @param {jQuery.Event} e Mouse click event
17866 */
17867 OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
17868 if ( !this.isDisabled() && e.which === 1 ) {
17869 this.setValue( !this.value );
17870 }
17871 return false;
17872 };
17873
17874 /**
17875 * Handle key press events.
17876 *
17877 * @private
17878 * @param {jQuery.Event} e Key press event
17879 */
17880 OO.ui.ToggleSwitchWidget.prototype.onKeyPress = function ( e ) {
17881 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
17882 this.setValue( !this.value );
17883 return false;
17884 }
17885 };
17886
17887 /*!
17888 * Deprecated aliases for classes in the `OO.ui.mixin` namespace.
17889 */
17890
17891 /**
17892 * @inheritdoc OO.ui.mixin.ButtonElement
17893 * @deprecated Use {@link OO.ui.mixin.ButtonElement} instead.
17894 */
17895 OO.ui.ButtonElement = OO.ui.mixin.ButtonElement;
17896
17897 /**
17898 * @inheritdoc OO.ui.mixin.ClippableElement
17899 * @deprecated Use {@link OO.ui.mixin.ClippableElement} instead.
17900 */
17901 OO.ui.ClippableElement = OO.ui.mixin.ClippableElement;
17902
17903 /**
17904 * @inheritdoc OO.ui.mixin.DraggableElement
17905 * @deprecated Use {@link OO.ui.mixin.DraggableElement} instead.
17906 */
17907 OO.ui.DraggableElement = OO.ui.mixin.DraggableElement;
17908
17909 /**
17910 * @inheritdoc OO.ui.mixin.DraggableGroupElement
17911 * @deprecated Use {@link OO.ui.mixin.DraggableGroupElement} instead.
17912 */
17913 OO.ui.DraggableGroupElement = OO.ui.mixin.DraggableGroupElement;
17914
17915 /**
17916 * @inheritdoc OO.ui.mixin.FlaggedElement
17917 * @deprecated Use {@link OO.ui.mixin.FlaggedElement} instead.
17918 */
17919 OO.ui.FlaggedElement = OO.ui.mixin.FlaggedElement;
17920
17921 /**
17922 * @inheritdoc OO.ui.mixin.GroupElement
17923 * @deprecated Use {@link OO.ui.mixin.GroupElement} instead.
17924 */
17925 OO.ui.GroupElement = OO.ui.mixin.GroupElement;
17926
17927 /**
17928 * @inheritdoc OO.ui.mixin.GroupWidget
17929 * @deprecated Use {@link OO.ui.mixin.GroupWidget} instead.
17930 */
17931 OO.ui.GroupWidget = OO.ui.mixin.GroupWidget;
17932
17933 /**
17934 * @inheritdoc OO.ui.mixin.IconElement
17935 * @deprecated Use {@link OO.ui.mixin.IconElement} instead.
17936 */
17937 OO.ui.IconElement = OO.ui.mixin.IconElement;
17938
17939 /**
17940 * @inheritdoc OO.ui.mixin.IndicatorElement
17941 * @deprecated Use {@link OO.ui.mixin.IndicatorElement} instead.
17942 */
17943 OO.ui.IndicatorElement = OO.ui.mixin.IndicatorElement;
17944
17945 /**
17946 * @inheritdoc OO.ui.mixin.ItemWidget
17947 * @deprecated Use {@link OO.ui.mixin.ItemWidget} instead.
17948 */
17949 OO.ui.ItemWidget = OO.ui.mixin.ItemWidget;
17950
17951 /**
17952 * @inheritdoc OO.ui.mixin.LabelElement
17953 * @deprecated Use {@link OO.ui.mixin.LabelElement} instead.
17954 */
17955 OO.ui.LabelElement = OO.ui.mixin.LabelElement;
17956
17957 /**
17958 * @inheritdoc OO.ui.mixin.LookupElement
17959 * @deprecated Use {@link OO.ui.mixin.LookupElement} instead.
17960 */
17961 OO.ui.LookupElement = OO.ui.mixin.LookupElement;
17962
17963 /**
17964 * @inheritdoc OO.ui.mixin.PendingElement
17965 * @deprecated Use {@link OO.ui.mixin.PendingElement} instead.
17966 */
17967 OO.ui.PendingElement = OO.ui.mixin.PendingElement;
17968
17969 /**
17970 * @inheritdoc OO.ui.mixin.PopupElement
17971 * @deprecated Use {@link OO.ui.mixin.PopupElement} instead.
17972 */
17973 OO.ui.PopupElement = OO.ui.mixin.PopupElement;
17974
17975 /**
17976 * @inheritdoc OO.ui.mixin.TabIndexedElement
17977 * @deprecated Use {@link OO.ui.mixin.TabIndexedElement} instead.
17978 */
17979 OO.ui.TabIndexedElement = OO.ui.mixin.TabIndexedElement;
17980
17981 /**
17982 * @inheritdoc OO.ui.mixin.TitledElement
17983 * @deprecated Use {@link OO.ui.mixin.TitledElement} instead.
17984 */
17985 OO.ui.TitledElement = OO.ui.mixin.TitledElement;
17986
17987 }( OO ) );