Don't check namespace in SpecialWantedtemplates
[lhc/web/wiklou.git] / resources / lib / oojs-ui / oojs-ui.js
1 /*!
2 * OOjs UI v0.11.4
3 * https://www.mediawiki.org/wiki/OOjs_UI
4 *
5 * Copyright 2011–2015 OOjs Team and other contributors.
6 * Released under the MIT license
7 * http://oojs.mit-license.org
8 *
9 * Date: 2015-06-09T22:03:14Z
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.super.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.super.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.super.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.super.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.super.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.super.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.super.prototype.getActionProcess.call( this, action );
530 * };
531 * MyProcessDialog.prototype.getBodyHeight = function () {
532 * return this.panel1.$element.outerHeight( true );
533 * };
534 * var windowManager = new OO.ui.WindowManager();
535 * $( 'body' ).append( windowManager.$element );
536 * var dialog = new MyProcessDialog( {
537 * size: 'medium'
538 * } );
539 * windowManager.addWindows( [ dialog ] );
540 * windowManager.openWindow( dialog );
541 *
542 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
543 *
544 * @abstract
545 * @class
546 * @mixins OO.EventEmitter
547 *
548 * @constructor
549 * @param {Object} [config] Configuration options
550 */
551 OO.ui.ActionSet = function OoUiActionSet( config ) {
552 // Configuration initialization
553 config = config || {};
554
555 // Mixin constructors
556 OO.EventEmitter.call( this );
557
558 // Properties
559 this.list = [];
560 this.categories = {
561 actions: 'getAction',
562 flags: 'getFlags',
563 modes: 'getModes'
564 };
565 this.categorized = {};
566 this.special = {};
567 this.others = [];
568 this.organized = false;
569 this.changing = false;
570 this.changed = false;
571 };
572
573 /* Setup */
574
575 OO.mixinClass( OO.ui.ActionSet, OO.EventEmitter );
576
577 /* Static Properties */
578
579 /**
580 * Symbolic name of the flags used to identify special actions. Special actions are displayed in the
581 * header of a {@link OO.ui.ProcessDialog process dialog}.
582 * See the [OOjs UI documentation on MediaWiki][2] for more information and examples.
583 *
584 * [2]:https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
585 *
586 * @abstract
587 * @static
588 * @inheritable
589 * @property {string}
590 */
591 OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ];
592
593 /* Events */
594
595 /**
596 * @event click
597 *
598 * A 'click' event is emitted when an action is clicked.
599 *
600 * @param {OO.ui.ActionWidget} action Action that was clicked
601 */
602
603 /**
604 * @event resize
605 *
606 * A 'resize' event is emitted when an action widget is resized.
607 *
608 * @param {OO.ui.ActionWidget} action Action that was resized
609 */
610
611 /**
612 * @event add
613 *
614 * An 'add' event is emitted when actions are {@link #method-add added} to the action set.
615 *
616 * @param {OO.ui.ActionWidget[]} added Actions added
617 */
618
619 /**
620 * @event remove
621 *
622 * A 'remove' event is emitted when actions are {@link #method-remove removed}
623 * or {@link #clear cleared}.
624 *
625 * @param {OO.ui.ActionWidget[]} added Actions removed
626 */
627
628 /**
629 * @event change
630 *
631 * A 'change' event is emitted when actions are {@link #method-add added}, {@link #clear cleared},
632 * or {@link #method-remove removed} from the action set or when the {@link #setMode mode} is changed.
633 *
634 */
635
636 /* Methods */
637
638 /**
639 * Handle action change events.
640 *
641 * @private
642 * @fires change
643 */
644 OO.ui.ActionSet.prototype.onActionChange = function () {
645 this.organized = false;
646 if ( this.changing ) {
647 this.changed = true;
648 } else {
649 this.emit( 'change' );
650 }
651 };
652
653 /**
654 * Check if an action is one of the special actions.
655 *
656 * @param {OO.ui.ActionWidget} action Action to check
657 * @return {boolean} Action is special
658 */
659 OO.ui.ActionSet.prototype.isSpecial = function ( action ) {
660 var flag;
661
662 for ( flag in this.special ) {
663 if ( action === this.special[ flag ] ) {
664 return true;
665 }
666 }
667
668 return false;
669 };
670
671 /**
672 * Get action widgets based on the specified filter: ‘actions’, ‘flags’, ‘modes’, ‘visible’,
673 * or ‘disabled’.
674 *
675 * @param {Object} [filters] Filters to use, omit to get all actions
676 * @param {string|string[]} [filters.actions] Actions that action widgets must have
677 * @param {string|string[]} [filters.flags] Flags that action widgets must have (e.g., 'safe')
678 * @param {string|string[]} [filters.modes] Modes that action widgets must have
679 * @param {boolean} [filters.visible] Action widgets must be visible
680 * @param {boolean} [filters.disabled] Action widgets must be disabled
681 * @return {OO.ui.ActionWidget[]} Action widgets matching all criteria
682 */
683 OO.ui.ActionSet.prototype.get = function ( filters ) {
684 var i, len, list, category, actions, index, match, matches;
685
686 if ( filters ) {
687 this.organize();
688
689 // Collect category candidates
690 matches = [];
691 for ( category in this.categorized ) {
692 list = filters[ category ];
693 if ( list ) {
694 if ( !Array.isArray( list ) ) {
695 list = [ list ];
696 }
697 for ( i = 0, len = list.length; i < len; i++ ) {
698 actions = this.categorized[ category ][ list[ i ] ];
699 if ( Array.isArray( actions ) ) {
700 matches.push.apply( matches, actions );
701 }
702 }
703 }
704 }
705 // Remove by boolean filters
706 for ( i = 0, len = matches.length; i < len; i++ ) {
707 match = matches[ i ];
708 if (
709 ( filters.visible !== undefined && match.isVisible() !== filters.visible ) ||
710 ( filters.disabled !== undefined && match.isDisabled() !== filters.disabled )
711 ) {
712 matches.splice( i, 1 );
713 len--;
714 i--;
715 }
716 }
717 // Remove duplicates
718 for ( i = 0, len = matches.length; i < len; i++ ) {
719 match = matches[ i ];
720 index = matches.lastIndexOf( match );
721 while ( index !== i ) {
722 matches.splice( index, 1 );
723 len--;
724 index = matches.lastIndexOf( match );
725 }
726 }
727 return matches;
728 }
729 return this.list.slice();
730 };
731
732 /**
733 * Get 'special' actions.
734 *
735 * Special actions are the first visible action widgets with special flags, such as 'safe' and 'primary'.
736 * Special flags can be configured in subclasses by changing the static #specialFlags property.
737 *
738 * @return {OO.ui.ActionWidget[]|null} 'Special' action widgets.
739 */
740 OO.ui.ActionSet.prototype.getSpecial = function () {
741 this.organize();
742 return $.extend( {}, this.special );
743 };
744
745 /**
746 * Get 'other' actions.
747 *
748 * Other actions include all non-special visible action widgets.
749 *
750 * @return {OO.ui.ActionWidget[]} 'Other' action widgets
751 */
752 OO.ui.ActionSet.prototype.getOthers = function () {
753 this.organize();
754 return this.others.slice();
755 };
756
757 /**
758 * Set the mode (e.g., ‘edit’ or ‘view’). Only {@link OO.ui.ActionWidget#modes actions} configured
759 * to be available in the specified mode will be made visible. All other actions will be hidden.
760 *
761 * @param {string} mode The mode. Only actions configured to be available in the specified
762 * mode will be made visible.
763 * @chainable
764 * @fires toggle
765 * @fires change
766 */
767 OO.ui.ActionSet.prototype.setMode = function ( mode ) {
768 var i, len, action;
769
770 this.changing = true;
771 for ( i = 0, len = this.list.length; i < len; i++ ) {
772 action = this.list[ i ];
773 action.toggle( action.hasMode( mode ) );
774 }
775
776 this.organized = false;
777 this.changing = false;
778 this.emit( 'change' );
779
780 return this;
781 };
782
783 /**
784 * Set the abilities of the specified actions.
785 *
786 * Action widgets that are configured with the specified actions will be enabled
787 * or disabled based on the boolean values specified in the `actions`
788 * parameter.
789 *
790 * @param {Object.<string,boolean>} actions A list keyed by action name with boolean
791 * values that indicate whether or not the action should be enabled.
792 * @chainable
793 */
794 OO.ui.ActionSet.prototype.setAbilities = function ( actions ) {
795 var i, len, action, item;
796
797 for ( i = 0, len = this.list.length; i < len; i++ ) {
798 item = this.list[ i ];
799 action = item.getAction();
800 if ( actions[ action ] !== undefined ) {
801 item.setDisabled( !actions[ action ] );
802 }
803 }
804
805 return this;
806 };
807
808 /**
809 * Executes a function once per action.
810 *
811 * When making changes to multiple actions, use this method instead of iterating over the actions
812 * manually to defer emitting a #change event until after all actions have been changed.
813 *
814 * @param {Object|null} actions Filters to use to determine which actions to iterate over; see #get
815 * @param {Function} callback Callback to run for each action; callback is invoked with three
816 * arguments: the action, the action's index, the list of actions being iterated over
817 * @chainable
818 */
819 OO.ui.ActionSet.prototype.forEach = function ( filter, callback ) {
820 this.changed = false;
821 this.changing = true;
822 this.get( filter ).forEach( callback );
823 this.changing = false;
824 if ( this.changed ) {
825 this.emit( 'change' );
826 }
827
828 return this;
829 };
830
831 /**
832 * Add action widgets to the action set.
833 *
834 * @param {OO.ui.ActionWidget[]} actions Action widgets to add
835 * @chainable
836 * @fires add
837 * @fires change
838 */
839 OO.ui.ActionSet.prototype.add = function ( actions ) {
840 var i, len, action;
841
842 this.changing = true;
843 for ( i = 0, len = actions.length; i < len; i++ ) {
844 action = actions[ i ];
845 action.connect( this, {
846 click: [ 'emit', 'click', action ],
847 resize: [ 'emit', 'resize', action ],
848 toggle: [ 'onActionChange' ]
849 } );
850 this.list.push( action );
851 }
852 this.organized = false;
853 this.emit( 'add', actions );
854 this.changing = false;
855 this.emit( 'change' );
856
857 return this;
858 };
859
860 /**
861 * Remove action widgets from the set.
862 *
863 * To remove all actions, you may wish to use the #clear method instead.
864 *
865 * @param {OO.ui.ActionWidget[]} actions Action widgets to remove
866 * @chainable
867 * @fires remove
868 * @fires change
869 */
870 OO.ui.ActionSet.prototype.remove = function ( actions ) {
871 var i, len, index, action;
872
873 this.changing = true;
874 for ( i = 0, len = actions.length; i < len; i++ ) {
875 action = actions[ i ];
876 index = this.list.indexOf( action );
877 if ( index !== -1 ) {
878 action.disconnect( this );
879 this.list.splice( index, 1 );
880 }
881 }
882 this.organized = false;
883 this.emit( 'remove', actions );
884 this.changing = false;
885 this.emit( 'change' );
886
887 return this;
888 };
889
890 /**
891 * Remove all action widets from the set.
892 *
893 * To remove only specified actions, use the {@link #method-remove remove} method instead.
894 *
895 * @chainable
896 * @fires remove
897 * @fires change
898 */
899 OO.ui.ActionSet.prototype.clear = function () {
900 var i, len, action,
901 removed = this.list.slice();
902
903 this.changing = true;
904 for ( i = 0, len = this.list.length; i < len; i++ ) {
905 action = this.list[ i ];
906 action.disconnect( this );
907 }
908
909 this.list = [];
910
911 this.organized = false;
912 this.emit( 'remove', removed );
913 this.changing = false;
914 this.emit( 'change' );
915
916 return this;
917 };
918
919 /**
920 * Organize actions.
921 *
922 * This is called whenever organized information is requested. It will only reorganize the actions
923 * if something has changed since the last time it ran.
924 *
925 * @private
926 * @chainable
927 */
928 OO.ui.ActionSet.prototype.organize = function () {
929 var i, iLen, j, jLen, flag, action, category, list, item, special,
930 specialFlags = this.constructor.static.specialFlags;
931
932 if ( !this.organized ) {
933 this.categorized = {};
934 this.special = {};
935 this.others = [];
936 for ( i = 0, iLen = this.list.length; i < iLen; i++ ) {
937 action = this.list[ i ];
938 if ( action.isVisible() ) {
939 // Populate categories
940 for ( category in this.categories ) {
941 if ( !this.categorized[ category ] ) {
942 this.categorized[ category ] = {};
943 }
944 list = action[ this.categories[ category ] ]();
945 if ( !Array.isArray( list ) ) {
946 list = [ list ];
947 }
948 for ( j = 0, jLen = list.length; j < jLen; j++ ) {
949 item = list[ j ];
950 if ( !this.categorized[ category ][ item ] ) {
951 this.categorized[ category ][ item ] = [];
952 }
953 this.categorized[ category ][ item ].push( action );
954 }
955 }
956 // Populate special/others
957 special = false;
958 for ( j = 0, jLen = specialFlags.length; j < jLen; j++ ) {
959 flag = specialFlags[ j ];
960 if ( !this.special[ flag ] && action.hasFlag( flag ) ) {
961 this.special[ flag ] = action;
962 special = true;
963 break;
964 }
965 }
966 if ( !special ) {
967 this.others.push( action );
968 }
969 }
970 }
971 this.organized = true;
972 }
973
974 return this;
975 };
976
977 /**
978 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
979 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
980 * connected to them and can't be interacted with.
981 *
982 * @abstract
983 * @class
984 *
985 * @constructor
986 * @param {Object} [config] Configuration options
987 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
988 * to the top level (e.g., the outermost div) of the element. See the [OOjs UI documentation on MediaWiki][2]
989 * for an example.
990 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#cssExample
991 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
992 * @cfg {string} [text] Text to insert
993 * @cfg {Array} [content] An array of content elements to append (after #text).
994 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
995 * Instances of OO.ui.Element will have their $element appended.
996 * @cfg {jQuery} [$content] Content elements to append (after #text)
997 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
998 * Data can also be specified with the #setData method.
999 */
1000 OO.ui.Element = function OoUiElement( config ) {
1001 // Configuration initialization
1002 config = config || {};
1003
1004 // Properties
1005 this.$ = $;
1006 this.visible = true;
1007 this.data = config.data;
1008 this.$element = config.$element ||
1009 $( document.createElement( this.getTagName() ) );
1010 this.elementGroup = null;
1011 this.debouncedUpdateThemeClassesHandler = this.debouncedUpdateThemeClasses.bind( this );
1012 this.updateThemeClassesPending = false;
1013
1014 // Initialization
1015 if ( Array.isArray( config.classes ) ) {
1016 this.$element.addClass( config.classes.join( ' ' ) );
1017 }
1018 if ( config.id ) {
1019 this.$element.attr( 'id', config.id );
1020 }
1021 if ( config.text ) {
1022 this.$element.text( config.text );
1023 }
1024 if ( config.content ) {
1025 // The `content` property treats plain strings as text; use an
1026 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
1027 // appropriate $element appended.
1028 this.$element.append( config.content.map( function ( v ) {
1029 if ( typeof v === 'string' ) {
1030 // Escape string so it is properly represented in HTML.
1031 return document.createTextNode( v );
1032 } else if ( v instanceof OO.ui.HtmlSnippet ) {
1033 // Bypass escaping.
1034 return v.toString();
1035 } else if ( v instanceof OO.ui.Element ) {
1036 return v.$element;
1037 }
1038 return v;
1039 } ) );
1040 }
1041 if ( config.$content ) {
1042 // The `$content` property treats plain strings as HTML.
1043 this.$element.append( config.$content );
1044 }
1045 };
1046
1047 /* Setup */
1048
1049 OO.initClass( OO.ui.Element );
1050
1051 /* Static Properties */
1052
1053 /**
1054 * The name of the HTML tag used by the element.
1055 *
1056 * The static value may be ignored if the #getTagName method is overridden.
1057 *
1058 * @static
1059 * @inheritable
1060 * @property {string}
1061 */
1062 OO.ui.Element.static.tagName = 'div';
1063
1064 /* Static Methods */
1065
1066 /**
1067 * Reconstitute a JavaScript object corresponding to a widget created
1068 * by the PHP implementation.
1069 *
1070 * @param {string|HTMLElement|jQuery} idOrNode
1071 * A DOM id (if a string) or node for the widget to infuse.
1072 * @return {OO.ui.Element}
1073 * The `OO.ui.Element` corresponding to this (infusable) document node.
1074 * For `Tag` objects emitted on the HTML side (used occasionally for content)
1075 * the value returned is a newly-created Element wrapping around the existing
1076 * DOM node.
1077 */
1078 OO.ui.Element.static.infuse = function ( idOrNode ) {
1079 var obj = OO.ui.Element.static.unsafeInfuse( idOrNode, true );
1080 // Verify that the type matches up.
1081 // FIXME: uncomment after T89721 is fixed (see T90929)
1082 /*
1083 if ( !( obj instanceof this['class'] ) ) {
1084 throw new Error( 'Infusion type mismatch!' );
1085 }
1086 */
1087 return obj;
1088 };
1089
1090 /**
1091 * Implementation helper for `infuse`; skips the type check and has an
1092 * extra property so that only the top-level invocation touches the DOM.
1093 * @private
1094 * @param {string|HTMLElement|jQuery} idOrNode
1095 * @param {boolean} top True only for top-level invocation.
1096 * @return {OO.ui.Element}
1097 */
1098 OO.ui.Element.static.unsafeInfuse = function ( idOrNode, top ) {
1099 // look for a cached result of a previous infusion.
1100 var id, $elem, data, cls, obj;
1101 if ( typeof idOrNode === 'string' ) {
1102 id = idOrNode;
1103 $elem = $( document.getElementById( id ) );
1104 } else {
1105 $elem = $( idOrNode );
1106 id = $elem.attr( 'id' );
1107 }
1108 data = $elem.data( 'ooui-infused' );
1109 if ( data ) {
1110 // cached!
1111 if ( data === true ) {
1112 throw new Error( 'Circular dependency! ' + id );
1113 }
1114 return data;
1115 }
1116 if ( !$elem.length ) {
1117 throw new Error( 'Widget not found: ' + id );
1118 }
1119 data = $elem.attr( 'data-ooui' );
1120 if ( !data ) {
1121 throw new Error( 'No infusion data found: ' + id );
1122 }
1123 try {
1124 data = $.parseJSON( data );
1125 } catch ( _ ) {
1126 data = null;
1127 }
1128 if ( !( data && data._ ) ) {
1129 throw new Error( 'No valid infusion data found: ' + id );
1130 }
1131 if ( data._ === 'Tag' ) {
1132 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
1133 return new OO.ui.Element( { $element: $elem } );
1134 }
1135 cls = OO.ui[data._];
1136 if ( !cls ) {
1137 throw new Error( 'Unknown widget type: ' + id );
1138 }
1139 $elem.data( 'ooui-infused', true ); // prevent loops
1140 data.id = id; // implicit
1141 data = OO.copy( data, null, function deserialize( value ) {
1142 if ( OO.isPlainObject( value ) ) {
1143 if ( value.tag ) {
1144 return OO.ui.Element.static.unsafeInfuse( value.tag, false );
1145 }
1146 if ( value.html ) {
1147 return new OO.ui.HtmlSnippet( value.html );
1148 }
1149 }
1150 } );
1151 // jscs:disable requireCapitalizedConstructors
1152 obj = new cls( data ); // rebuild widget
1153 // now replace old DOM with this new DOM.
1154 if ( top ) {
1155 $elem.replaceWith( obj.$element );
1156 }
1157 obj.$element.data( 'ooui-infused', obj );
1158 // set the 'data-ooui' attribute so we can identify infused widgets
1159 obj.$element.attr( 'data-ooui', '' );
1160 return obj;
1161 };
1162
1163 /**
1164 * Get a jQuery function within a specific document.
1165 *
1166 * @static
1167 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
1168 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
1169 * not in an iframe
1170 * @return {Function} Bound jQuery function
1171 */
1172 OO.ui.Element.static.getJQuery = function ( context, $iframe ) {
1173 function wrapper( selector ) {
1174 return $( selector, wrapper.context );
1175 }
1176
1177 wrapper.context = this.getDocument( context );
1178
1179 if ( $iframe ) {
1180 wrapper.$iframe = $iframe;
1181 }
1182
1183 return wrapper;
1184 };
1185
1186 /**
1187 * Get the document of an element.
1188 *
1189 * @static
1190 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
1191 * @return {HTMLDocument|null} Document object
1192 */
1193 OO.ui.Element.static.getDocument = function ( obj ) {
1194 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
1195 return ( obj[ 0 ] && obj[ 0 ].ownerDocument ) ||
1196 // Empty jQuery selections might have a context
1197 obj.context ||
1198 // HTMLElement
1199 obj.ownerDocument ||
1200 // Window
1201 obj.document ||
1202 // HTMLDocument
1203 ( obj.nodeType === 9 && obj ) ||
1204 null;
1205 };
1206
1207 /**
1208 * Get the window of an element or document.
1209 *
1210 * @static
1211 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
1212 * @return {Window} Window object
1213 */
1214 OO.ui.Element.static.getWindow = function ( obj ) {
1215 var doc = this.getDocument( obj );
1216 return doc.parentWindow || doc.defaultView;
1217 };
1218
1219 /**
1220 * Get the direction of an element or document.
1221 *
1222 * @static
1223 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
1224 * @return {string} Text direction, either 'ltr' or 'rtl'
1225 */
1226 OO.ui.Element.static.getDir = function ( obj ) {
1227 var isDoc, isWin;
1228
1229 if ( obj instanceof jQuery ) {
1230 obj = obj[ 0 ];
1231 }
1232 isDoc = obj.nodeType === 9;
1233 isWin = obj.document !== undefined;
1234 if ( isDoc || isWin ) {
1235 if ( isWin ) {
1236 obj = obj.document;
1237 }
1238 obj = obj.body;
1239 }
1240 return $( obj ).css( 'direction' );
1241 };
1242
1243 /**
1244 * Get the offset between two frames.
1245 *
1246 * TODO: Make this function not use recursion.
1247 *
1248 * @static
1249 * @param {Window} from Window of the child frame
1250 * @param {Window} [to=window] Window of the parent frame
1251 * @param {Object} [offset] Offset to start with, used internally
1252 * @return {Object} Offset object, containing left and top properties
1253 */
1254 OO.ui.Element.static.getFrameOffset = function ( from, to, offset ) {
1255 var i, len, frames, frame, rect;
1256
1257 if ( !to ) {
1258 to = window;
1259 }
1260 if ( !offset ) {
1261 offset = { top: 0, left: 0 };
1262 }
1263 if ( from.parent === from ) {
1264 return offset;
1265 }
1266
1267 // Get iframe element
1268 frames = from.parent.document.getElementsByTagName( 'iframe' );
1269 for ( i = 0, len = frames.length; i < len; i++ ) {
1270 if ( frames[ i ].contentWindow === from ) {
1271 frame = frames[ i ];
1272 break;
1273 }
1274 }
1275
1276 // Recursively accumulate offset values
1277 if ( frame ) {
1278 rect = frame.getBoundingClientRect();
1279 offset.left += rect.left;
1280 offset.top += rect.top;
1281 if ( from !== to ) {
1282 this.getFrameOffset( from.parent, offset );
1283 }
1284 }
1285 return offset;
1286 };
1287
1288 /**
1289 * Get the offset between two elements.
1290 *
1291 * The two elements may be in a different frame, but in that case the frame $element is in must
1292 * be contained in the frame $anchor is in.
1293 *
1294 * @static
1295 * @param {jQuery} $element Element whose position to get
1296 * @param {jQuery} $anchor Element to get $element's position relative to
1297 * @return {Object} Translated position coordinates, containing top and left properties
1298 */
1299 OO.ui.Element.static.getRelativePosition = function ( $element, $anchor ) {
1300 var iframe, iframePos,
1301 pos = $element.offset(),
1302 anchorPos = $anchor.offset(),
1303 elementDocument = this.getDocument( $element ),
1304 anchorDocument = this.getDocument( $anchor );
1305
1306 // If $element isn't in the same document as $anchor, traverse up
1307 while ( elementDocument !== anchorDocument ) {
1308 iframe = elementDocument.defaultView.frameElement;
1309 if ( !iframe ) {
1310 throw new Error( '$element frame is not contained in $anchor frame' );
1311 }
1312 iframePos = $( iframe ).offset();
1313 pos.left += iframePos.left;
1314 pos.top += iframePos.top;
1315 elementDocument = iframe.ownerDocument;
1316 }
1317 pos.left -= anchorPos.left;
1318 pos.top -= anchorPos.top;
1319 return pos;
1320 };
1321
1322 /**
1323 * Get element border sizes.
1324 *
1325 * @static
1326 * @param {HTMLElement} el Element to measure
1327 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1328 */
1329 OO.ui.Element.static.getBorders = function ( el ) {
1330 var doc = el.ownerDocument,
1331 win = doc.parentWindow || doc.defaultView,
1332 style = win && win.getComputedStyle ?
1333 win.getComputedStyle( el, null ) :
1334 el.currentStyle,
1335 $el = $( el ),
1336 top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0,
1337 left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0,
1338 bottom = parseFloat( style ? style.borderBottomWidth : $el.css( 'borderBottomWidth' ) ) || 0,
1339 right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0;
1340
1341 return {
1342 top: top,
1343 left: left,
1344 bottom: bottom,
1345 right: right
1346 };
1347 };
1348
1349 /**
1350 * Get dimensions of an element or window.
1351 *
1352 * @static
1353 * @param {HTMLElement|Window} el Element to measure
1354 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1355 */
1356 OO.ui.Element.static.getDimensions = function ( el ) {
1357 var $el, $win,
1358 doc = el.ownerDocument || el.document,
1359 win = doc.parentWindow || doc.defaultView;
1360
1361 if ( win === el || el === doc.documentElement ) {
1362 $win = $( win );
1363 return {
1364 borders: { top: 0, left: 0, bottom: 0, right: 0 },
1365 scroll: {
1366 top: $win.scrollTop(),
1367 left: $win.scrollLeft()
1368 },
1369 scrollbar: { right: 0, bottom: 0 },
1370 rect: {
1371 top: 0,
1372 left: 0,
1373 bottom: $win.innerHeight(),
1374 right: $win.innerWidth()
1375 }
1376 };
1377 } else {
1378 $el = $( el );
1379 return {
1380 borders: this.getBorders( el ),
1381 scroll: {
1382 top: $el.scrollTop(),
1383 left: $el.scrollLeft()
1384 },
1385 scrollbar: {
1386 right: $el.innerWidth() - el.clientWidth,
1387 bottom: $el.innerHeight() - el.clientHeight
1388 },
1389 rect: el.getBoundingClientRect()
1390 };
1391 }
1392 };
1393
1394 /**
1395 * Get scrollable object parent
1396 *
1397 * documentElement can't be used to get or set the scrollTop
1398 * property on Blink. Changing and testing its value lets us
1399 * use 'body' or 'documentElement' based on what is working.
1400 *
1401 * https://code.google.com/p/chromium/issues/detail?id=303131
1402 *
1403 * @static
1404 * @param {HTMLElement} el Element to find scrollable parent for
1405 * @return {HTMLElement} Scrollable parent
1406 */
1407 OO.ui.Element.static.getRootScrollableElement = function ( el ) {
1408 var scrollTop, body;
1409
1410 if ( OO.ui.scrollableElement === undefined ) {
1411 body = el.ownerDocument.body;
1412 scrollTop = body.scrollTop;
1413 body.scrollTop = 1;
1414
1415 if ( body.scrollTop === 1 ) {
1416 body.scrollTop = scrollTop;
1417 OO.ui.scrollableElement = 'body';
1418 } else {
1419 OO.ui.scrollableElement = 'documentElement';
1420 }
1421 }
1422
1423 return el.ownerDocument[ OO.ui.scrollableElement ];
1424 };
1425
1426 /**
1427 * Get closest scrollable container.
1428 *
1429 * Traverses up until either a scrollable element or the root is reached, in which case the window
1430 * will be returned.
1431 *
1432 * @static
1433 * @param {HTMLElement} el Element to find scrollable container for
1434 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1435 * @return {HTMLElement} Closest scrollable container
1436 */
1437 OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) {
1438 var i, val,
1439 props = [ 'overflow' ],
1440 $parent = $( el ).parent();
1441
1442 if ( dimension === 'x' || dimension === 'y' ) {
1443 props.push( 'overflow-' + dimension );
1444 }
1445
1446 while ( $parent.length ) {
1447 if ( $parent[ 0 ] === this.getRootScrollableElement( el ) ) {
1448 return $parent[ 0 ];
1449 }
1450 i = props.length;
1451 while ( i-- ) {
1452 val = $parent.css( props[ i ] );
1453 if ( val === 'auto' || val === 'scroll' ) {
1454 return $parent[ 0 ];
1455 }
1456 }
1457 $parent = $parent.parent();
1458 }
1459 return this.getDocument( el ).body;
1460 };
1461
1462 /**
1463 * Scroll element into view.
1464 *
1465 * @static
1466 * @param {HTMLElement} el Element to scroll into view
1467 * @param {Object} [config] Configuration options
1468 * @param {string} [config.duration] jQuery animation duration value
1469 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1470 * to scroll in both directions
1471 * @param {Function} [config.complete] Function to call when scrolling completes
1472 */
1473 OO.ui.Element.static.scrollIntoView = function ( el, config ) {
1474 // Configuration initialization
1475 config = config || {};
1476
1477 var rel, anim = {},
1478 callback = typeof config.complete === 'function' && config.complete,
1479 sc = this.getClosestScrollableContainer( el, config.direction ),
1480 $sc = $( sc ),
1481 eld = this.getDimensions( el ),
1482 scd = this.getDimensions( sc ),
1483 $win = $( this.getWindow( el ) );
1484
1485 // Compute the distances between the edges of el and the edges of the scroll viewport
1486 if ( $sc.is( 'html, body' ) ) {
1487 // If the scrollable container is the root, this is easy
1488 rel = {
1489 top: eld.rect.top,
1490 bottom: $win.innerHeight() - eld.rect.bottom,
1491 left: eld.rect.left,
1492 right: $win.innerWidth() - eld.rect.right
1493 };
1494 } else {
1495 // Otherwise, we have to subtract el's coordinates from sc's coordinates
1496 rel = {
1497 top: eld.rect.top - ( scd.rect.top + scd.borders.top ),
1498 bottom: scd.rect.bottom - scd.borders.bottom - scd.scrollbar.bottom - eld.rect.bottom,
1499 left: eld.rect.left - ( scd.rect.left + scd.borders.left ),
1500 right: scd.rect.right - scd.borders.right - scd.scrollbar.right - eld.rect.right
1501 };
1502 }
1503
1504 if ( !config.direction || config.direction === 'y' ) {
1505 if ( rel.top < 0 ) {
1506 anim.scrollTop = scd.scroll.top + rel.top;
1507 } else if ( rel.top > 0 && rel.bottom < 0 ) {
1508 anim.scrollTop = scd.scroll.top + Math.min( rel.top, -rel.bottom );
1509 }
1510 }
1511 if ( !config.direction || config.direction === 'x' ) {
1512 if ( rel.left < 0 ) {
1513 anim.scrollLeft = scd.scroll.left + rel.left;
1514 } else if ( rel.left > 0 && rel.right < 0 ) {
1515 anim.scrollLeft = scd.scroll.left + Math.min( rel.left, -rel.right );
1516 }
1517 }
1518 if ( !$.isEmptyObject( anim ) ) {
1519 $sc.stop( true ).animate( anim, config.duration || 'fast' );
1520 if ( callback ) {
1521 $sc.queue( function ( next ) {
1522 callback();
1523 next();
1524 } );
1525 }
1526 } else {
1527 if ( callback ) {
1528 callback();
1529 }
1530 }
1531 };
1532
1533 /**
1534 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1535 * and reserve space for them, because it probably doesn't.
1536 *
1537 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1538 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1539 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1540 * and then reattach (or show) them back.
1541 *
1542 * @static
1543 * @param {HTMLElement} el Element to reconsider the scrollbars on
1544 */
1545 OO.ui.Element.static.reconsiderScrollbars = function ( el ) {
1546 var i, len, scrollLeft, scrollTop, nodes = [];
1547 // Save scroll position
1548 scrollLeft = el.scrollLeft;
1549 scrollTop = el.scrollTop;
1550 // Detach all children
1551 while ( el.firstChild ) {
1552 nodes.push( el.firstChild );
1553 el.removeChild( el.firstChild );
1554 }
1555 // Force reflow
1556 void el.offsetHeight;
1557 // Reattach all children
1558 for ( i = 0, len = nodes.length; i < len; i++ ) {
1559 el.appendChild( nodes[ i ] );
1560 }
1561 // Restore scroll position (no-op if scrollbars disappeared)
1562 el.scrollLeft = scrollLeft;
1563 el.scrollTop = scrollTop;
1564 };
1565
1566 /* Methods */
1567
1568 /**
1569 * Toggle visibility of an element.
1570 *
1571 * @param {boolean} [show] Make element visible, omit to toggle visibility
1572 * @fires visible
1573 * @chainable
1574 */
1575 OO.ui.Element.prototype.toggle = function ( show ) {
1576 show = show === undefined ? !this.visible : !!show;
1577
1578 if ( show !== this.isVisible() ) {
1579 this.visible = show;
1580 this.$element.toggleClass( 'oo-ui-element-hidden', !this.visible );
1581 this.emit( 'toggle', show );
1582 }
1583
1584 return this;
1585 };
1586
1587 /**
1588 * Check if element is visible.
1589 *
1590 * @return {boolean} element is visible
1591 */
1592 OO.ui.Element.prototype.isVisible = function () {
1593 return this.visible;
1594 };
1595
1596 /**
1597 * Get element data.
1598 *
1599 * @return {Mixed} Element data
1600 */
1601 OO.ui.Element.prototype.getData = function () {
1602 return this.data;
1603 };
1604
1605 /**
1606 * Set element data.
1607 *
1608 * @param {Mixed} Element data
1609 * @chainable
1610 */
1611 OO.ui.Element.prototype.setData = function ( data ) {
1612 this.data = data;
1613 return this;
1614 };
1615
1616 /**
1617 * Check if element supports one or more methods.
1618 *
1619 * @param {string|string[]} methods Method or list of methods to check
1620 * @return {boolean} All methods are supported
1621 */
1622 OO.ui.Element.prototype.supports = function ( methods ) {
1623 var i, len,
1624 support = 0;
1625
1626 methods = Array.isArray( methods ) ? methods : [ methods ];
1627 for ( i = 0, len = methods.length; i < len; i++ ) {
1628 if ( $.isFunction( this[ methods[ i ] ] ) ) {
1629 support++;
1630 }
1631 }
1632
1633 return methods.length === support;
1634 };
1635
1636 /**
1637 * Update the theme-provided classes.
1638 *
1639 * @localdoc This is called in element mixins and widget classes any time state changes.
1640 * Updating is debounced, minimizing overhead of changing multiple attributes and
1641 * guaranteeing that theme updates do not occur within an element's constructor
1642 */
1643 OO.ui.Element.prototype.updateThemeClasses = function () {
1644 if ( !this.updateThemeClassesPending ) {
1645 this.updateThemeClassesPending = true;
1646 setTimeout( this.debouncedUpdateThemeClassesHandler );
1647 }
1648 };
1649
1650 /**
1651 * @private
1652 */
1653 OO.ui.Element.prototype.debouncedUpdateThemeClasses = function () {
1654 OO.ui.theme.updateElementClasses( this );
1655 this.updateThemeClassesPending = false;
1656 };
1657
1658 /**
1659 * Get the HTML tag name.
1660 *
1661 * Override this method to base the result on instance information.
1662 *
1663 * @return {string} HTML tag name
1664 */
1665 OO.ui.Element.prototype.getTagName = function () {
1666 return this.constructor.static.tagName;
1667 };
1668
1669 /**
1670 * Check if the element is attached to the DOM
1671 * @return {boolean} The element is attached to the DOM
1672 */
1673 OO.ui.Element.prototype.isElementAttached = function () {
1674 return $.contains( this.getElementDocument(), this.$element[ 0 ] );
1675 };
1676
1677 /**
1678 * Get the DOM document.
1679 *
1680 * @return {HTMLDocument} Document object
1681 */
1682 OO.ui.Element.prototype.getElementDocument = function () {
1683 // Don't cache this in other ways either because subclasses could can change this.$element
1684 return OO.ui.Element.static.getDocument( this.$element );
1685 };
1686
1687 /**
1688 * Get the DOM window.
1689 *
1690 * @return {Window} Window object
1691 */
1692 OO.ui.Element.prototype.getElementWindow = function () {
1693 return OO.ui.Element.static.getWindow( this.$element );
1694 };
1695
1696 /**
1697 * Get closest scrollable container.
1698 */
1699 OO.ui.Element.prototype.getClosestScrollableElementContainer = function () {
1700 return OO.ui.Element.static.getClosestScrollableContainer( this.$element[ 0 ] );
1701 };
1702
1703 /**
1704 * Get group element is in.
1705 *
1706 * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1707 */
1708 OO.ui.Element.prototype.getElementGroup = function () {
1709 return this.elementGroup;
1710 };
1711
1712 /**
1713 * Set group element is in.
1714 *
1715 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1716 * @chainable
1717 */
1718 OO.ui.Element.prototype.setElementGroup = function ( group ) {
1719 this.elementGroup = group;
1720 return this;
1721 };
1722
1723 /**
1724 * Scroll element into view.
1725 *
1726 * @param {Object} [config] Configuration options
1727 */
1728 OO.ui.Element.prototype.scrollElementIntoView = function ( config ) {
1729 return OO.ui.Element.static.scrollIntoView( this.$element[ 0 ], config );
1730 };
1731
1732 /**
1733 * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way
1734 * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined.
1735 * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout},
1736 * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1737 * and {@link OO.ui.BookletLayout BookletLayout} for more information and examples.
1738 *
1739 * @abstract
1740 * @class
1741 * @extends OO.ui.Element
1742 * @mixins OO.EventEmitter
1743 *
1744 * @constructor
1745 * @param {Object} [config] Configuration options
1746 */
1747 OO.ui.Layout = function OoUiLayout( config ) {
1748 // Configuration initialization
1749 config = config || {};
1750
1751 // Parent constructor
1752 OO.ui.Layout.super.call( this, config );
1753
1754 // Mixin constructors
1755 OO.EventEmitter.call( this );
1756
1757 // Initialization
1758 this.$element.addClass( 'oo-ui-layout' );
1759 };
1760
1761 /* Setup */
1762
1763 OO.inheritClass( OO.ui.Layout, OO.ui.Element );
1764 OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
1765
1766 /**
1767 * Widgets are compositions of one or more OOjs UI elements that users can both view
1768 * and interact with. All widgets can be configured and modified via a standard API,
1769 * and their state can change dynamically according to a model.
1770 *
1771 * @abstract
1772 * @class
1773 * @extends OO.ui.Element
1774 * @mixins OO.EventEmitter
1775 *
1776 * @constructor
1777 * @param {Object} [config] Configuration options
1778 * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1779 * appearance reflects this state.
1780 */
1781 OO.ui.Widget = function OoUiWidget( config ) {
1782 // Initialize config
1783 config = $.extend( { disabled: false }, config );
1784
1785 // Parent constructor
1786 OO.ui.Widget.super.call( this, config );
1787
1788 // Mixin constructors
1789 OO.EventEmitter.call( this );
1790
1791 // Properties
1792 this.disabled = null;
1793 this.wasDisabled = null;
1794
1795 // Initialization
1796 this.$element.addClass( 'oo-ui-widget' );
1797 this.setDisabled( !!config.disabled );
1798 };
1799
1800 /* Setup */
1801
1802 OO.inheritClass( OO.ui.Widget, OO.ui.Element );
1803 OO.mixinClass( OO.ui.Widget, OO.EventEmitter );
1804
1805 /* Static Properties */
1806
1807 /**
1808 * Whether this widget will behave reasonably when wrapped in a HTML `<label>`. If this is true,
1809 * wrappers such as OO.ui.FieldLayout may use a `<label>` instead of implementing own label click
1810 * handling.
1811 *
1812 * @static
1813 * @inheritable
1814 * @property {boolean}
1815 */
1816 OO.ui.Widget.static.supportsSimpleLabel = false;
1817
1818 /* Events */
1819
1820 /**
1821 * @event disable
1822 *
1823 * A 'disable' event is emitted when a widget is disabled.
1824 *
1825 * @param {boolean} disabled Widget is disabled
1826 */
1827
1828 /**
1829 * @event toggle
1830 *
1831 * A 'toggle' event is emitted when the visibility of the widget changes.
1832 *
1833 * @param {boolean} visible Widget is visible
1834 */
1835
1836 /* Methods */
1837
1838 /**
1839 * Check if the widget is disabled.
1840 *
1841 * @return {boolean} Widget is disabled
1842 */
1843 OO.ui.Widget.prototype.isDisabled = function () {
1844 return this.disabled;
1845 };
1846
1847 /**
1848 * Set the 'disabled' state of the widget.
1849 *
1850 * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1851 *
1852 * @param {boolean} disabled Disable widget
1853 * @chainable
1854 */
1855 OO.ui.Widget.prototype.setDisabled = function ( disabled ) {
1856 var isDisabled;
1857
1858 this.disabled = !!disabled;
1859 isDisabled = this.isDisabled();
1860 if ( isDisabled !== this.wasDisabled ) {
1861 this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled );
1862 this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled );
1863 this.$element.attr( 'aria-disabled', isDisabled.toString() );
1864 this.emit( 'disable', isDisabled );
1865 this.updateThemeClasses();
1866 }
1867 this.wasDisabled = isDisabled;
1868
1869 return this;
1870 };
1871
1872 /**
1873 * Update the disabled state, in case of changes in parent widget.
1874 *
1875 * @chainable
1876 */
1877 OO.ui.Widget.prototype.updateDisabled = function () {
1878 this.setDisabled( this.disabled );
1879 return this;
1880 };
1881
1882 /**
1883 * A window is a container for elements that are in a child frame. They are used with
1884 * a window manager (OO.ui.WindowManager), which is used to open and close the window and control
1885 * its presentation. The size of a window is specified using a symbolic name (e.g., ‘small’, ‘medium’,
1886 * ‘large’), which is interpreted by the window manager. If the requested size is not recognized,
1887 * the window manager will choose a sensible fallback.
1888 *
1889 * The lifecycle of a window has three primary stages (opening, opened, and closing) in which
1890 * different processes are executed:
1891 *
1892 * **opening**: The opening stage begins when the window manager's {@link OO.ui.WindowManager#openWindow
1893 * openWindow} or the window's {@link #open open} methods are used, and the window manager begins to open
1894 * the window.
1895 *
1896 * - {@link #getSetupProcess} method is called and its result executed
1897 * - {@link #getReadyProcess} method is called and its result executed
1898 *
1899 * **opened**: The window is now open
1900 *
1901 * **closing**: The closing stage begins when the window manager's
1902 * {@link OO.ui.WindowManager#closeWindow closeWindow}
1903 * or the window's {@link #close} methods are used, and the window manager begins to close the window.
1904 *
1905 * - {@link #getHoldProcess} method is called and its result executed
1906 * - {@link #getTeardownProcess} method is called and its result executed. The window is now closed
1907 *
1908 * Each of the window's processes (setup, ready, hold, and teardown) can be extended in subclasses
1909 * by overriding the window's #getSetupProcess, #getReadyProcess, #getHoldProcess and #getTeardownProcess
1910 * methods. Note that each {@link OO.ui.Process process} is executed in series, so asynchronous
1911 * processing can complete. Always assume window processes are executed asynchronously.
1912 *
1913 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
1914 *
1915 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows
1916 *
1917 * @abstract
1918 * @class
1919 * @extends OO.ui.Element
1920 * @mixins OO.EventEmitter
1921 *
1922 * @constructor
1923 * @param {Object} [config] Configuration options
1924 * @cfg {string} [size] Symbolic name of the dialog size: `small`, `medium`, `large`, `larger` or
1925 * `full`. If omitted, the value of the {@link #static-size static size} property will be used.
1926 */
1927 OO.ui.Window = function OoUiWindow( config ) {
1928 // Configuration initialization
1929 config = config || {};
1930
1931 // Parent constructor
1932 OO.ui.Window.super.call( this, config );
1933
1934 // Mixin constructors
1935 OO.EventEmitter.call( this );
1936
1937 // Properties
1938 this.manager = null;
1939 this.size = config.size || this.constructor.static.size;
1940 this.$frame = $( '<div>' );
1941 this.$overlay = $( '<div>' );
1942 this.$content = $( '<div>' );
1943
1944 // Initialization
1945 this.$overlay.addClass( 'oo-ui-window-overlay' );
1946 this.$content
1947 .addClass( 'oo-ui-window-content' )
1948 .attr( 'tabindex', 0 );
1949 this.$frame
1950 .addClass( 'oo-ui-window-frame' )
1951 .append( this.$content );
1952
1953 this.$element
1954 .addClass( 'oo-ui-window' )
1955 .append( this.$frame, this.$overlay );
1956
1957 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
1958 // that reference properties not initialized at that time of parent class construction
1959 // TODO: Find a better way to handle post-constructor setup
1960 this.visible = false;
1961 this.$element.addClass( 'oo-ui-element-hidden' );
1962 };
1963
1964 /* Setup */
1965
1966 OO.inheritClass( OO.ui.Window, OO.ui.Element );
1967 OO.mixinClass( OO.ui.Window, OO.EventEmitter );
1968
1969 /* Static Properties */
1970
1971 /**
1972 * Symbolic name of the window size: `small`, `medium`, `large`, `larger` or `full`.
1973 *
1974 * The static size is used if no #size is configured during construction.
1975 *
1976 * @static
1977 * @inheritable
1978 * @property {string}
1979 */
1980 OO.ui.Window.static.size = 'medium';
1981
1982 /* Methods */
1983
1984 /**
1985 * Handle mouse down events.
1986 *
1987 * @private
1988 * @param {jQuery.Event} e Mouse down event
1989 */
1990 OO.ui.Window.prototype.onMouseDown = function ( e ) {
1991 // Prevent clicking on the click-block from stealing focus
1992 if ( e.target === this.$element[ 0 ] ) {
1993 return false;
1994 }
1995 };
1996
1997 /**
1998 * Check if the window has been initialized.
1999 *
2000 * Initialization occurs when a window is added to a manager.
2001 *
2002 * @return {boolean} Window has been initialized
2003 */
2004 OO.ui.Window.prototype.isInitialized = function () {
2005 return !!this.manager;
2006 };
2007
2008 /**
2009 * Check if the window is visible.
2010 *
2011 * @return {boolean} Window is visible
2012 */
2013 OO.ui.Window.prototype.isVisible = function () {
2014 return this.visible;
2015 };
2016
2017 /**
2018 * Check if the window is opening.
2019 *
2020 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpening isOpening}
2021 * method.
2022 *
2023 * @return {boolean} Window is opening
2024 */
2025 OO.ui.Window.prototype.isOpening = function () {
2026 return this.manager.isOpening( this );
2027 };
2028
2029 /**
2030 * Check if the window is closing.
2031 *
2032 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isClosing isClosing} method.
2033 *
2034 * @return {boolean} Window is closing
2035 */
2036 OO.ui.Window.prototype.isClosing = function () {
2037 return this.manager.isClosing( this );
2038 };
2039
2040 /**
2041 * Check if the window is opened.
2042 *
2043 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpened isOpened} method.
2044 *
2045 * @return {boolean} Window is opened
2046 */
2047 OO.ui.Window.prototype.isOpened = function () {
2048 return this.manager.isOpened( this );
2049 };
2050
2051 /**
2052 * Get the window manager.
2053 *
2054 * All windows must be attached to a window manager, which is used to open
2055 * and close the window and control its presentation.
2056 *
2057 * @return {OO.ui.WindowManager} Manager of window
2058 */
2059 OO.ui.Window.prototype.getManager = function () {
2060 return this.manager;
2061 };
2062
2063 /**
2064 * Get the symbolic name of the window size (e.g., `small` or `medium`).
2065 *
2066 * @return {string} Symbolic name of the size: `small`, `medium`, `large`, `larger`, `full`
2067 */
2068 OO.ui.Window.prototype.getSize = function () {
2069 return this.size;
2070 };
2071
2072 /**
2073 * Disable transitions on window's frame for the duration of the callback function, then enable them
2074 * back.
2075 *
2076 * @private
2077 * @param {Function} callback Function to call while transitions are disabled
2078 */
2079 OO.ui.Window.prototype.withoutSizeTransitions = function ( callback ) {
2080 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
2081 // Disable transitions first, otherwise we'll get values from when the window was animating.
2082 var oldTransition,
2083 styleObj = this.$frame[ 0 ].style;
2084 oldTransition = styleObj.transition || styleObj.OTransition || styleObj.MsTransition ||
2085 styleObj.MozTransition || styleObj.WebkitTransition;
2086 styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
2087 styleObj.MozTransition = styleObj.WebkitTransition = 'none';
2088 callback();
2089 // Force reflow to make sure the style changes done inside callback really are not transitioned
2090 this.$frame.height();
2091 styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
2092 styleObj.MozTransition = styleObj.WebkitTransition = oldTransition;
2093 };
2094
2095 /**
2096 * Get the height of the full window contents (i.e., the window head, body and foot together).
2097 *
2098 * What consistitutes the head, body, and foot varies depending on the window type.
2099 * A {@link OO.ui.MessageDialog message dialog} displays a title and message in its body,
2100 * and any actions in the foot. A {@link OO.ui.ProcessDialog process dialog} displays a title
2101 * and special actions in the head, and dialog content in the body.
2102 *
2103 * To get just the height of the dialog body, use the #getBodyHeight method.
2104 *
2105 * @return {number} The height of the window contents (the dialog head, body and foot) in pixels
2106 */
2107 OO.ui.Window.prototype.getContentHeight = function () {
2108 var bodyHeight,
2109 win = this,
2110 bodyStyleObj = this.$body[ 0 ].style,
2111 frameStyleObj = this.$frame[ 0 ].style;
2112
2113 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
2114 // Disable transitions first, otherwise we'll get values from when the window was animating.
2115 this.withoutSizeTransitions( function () {
2116 var oldHeight = frameStyleObj.height,
2117 oldPosition = bodyStyleObj.position;
2118 frameStyleObj.height = '1px';
2119 // Force body to resize to new width
2120 bodyStyleObj.position = 'relative';
2121 bodyHeight = win.getBodyHeight();
2122 frameStyleObj.height = oldHeight;
2123 bodyStyleObj.position = oldPosition;
2124 } );
2125
2126 return (
2127 // Add buffer for border
2128 ( this.$frame.outerHeight() - this.$frame.innerHeight() ) +
2129 // Use combined heights of children
2130 ( this.$head.outerHeight( true ) + bodyHeight + this.$foot.outerHeight( true ) )
2131 );
2132 };
2133
2134 /**
2135 * Get the height of the window body.
2136 *
2137 * To get the height of the full window contents (the window body, head, and foot together),
2138 * use #getContentHeight.
2139 *
2140 * When this function is called, the window will temporarily have been resized
2141 * to height=1px, so .scrollHeight measurements can be taken accurately.
2142 *
2143 * @return {number} Height of the window body in pixels
2144 */
2145 OO.ui.Window.prototype.getBodyHeight = function () {
2146 return this.$body[ 0 ].scrollHeight;
2147 };
2148
2149 /**
2150 * Get the directionality of the frame (right-to-left or left-to-right).
2151 *
2152 * @return {string} Directionality: `'ltr'` or `'rtl'`
2153 */
2154 OO.ui.Window.prototype.getDir = function () {
2155 return this.dir;
2156 };
2157
2158 /**
2159 * Get the 'setup' process.
2160 *
2161 * The setup process is used to set up a window for use in a particular context,
2162 * based on the `data` argument. This method is called during the opening phase of the window’s
2163 * lifecycle.
2164 *
2165 * Override this method to add additional steps to the ‘setup’ process the parent method provides
2166 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2167 * of OO.ui.Process.
2168 *
2169 * To add window content that persists between openings, you may wish to use the #initialize method
2170 * instead.
2171 *
2172 * @abstract
2173 * @param {Object} [data] Window opening data
2174 * @return {OO.ui.Process} Setup process
2175 */
2176 OO.ui.Window.prototype.getSetupProcess = function () {
2177 return new OO.ui.Process();
2178 };
2179
2180 /**
2181 * Get the ‘ready’ process.
2182 *
2183 * The ready process is used to ready a window for use in a particular
2184 * context, based on the `data` argument. This method is called during the opening phase of
2185 * the window’s lifecycle, after the window has been {@link #getSetupProcess setup}.
2186 *
2187 * Override this method to add additional steps to the ‘ready’ process the parent method
2188 * provides using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next}
2189 * methods of OO.ui.Process.
2190 *
2191 * @abstract
2192 * @param {Object} [data] Window opening data
2193 * @return {OO.ui.Process} Ready process
2194 */
2195 OO.ui.Window.prototype.getReadyProcess = function () {
2196 return new OO.ui.Process();
2197 };
2198
2199 /**
2200 * Get the 'hold' process.
2201 *
2202 * The hold proccess is used to keep a window from being used in a particular context,
2203 * based on the `data` argument. This method is called during the closing phase of the window’s
2204 * lifecycle.
2205 *
2206 * Override this method to add additional steps to the 'hold' process the parent method provides
2207 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2208 * of OO.ui.Process.
2209 *
2210 * @abstract
2211 * @param {Object} [data] Window closing data
2212 * @return {OO.ui.Process} Hold process
2213 */
2214 OO.ui.Window.prototype.getHoldProcess = function () {
2215 return new OO.ui.Process();
2216 };
2217
2218 /**
2219 * Get the ‘teardown’ process.
2220 *
2221 * The teardown process is used to teardown a window after use. During teardown,
2222 * user interactions within the window are conveyed and the window is closed, based on the `data`
2223 * argument. This method is called during the closing phase of the window’s lifecycle.
2224 *
2225 * Override this method to add additional steps to the ‘teardown’ process the parent method provides
2226 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2227 * of OO.ui.Process.
2228 *
2229 * @abstract
2230 * @param {Object} [data] Window closing data
2231 * @return {OO.ui.Process} Teardown process
2232 */
2233 OO.ui.Window.prototype.getTeardownProcess = function () {
2234 return new OO.ui.Process();
2235 };
2236
2237 /**
2238 * Set the window manager.
2239 *
2240 * This will cause the window to initialize. Calling it more than once will cause an error.
2241 *
2242 * @param {OO.ui.WindowManager} manager Manager for this window
2243 * @throws {Error} An error is thrown if the method is called more than once
2244 * @chainable
2245 */
2246 OO.ui.Window.prototype.setManager = function ( manager ) {
2247 if ( this.manager ) {
2248 throw new Error( 'Cannot set window manager, window already has a manager' );
2249 }
2250
2251 this.manager = manager;
2252 this.initialize();
2253
2254 return this;
2255 };
2256
2257 /**
2258 * Set the window size by symbolic name (e.g., 'small' or 'medium')
2259 *
2260 * @param {string} size Symbolic name of size: `small`, `medium`, `large`, `larger` or
2261 * `full`
2262 * @chainable
2263 */
2264 OO.ui.Window.prototype.setSize = function ( size ) {
2265 this.size = size;
2266 this.updateSize();
2267 return this;
2268 };
2269
2270 /**
2271 * Update the window size.
2272 *
2273 * @throws {Error} An error is thrown if the window is not attached to a window manager
2274 * @chainable
2275 */
2276 OO.ui.Window.prototype.updateSize = function () {
2277 if ( !this.manager ) {
2278 throw new Error( 'Cannot update window size, must be attached to a manager' );
2279 }
2280
2281 this.manager.updateWindowSize( this );
2282
2283 return this;
2284 };
2285
2286 /**
2287 * Set window dimensions. This method is called by the {@link OO.ui.WindowManager window manager}
2288 * when the window is opening. In general, setDimensions should not be called directly.
2289 *
2290 * To set the size of the window, use the #setSize method.
2291 *
2292 * @param {Object} dim CSS dimension properties
2293 * @param {string|number} [dim.width] Width
2294 * @param {string|number} [dim.minWidth] Minimum width
2295 * @param {string|number} [dim.maxWidth] Maximum width
2296 * @param {string|number} [dim.width] Height, omit to set based on height of contents
2297 * @param {string|number} [dim.minWidth] Minimum height
2298 * @param {string|number} [dim.maxWidth] Maximum height
2299 * @chainable
2300 */
2301 OO.ui.Window.prototype.setDimensions = function ( dim ) {
2302 var height,
2303 win = this,
2304 styleObj = this.$frame[ 0 ].style;
2305
2306 // Calculate the height we need to set using the correct width
2307 if ( dim.height === undefined ) {
2308 this.withoutSizeTransitions( function () {
2309 var oldWidth = styleObj.width;
2310 win.$frame.css( 'width', dim.width || '' );
2311 height = win.getContentHeight();
2312 styleObj.width = oldWidth;
2313 } );
2314 } else {
2315 height = dim.height;
2316 }
2317
2318 this.$frame.css( {
2319 width: dim.width || '',
2320 minWidth: dim.minWidth || '',
2321 maxWidth: dim.maxWidth || '',
2322 height: height || '',
2323 minHeight: dim.minHeight || '',
2324 maxHeight: dim.maxHeight || ''
2325 } );
2326
2327 return this;
2328 };
2329
2330 /**
2331 * Initialize window contents.
2332 *
2333 * Before the window is opened for the first time, #initialize is called so that content that
2334 * persists between openings can be added to the window.
2335 *
2336 * To set up a window with new content each time the window opens, use #getSetupProcess.
2337 *
2338 * @throws {Error} An error is thrown if the window is not attached to a window manager
2339 * @chainable
2340 */
2341 OO.ui.Window.prototype.initialize = function () {
2342 if ( !this.manager ) {
2343 throw new Error( 'Cannot initialize window, must be attached to a manager' );
2344 }
2345
2346 // Properties
2347 this.$head = $( '<div>' );
2348 this.$body = $( '<div>' );
2349 this.$foot = $( '<div>' );
2350 this.dir = OO.ui.Element.static.getDir( this.$content ) || 'ltr';
2351 this.$document = $( this.getElementDocument() );
2352
2353 // Events
2354 this.$element.on( 'mousedown', this.onMouseDown.bind( this ) );
2355
2356 // Initialization
2357 this.$head.addClass( 'oo-ui-window-head' );
2358 this.$body.addClass( 'oo-ui-window-body' );
2359 this.$foot.addClass( 'oo-ui-window-foot' );
2360 this.$content.append( this.$head, this.$body, this.$foot );
2361
2362 return this;
2363 };
2364
2365 /**
2366 * Open the window.
2367 *
2368 * This method is a wrapper around a call to the window manager’s {@link OO.ui.WindowManager#openWindow openWindow}
2369 * method, which returns a promise resolved when the window is done opening.
2370 *
2371 * To customize the window each time it opens, use #getSetupProcess or #getReadyProcess.
2372 *
2373 * @param {Object} [data] Window opening data
2374 * @return {jQuery.Promise} Promise resolved with a value when the window is opened, or rejected
2375 * if the window fails to open. When the promise is resolved successfully, the first argument of the
2376 * value is a new promise, which is resolved when the window begins closing.
2377 * @throws {Error} An error is thrown if the window is not attached to a window manager
2378 */
2379 OO.ui.Window.prototype.open = function ( data ) {
2380 if ( !this.manager ) {
2381 throw new Error( 'Cannot open window, must be attached to a manager' );
2382 }
2383
2384 return this.manager.openWindow( this, data );
2385 };
2386
2387 /**
2388 * Close the window.
2389 *
2390 * This method is a wrapper around a call to the window
2391 * manager’s {@link OO.ui.WindowManager#closeWindow closeWindow} method,
2392 * which returns a closing promise resolved when the window is done closing.
2393 *
2394 * The window's #getHoldProcess and #getTeardownProcess methods are called during the closing
2395 * phase of the window’s lifecycle and can be used to specify closing behavior each time
2396 * the window closes.
2397 *
2398 * @param {Object} [data] Window closing data
2399 * @return {jQuery.Promise} Promise resolved when window is closed
2400 * @throws {Error} An error is thrown if the window is not attached to a window manager
2401 */
2402 OO.ui.Window.prototype.close = function ( data ) {
2403 if ( !this.manager ) {
2404 throw new Error( 'Cannot close window, must be attached to a manager' );
2405 }
2406
2407 return this.manager.closeWindow( this, data );
2408 };
2409
2410 /**
2411 * Setup window.
2412 *
2413 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2414 * by other systems.
2415 *
2416 * @param {Object} [data] Window opening data
2417 * @return {jQuery.Promise} Promise resolved when window is setup
2418 */
2419 OO.ui.Window.prototype.setup = function ( data ) {
2420 var win = this,
2421 deferred = $.Deferred();
2422
2423 this.toggle( true );
2424
2425 this.getSetupProcess( data ).execute().done( function () {
2426 // Force redraw by asking the browser to measure the elements' widths
2427 win.$element.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2428 win.$content.addClass( 'oo-ui-window-content-setup' ).width();
2429 deferred.resolve();
2430 } );
2431
2432 return deferred.promise();
2433 };
2434
2435 /**
2436 * Ready window.
2437 *
2438 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2439 * by other systems.
2440 *
2441 * @param {Object} [data] Window opening data
2442 * @return {jQuery.Promise} Promise resolved when window is ready
2443 */
2444 OO.ui.Window.prototype.ready = function ( data ) {
2445 var win = this,
2446 deferred = $.Deferred();
2447
2448 this.$content.focus();
2449 this.getReadyProcess( data ).execute().done( function () {
2450 // Force redraw by asking the browser to measure the elements' widths
2451 win.$element.addClass( 'oo-ui-window-ready' ).width();
2452 win.$content.addClass( 'oo-ui-window-content-ready' ).width();
2453 deferred.resolve();
2454 } );
2455
2456 return deferred.promise();
2457 };
2458
2459 /**
2460 * Hold window.
2461 *
2462 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2463 * by other systems.
2464 *
2465 * @param {Object} [data] Window closing data
2466 * @return {jQuery.Promise} Promise resolved when window is held
2467 */
2468 OO.ui.Window.prototype.hold = function ( data ) {
2469 var win = this,
2470 deferred = $.Deferred();
2471
2472 this.getHoldProcess( data ).execute().done( function () {
2473 // Get the focused element within the window's content
2474 var $focus = win.$content.find( OO.ui.Element.static.getDocument( win.$content ).activeElement );
2475
2476 // Blur the focused element
2477 if ( $focus.length ) {
2478 $focus[ 0 ].blur();
2479 }
2480
2481 // Force redraw by asking the browser to measure the elements' widths
2482 win.$element.removeClass( 'oo-ui-window-ready' ).width();
2483 win.$content.removeClass( 'oo-ui-window-content-ready' ).width();
2484 deferred.resolve();
2485 } );
2486
2487 return deferred.promise();
2488 };
2489
2490 /**
2491 * Teardown window.
2492 *
2493 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2494 * by other systems.
2495 *
2496 * @param {Object} [data] Window closing data
2497 * @return {jQuery.Promise} Promise resolved when window is torn down
2498 */
2499 OO.ui.Window.prototype.teardown = function ( data ) {
2500 var win = this;
2501
2502 return this.getTeardownProcess( data ).execute()
2503 .done( function () {
2504 // Force redraw by asking the browser to measure the elements' widths
2505 win.$element.removeClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2506 win.$content.removeClass( 'oo-ui-window-content-setup' ).width();
2507 win.toggle( false );
2508 } );
2509 };
2510
2511 /**
2512 * The Dialog class serves as the base class for the other types of dialogs.
2513 * Unless extended to include controls, the rendered dialog box is a simple window
2514 * that users can close by hitting the ‘Esc’ key. Dialog windows are used with OO.ui.WindowManager,
2515 * which opens, closes, and controls the presentation of the window. See the
2516 * [OOjs UI documentation on MediaWiki] [1] for more information.
2517 *
2518 * @example
2519 * // A simple dialog window.
2520 * function MyDialog( config ) {
2521 * MyDialog.super.call( this, config );
2522 * }
2523 * OO.inheritClass( MyDialog, OO.ui.Dialog );
2524 * MyDialog.prototype.initialize = function () {
2525 * MyDialog.super.prototype.initialize.call( this );
2526 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
2527 * this.content.$element.append( '<p>A simple dialog window. Press \'Esc\' to close.</p>' );
2528 * this.$body.append( this.content.$element );
2529 * };
2530 * MyDialog.prototype.getBodyHeight = function () {
2531 * return this.content.$element.outerHeight( true );
2532 * };
2533 * var myDialog = new MyDialog( {
2534 * size: 'medium'
2535 * } );
2536 * // Create and append a window manager, which opens and closes the window.
2537 * var windowManager = new OO.ui.WindowManager();
2538 * $( 'body' ).append( windowManager.$element );
2539 * windowManager.addWindows( [ myDialog ] );
2540 * // Open the window!
2541 * windowManager.openWindow( myDialog );
2542 *
2543 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Dialogs
2544 *
2545 * @abstract
2546 * @class
2547 * @extends OO.ui.Window
2548 * @mixins OO.ui.mixin.PendingElement
2549 *
2550 * @constructor
2551 * @param {Object} [config] Configuration options
2552 */
2553 OO.ui.Dialog = function OoUiDialog( config ) {
2554 // Parent constructor
2555 OO.ui.Dialog.super.call( this, config );
2556
2557 // Mixin constructors
2558 OO.ui.mixin.PendingElement.call( this );
2559
2560 // Properties
2561 this.actions = new OO.ui.ActionSet();
2562 this.attachedActions = [];
2563 this.currentAction = null;
2564 this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
2565
2566 // Events
2567 this.actions.connect( this, {
2568 click: 'onActionClick',
2569 resize: 'onActionResize',
2570 change: 'onActionsChange'
2571 } );
2572
2573 // Initialization
2574 this.$element
2575 .addClass( 'oo-ui-dialog' )
2576 .attr( 'role', 'dialog' );
2577 };
2578
2579 /* Setup */
2580
2581 OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
2582 OO.mixinClass( OO.ui.Dialog, OO.ui.mixin.PendingElement );
2583
2584 /* Static Properties */
2585
2586 /**
2587 * Symbolic name of dialog.
2588 *
2589 * The dialog class must have a symbolic name in order to be registered with OO.Factory.
2590 * Please see the [OOjs UI documentation on MediaWiki] [3] for more information.
2591 *
2592 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
2593 *
2594 * @abstract
2595 * @static
2596 * @inheritable
2597 * @property {string}
2598 */
2599 OO.ui.Dialog.static.name = '';
2600
2601 /**
2602 * The dialog title.
2603 *
2604 * The title can be specified as a plaintext string, a {@link OO.ui.mixin.LabelElement Label} node, or a function
2605 * that will produce a Label node or string. The title can also be specified with data passed to the
2606 * constructor (see #getSetupProcess). In this case, the static value will be overriden.
2607 *
2608 * @abstract
2609 * @static
2610 * @inheritable
2611 * @property {jQuery|string|Function}
2612 */
2613 OO.ui.Dialog.static.title = '';
2614
2615 /**
2616 * An array of configured {@link OO.ui.ActionWidget action widgets}.
2617 *
2618 * Actions can also be specified with data passed to the constructor (see #getSetupProcess). In this case, the static
2619 * value will be overriden.
2620 *
2621 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
2622 *
2623 * @static
2624 * @inheritable
2625 * @property {Object[]}
2626 */
2627 OO.ui.Dialog.static.actions = [];
2628
2629 /**
2630 * Close the dialog when the 'Esc' key is pressed.
2631 *
2632 * @static
2633 * @abstract
2634 * @inheritable
2635 * @property {boolean}
2636 */
2637 OO.ui.Dialog.static.escapable = true;
2638
2639 /* Methods */
2640
2641 /**
2642 * Handle frame document key down events.
2643 *
2644 * @private
2645 * @param {jQuery.Event} e Key down event
2646 */
2647 OO.ui.Dialog.prototype.onDocumentKeyDown = function ( e ) {
2648 if ( e.which === OO.ui.Keys.ESCAPE ) {
2649 this.close();
2650 e.preventDefault();
2651 e.stopPropagation();
2652 }
2653 };
2654
2655 /**
2656 * Handle action resized events.
2657 *
2658 * @private
2659 * @param {OO.ui.ActionWidget} action Action that was resized
2660 */
2661 OO.ui.Dialog.prototype.onActionResize = function () {
2662 // Override in subclass
2663 };
2664
2665 /**
2666 * Handle action click events.
2667 *
2668 * @private
2669 * @param {OO.ui.ActionWidget} action Action that was clicked
2670 */
2671 OO.ui.Dialog.prototype.onActionClick = function ( action ) {
2672 if ( !this.isPending() ) {
2673 this.executeAction( action.getAction() );
2674 }
2675 };
2676
2677 /**
2678 * Handle actions change event.
2679 *
2680 * @private
2681 */
2682 OO.ui.Dialog.prototype.onActionsChange = function () {
2683 this.detachActions();
2684 if ( !this.isClosing() ) {
2685 this.attachActions();
2686 }
2687 };
2688
2689 /**
2690 * Get the set of actions used by the dialog.
2691 *
2692 * @return {OO.ui.ActionSet}
2693 */
2694 OO.ui.Dialog.prototype.getActions = function () {
2695 return this.actions;
2696 };
2697
2698 /**
2699 * Get a process for taking action.
2700 *
2701 * When you override this method, you can create a new OO.ui.Process and return it, or add additional
2702 * accept steps to the process the parent method provides using the {@link OO.ui.Process#first 'first'}
2703 * and {@link OO.ui.Process#next 'next'} methods of OO.ui.Process.
2704 *
2705 * @abstract
2706 * @param {string} [action] Symbolic name of action
2707 * @return {OO.ui.Process} Action process
2708 */
2709 OO.ui.Dialog.prototype.getActionProcess = function ( action ) {
2710 return new OO.ui.Process()
2711 .next( function () {
2712 if ( !action ) {
2713 // An empty action always closes the dialog without data, which should always be
2714 // safe and make no changes
2715 this.close();
2716 }
2717 }, this );
2718 };
2719
2720 /**
2721 * @inheritdoc
2722 *
2723 * @param {Object} [data] Dialog opening data
2724 * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use
2725 * the {@link #static-title static title}
2726 * @param {Object[]} [data.actions] List of configuration options for each
2727 * {@link OO.ui.ActionWidget action widget}, omit to use {@link #static-actions static actions}.
2728 */
2729 OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
2730 data = data || {};
2731
2732 // Parent method
2733 return OO.ui.Dialog.super.prototype.getSetupProcess.call( this, data )
2734 .next( function () {
2735 var config = this.constructor.static,
2736 actions = data.actions !== undefined ? data.actions : config.actions;
2737
2738 this.title.setLabel(
2739 data.title !== undefined ? data.title : this.constructor.static.title
2740 );
2741 this.actions.add( this.getActionWidgets( actions ) );
2742
2743 if ( this.constructor.static.escapable ) {
2744 this.$document.on( 'keydown', this.onDocumentKeyDownHandler );
2745 }
2746 }, this );
2747 };
2748
2749 /**
2750 * @inheritdoc
2751 */
2752 OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
2753 // Parent method
2754 return OO.ui.Dialog.super.prototype.getTeardownProcess.call( this, data )
2755 .first( function () {
2756 if ( this.constructor.static.escapable ) {
2757 this.$document.off( 'keydown', this.onDocumentKeyDownHandler );
2758 }
2759
2760 this.actions.clear();
2761 this.currentAction = null;
2762 }, this );
2763 };
2764
2765 /**
2766 * @inheritdoc
2767 */
2768 OO.ui.Dialog.prototype.initialize = function () {
2769 // Parent method
2770 OO.ui.Dialog.super.prototype.initialize.call( this );
2771
2772 var titleId = OO.ui.generateElementId();
2773
2774 // Properties
2775 this.title = new OO.ui.LabelWidget( {
2776 id: titleId
2777 } );
2778
2779 // Initialization
2780 this.$content.addClass( 'oo-ui-dialog-content' );
2781 this.$element.attr( 'aria-labelledby', titleId );
2782 this.setPendingElement( this.$head );
2783 };
2784
2785 /**
2786 * Get action widgets from a list of configs
2787 *
2788 * @param {Object[]} actions Action widget configs
2789 * @return {OO.ui.ActionWidget[]} Action widgets
2790 */
2791 OO.ui.Dialog.prototype.getActionWidgets = function ( actions ) {
2792 var i, len, widgets = [];
2793 for ( i = 0, len = actions.length; i < len; i++ ) {
2794 widgets.push(
2795 new OO.ui.ActionWidget( actions[ i ] )
2796 );
2797 }
2798 return widgets;
2799 };
2800
2801 /**
2802 * Attach action actions.
2803 *
2804 * @protected
2805 */
2806 OO.ui.Dialog.prototype.attachActions = function () {
2807 // Remember the list of potentially attached actions
2808 this.attachedActions = this.actions.get();
2809 };
2810
2811 /**
2812 * Detach action actions.
2813 *
2814 * @protected
2815 * @chainable
2816 */
2817 OO.ui.Dialog.prototype.detachActions = function () {
2818 var i, len;
2819
2820 // Detach all actions that may have been previously attached
2821 for ( i = 0, len = this.attachedActions.length; i < len; i++ ) {
2822 this.attachedActions[ i ].$element.detach();
2823 }
2824 this.attachedActions = [];
2825 };
2826
2827 /**
2828 * Execute an action.
2829 *
2830 * @param {string} action Symbolic name of action to execute
2831 * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
2832 */
2833 OO.ui.Dialog.prototype.executeAction = function ( action ) {
2834 this.pushPending();
2835 this.currentAction = action;
2836 return this.getActionProcess( action ).execute()
2837 .always( this.popPending.bind( this ) );
2838 };
2839
2840 /**
2841 * Window managers are used to open and close {@link OO.ui.Window windows} and control their presentation.
2842 * Managed windows are mutually exclusive. If a new window is opened while a current window is opening
2843 * or is opened, the current window will be closed and any ongoing {@link OO.ui.Process process} will be cancelled. Windows
2844 * themselves are persistent and—rather than being torn down when closed—can be repopulated with the
2845 * pertinent data and reused.
2846 *
2847 * Over the lifecycle of a window, the window manager makes available three promises: `opening`,
2848 * `opened`, and `closing`, which represent the primary stages of the cycle:
2849 *
2850 * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s
2851 * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window.
2852 *
2853 * - an `opening` event is emitted with an `opening` promise
2854 * - the #getSetupDelay method is called and the returned value is used to time a pause in execution before
2855 * the window’s {@link OO.ui.Window#getSetupProcess getSetupProcess} method is called on the
2856 * window and its result executed
2857 * - a `setup` progress notification is emitted from the `opening` promise
2858 * - the #getReadyDelay method is called the returned value is used to time a pause in execution before
2859 * the window’s {@link OO.ui.Window#getReadyProcess getReadyProcess} method is called on the
2860 * window and its result executed
2861 * - a `ready` progress notification is emitted from the `opening` promise
2862 * - the `opening` promise is resolved with an `opened` promise
2863 *
2864 * **Opened**: the window is now open.
2865 *
2866 * **Closing**: the closing stage begins when the window manager's #closeWindow or the
2867 * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins
2868 * to close the window.
2869 *
2870 * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted
2871 * - the #getHoldDelay method is called and the returned value is used to time a pause in execution before
2872 * the window's {@link OO.ui.Window#getHoldProcess getHoldProces} method is called on the
2873 * window and its result executed
2874 * - a `hold` progress notification is emitted from the `closing` promise
2875 * - the #getTeardownDelay() method is called and the returned value is used to time a pause in execution before
2876 * the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method is called on the
2877 * window and its result executed
2878 * - a `teardown` progress notification is emitted from the `closing` promise
2879 * - the `closing` promise is resolved. The window is now closed
2880 *
2881 * See the [OOjs UI documentation on MediaWiki][1] for more information.
2882 *
2883 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
2884 *
2885 * @class
2886 * @extends OO.ui.Element
2887 * @mixins OO.EventEmitter
2888 *
2889 * @constructor
2890 * @param {Object} [config] Configuration options
2891 * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
2892 * Note that window classes that are instantiated with a factory must have
2893 * a {@link OO.ui.Dialog#static-name static name} property that specifies a symbolic name.
2894 * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
2895 */
2896 OO.ui.WindowManager = function OoUiWindowManager( config ) {
2897 // Configuration initialization
2898 config = config || {};
2899
2900 // Parent constructor
2901 OO.ui.WindowManager.super.call( this, config );
2902
2903 // Mixin constructors
2904 OO.EventEmitter.call( this );
2905
2906 // Properties
2907 this.factory = config.factory;
2908 this.modal = config.modal === undefined || !!config.modal;
2909 this.windows = {};
2910 this.opening = null;
2911 this.opened = null;
2912 this.closing = null;
2913 this.preparingToOpen = null;
2914 this.preparingToClose = null;
2915 this.currentWindow = null;
2916 this.globalEvents = false;
2917 this.$ariaHidden = null;
2918 this.onWindowResizeTimeout = null;
2919 this.onWindowResizeHandler = this.onWindowResize.bind( this );
2920 this.afterWindowResizeHandler = this.afterWindowResize.bind( this );
2921
2922 // Initialization
2923 this.$element
2924 .addClass( 'oo-ui-windowManager' )
2925 .toggleClass( 'oo-ui-windowManager-modal', this.modal );
2926 };
2927
2928 /* Setup */
2929
2930 OO.inheritClass( OO.ui.WindowManager, OO.ui.Element );
2931 OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter );
2932
2933 /* Events */
2934
2935 /**
2936 * An 'opening' event is emitted when the window begins to be opened.
2937 *
2938 * @event opening
2939 * @param {OO.ui.Window} win Window that's being opened
2940 * @param {jQuery.Promise} opening An `opening` promise resolved with a value when the window is opened successfully.
2941 * When the `opening` promise is resolved, the first argument of the value is an 'opened' promise, the second argument
2942 * is the opening data. The `opening` promise emits `setup` and `ready` notifications when those processes are complete.
2943 * @param {Object} data Window opening data
2944 */
2945
2946 /**
2947 * A 'closing' event is emitted when the window begins to be closed.
2948 *
2949 * @event closing
2950 * @param {OO.ui.Window} win Window that's being closed
2951 * @param {jQuery.Promise} closing A `closing` promise is resolved with a value when the window
2952 * is closed successfully. The promise emits `hold` and `teardown` notifications when those
2953 * processes are complete. When the `closing` promise is resolved, the first argument of its value
2954 * is the closing data.
2955 * @param {Object} data Window closing data
2956 */
2957
2958 /**
2959 * A 'resize' event is emitted when a window is resized.
2960 *
2961 * @event resize
2962 * @param {OO.ui.Window} win Window that was resized
2963 */
2964
2965 /* Static Properties */
2966
2967 /**
2968 * Map of the symbolic name of each window size and its CSS properties.
2969 *
2970 * @static
2971 * @inheritable
2972 * @property {Object}
2973 */
2974 OO.ui.WindowManager.static.sizes = {
2975 small: {
2976 width: 300
2977 },
2978 medium: {
2979 width: 500
2980 },
2981 large: {
2982 width: 700
2983 },
2984 larger: {
2985 width: 900
2986 },
2987 full: {
2988 // These can be non-numeric because they are never used in calculations
2989 width: '100%',
2990 height: '100%'
2991 }
2992 };
2993
2994 /**
2995 * Symbolic name of the default window size.
2996 *
2997 * The default size is used if the window's requested size is not recognized.
2998 *
2999 * @static
3000 * @inheritable
3001 * @property {string}
3002 */
3003 OO.ui.WindowManager.static.defaultSize = 'medium';
3004
3005 /* Methods */
3006
3007 /**
3008 * Handle window resize events.
3009 *
3010 * @private
3011 * @param {jQuery.Event} e Window resize event
3012 */
3013 OO.ui.WindowManager.prototype.onWindowResize = function () {
3014 clearTimeout( this.onWindowResizeTimeout );
3015 this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 );
3016 };
3017
3018 /**
3019 * Handle window resize events.
3020 *
3021 * @private
3022 * @param {jQuery.Event} e Window resize event
3023 */
3024 OO.ui.WindowManager.prototype.afterWindowResize = function () {
3025 if ( this.currentWindow ) {
3026 this.updateWindowSize( this.currentWindow );
3027 }
3028 };
3029
3030 /**
3031 * Check if window is opening.
3032 *
3033 * @return {boolean} Window is opening
3034 */
3035 OO.ui.WindowManager.prototype.isOpening = function ( win ) {
3036 return win === this.currentWindow && !!this.opening && this.opening.state() === 'pending';
3037 };
3038
3039 /**
3040 * Check if window is closing.
3041 *
3042 * @return {boolean} Window is closing
3043 */
3044 OO.ui.WindowManager.prototype.isClosing = function ( win ) {
3045 return win === this.currentWindow && !!this.closing && this.closing.state() === 'pending';
3046 };
3047
3048 /**
3049 * Check if window is opened.
3050 *
3051 * @return {boolean} Window is opened
3052 */
3053 OO.ui.WindowManager.prototype.isOpened = function ( win ) {
3054 return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending';
3055 };
3056
3057 /**
3058 * Check if a window is being managed.
3059 *
3060 * @param {OO.ui.Window} win Window to check
3061 * @return {boolean} Window is being managed
3062 */
3063 OO.ui.WindowManager.prototype.hasWindow = function ( win ) {
3064 var name;
3065
3066 for ( name in this.windows ) {
3067 if ( this.windows[ name ] === win ) {
3068 return true;
3069 }
3070 }
3071
3072 return false;
3073 };
3074
3075 /**
3076 * Get the number of milliseconds to wait after opening begins before executing the ‘setup’ process.
3077 *
3078 * @param {OO.ui.Window} win Window being opened
3079 * @param {Object} [data] Window opening data
3080 * @return {number} Milliseconds to wait
3081 */
3082 OO.ui.WindowManager.prototype.getSetupDelay = function () {
3083 return 0;
3084 };
3085
3086 /**
3087 * Get the number of milliseconds to wait after setup has finished before executing the ‘ready’ process.
3088 *
3089 * @param {OO.ui.Window} win Window being opened
3090 * @param {Object} [data] Window opening data
3091 * @return {number} Milliseconds to wait
3092 */
3093 OO.ui.WindowManager.prototype.getReadyDelay = function () {
3094 return 0;
3095 };
3096
3097 /**
3098 * Get the number of milliseconds to wait after closing has begun before executing the 'hold' process.
3099 *
3100 * @param {OO.ui.Window} win Window being closed
3101 * @param {Object} [data] Window closing data
3102 * @return {number} Milliseconds to wait
3103 */
3104 OO.ui.WindowManager.prototype.getHoldDelay = function () {
3105 return 0;
3106 };
3107
3108 /**
3109 * Get the number of milliseconds to wait after the ‘hold’ process has finished before
3110 * executing the ‘teardown’ process.
3111 *
3112 * @param {OO.ui.Window} win Window being closed
3113 * @param {Object} [data] Window closing data
3114 * @return {number} Milliseconds to wait
3115 */
3116 OO.ui.WindowManager.prototype.getTeardownDelay = function () {
3117 return this.modal ? 250 : 0;
3118 };
3119
3120 /**
3121 * Get a window by its symbolic name.
3122 *
3123 * If the window is not yet instantiated and its symbolic name is recognized by a factory, it will be
3124 * instantiated and added to the window manager automatically. Please see the [OOjs UI documentation on MediaWiki][3]
3125 * for more information about using factories.
3126 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
3127 *
3128 * @param {string} name Symbolic name of the window
3129 * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
3130 * @throws {Error} An error is thrown if the symbolic name is not recognized by the factory.
3131 * @throws {Error} An error is thrown if the named window is not recognized as a managed window.
3132 */
3133 OO.ui.WindowManager.prototype.getWindow = function ( name ) {
3134 var deferred = $.Deferred(),
3135 win = this.windows[ name ];
3136
3137 if ( !( win instanceof OO.ui.Window ) ) {
3138 if ( this.factory ) {
3139 if ( !this.factory.lookup( name ) ) {
3140 deferred.reject( new OO.ui.Error(
3141 'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
3142 ) );
3143 } else {
3144 win = this.factory.create( name );
3145 this.addWindows( [ win ] );
3146 deferred.resolve( win );
3147 }
3148 } else {
3149 deferred.reject( new OO.ui.Error(
3150 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
3151 ) );
3152 }
3153 } else {
3154 deferred.resolve( win );
3155 }
3156
3157 return deferred.promise();
3158 };
3159
3160 /**
3161 * Get current window.
3162 *
3163 * @return {OO.ui.Window|null} Currently opening/opened/closing window
3164 */
3165 OO.ui.WindowManager.prototype.getCurrentWindow = function () {
3166 return this.currentWindow;
3167 };
3168
3169 /**
3170 * Open a window.
3171 *
3172 * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
3173 * @param {Object} [data] Window opening data
3174 * @return {jQuery.Promise} An `opening` promise resolved when the window is done opening.
3175 * See {@link #event-opening 'opening' event} for more information about `opening` promises.
3176 * @fires opening
3177 */
3178 OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
3179 var manager = this,
3180 opening = $.Deferred();
3181
3182 // Argument handling
3183 if ( typeof win === 'string' ) {
3184 return this.getWindow( win ).then( function ( win ) {
3185 return manager.openWindow( win, data );
3186 } );
3187 }
3188
3189 // Error handling
3190 if ( !this.hasWindow( win ) ) {
3191 opening.reject( new OO.ui.Error(
3192 'Cannot open window: window is not attached to manager'
3193 ) );
3194 } else if ( this.preparingToOpen || this.opening || this.opened ) {
3195 opening.reject( new OO.ui.Error(
3196 'Cannot open window: another window is opening or open'
3197 ) );
3198 }
3199
3200 // Window opening
3201 if ( opening.state() !== 'rejected' ) {
3202 // If a window is currently closing, wait for it to complete
3203 this.preparingToOpen = $.when( this.closing );
3204 // Ensure handlers get called after preparingToOpen is set
3205 this.preparingToOpen.done( function () {
3206 if ( manager.modal ) {
3207 manager.toggleGlobalEvents( true );
3208 manager.toggleAriaIsolation( true );
3209 }
3210 manager.currentWindow = win;
3211 manager.opening = opening;
3212 manager.preparingToOpen = null;
3213 manager.emit( 'opening', win, opening, data );
3214 setTimeout( function () {
3215 win.setup( data ).then( function () {
3216 manager.updateWindowSize( win );
3217 manager.opening.notify( { state: 'setup' } );
3218 setTimeout( function () {
3219 win.ready( data ).then( function () {
3220 manager.opening.notify( { state: 'ready' } );
3221 manager.opening = null;
3222 manager.opened = $.Deferred();
3223 opening.resolve( manager.opened.promise(), data );
3224 } );
3225 }, manager.getReadyDelay() );
3226 } );
3227 }, manager.getSetupDelay() );
3228 } );
3229 }
3230
3231 return opening.promise();
3232 };
3233
3234 /**
3235 * Close a window.
3236 *
3237 * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
3238 * @param {Object} [data] Window closing data
3239 * @return {jQuery.Promise} A `closing` promise resolved when the window is done closing.
3240 * See {@link #event-closing 'closing' event} for more information about closing promises.
3241 * @throws {Error} An error is thrown if the window is not managed by the window manager.
3242 * @fires closing
3243 */
3244 OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
3245 var manager = this,
3246 closing = $.Deferred(),
3247 opened;
3248
3249 // Argument handling
3250 if ( typeof win === 'string' ) {
3251 win = this.windows[ win ];
3252 } else if ( !this.hasWindow( win ) ) {
3253 win = null;
3254 }
3255
3256 // Error handling
3257 if ( !win ) {
3258 closing.reject( new OO.ui.Error(
3259 'Cannot close window: window is not attached to manager'
3260 ) );
3261 } else if ( win !== this.currentWindow ) {
3262 closing.reject( new OO.ui.Error(
3263 'Cannot close window: window already closed with different data'
3264 ) );
3265 } else if ( this.preparingToClose || this.closing ) {
3266 closing.reject( new OO.ui.Error(
3267 'Cannot close window: window already closing with different data'
3268 ) );
3269 }
3270
3271 // Window closing
3272 if ( closing.state() !== 'rejected' ) {
3273 // If the window is currently opening, close it when it's done
3274 this.preparingToClose = $.when( this.opening );
3275 // Ensure handlers get called after preparingToClose is set
3276 this.preparingToClose.done( function () {
3277 manager.closing = closing;
3278 manager.preparingToClose = null;
3279 manager.emit( 'closing', win, closing, data );
3280 opened = manager.opened;
3281 manager.opened = null;
3282 opened.resolve( closing.promise(), data );
3283 setTimeout( function () {
3284 win.hold( data ).then( function () {
3285 closing.notify( { state: 'hold' } );
3286 setTimeout( function () {
3287 win.teardown( data ).then( function () {
3288 closing.notify( { state: 'teardown' } );
3289 if ( manager.modal ) {
3290 manager.toggleGlobalEvents( false );
3291 manager.toggleAriaIsolation( false );
3292 }
3293 manager.closing = null;
3294 manager.currentWindow = null;
3295 closing.resolve( data );
3296 } );
3297 }, manager.getTeardownDelay() );
3298 } );
3299 }, manager.getHoldDelay() );
3300 } );
3301 }
3302
3303 return closing.promise();
3304 };
3305
3306 /**
3307 * Add windows to the window manager.
3308 *
3309 * Windows can be added by reference, symbolic name, or explicitly defined symbolic names.
3310 * See the [OOjs ui documentation on MediaWiki] [2] for examples.
3311 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
3312 *
3313 * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows An array of window objects specified
3314 * by reference, symbolic name, or explicitly defined symbolic names.
3315 * @throws {Error} An error is thrown if a window is added by symbolic name, but has neither an
3316 * explicit nor a statically configured symbolic name.
3317 */
3318 OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
3319 var i, len, win, name, list;
3320
3321 if ( Array.isArray( windows ) ) {
3322 // Convert to map of windows by looking up symbolic names from static configuration
3323 list = {};
3324 for ( i = 0, len = windows.length; i < len; i++ ) {
3325 name = windows[ i ].constructor.static.name;
3326 if ( typeof name !== 'string' ) {
3327 throw new Error( 'Cannot add window' );
3328 }
3329 list[ name ] = windows[ i ];
3330 }
3331 } else if ( OO.isPlainObject( windows ) ) {
3332 list = windows;
3333 }
3334
3335 // Add windows
3336 for ( name in list ) {
3337 win = list[ name ];
3338 this.windows[ name ] = win.toggle( false );
3339 this.$element.append( win.$element );
3340 win.setManager( this );
3341 }
3342 };
3343
3344 /**
3345 * Remove the specified windows from the windows manager.
3346 *
3347 * Windows will be closed before they are removed. If you wish to remove all windows, you may wish to use
3348 * the #clearWindows method instead. If you no longer need the window manager and want to ensure that it no
3349 * longer listens to events, use the #destroy method.
3350 *
3351 * @param {string[]} names Symbolic names of windows to remove
3352 * @return {jQuery.Promise} Promise resolved when window is closed and removed
3353 * @throws {Error} An error is thrown if the named windows are not managed by the window manager.
3354 */
3355 OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
3356 var i, len, win, name, cleanupWindow,
3357 manager = this,
3358 promises = [],
3359 cleanup = function ( name, win ) {
3360 delete manager.windows[ name ];
3361 win.$element.detach();
3362 };
3363
3364 for ( i = 0, len = names.length; i < len; i++ ) {
3365 name = names[ i ];
3366 win = this.windows[ name ];
3367 if ( !win ) {
3368 throw new Error( 'Cannot remove window' );
3369 }
3370 cleanupWindow = cleanup.bind( null, name, win );
3371 promises.push( this.closeWindow( name ).then( cleanupWindow, cleanupWindow ) );
3372 }
3373
3374 return $.when.apply( $, promises );
3375 };
3376
3377 /**
3378 * Remove all windows from the window manager.
3379 *
3380 * Windows will be closed before they are removed. Note that the window manager, though not in use, will still
3381 * listen to events. If the window manager will not be used again, you may wish to use the #destroy method instead.
3382 * To remove just a subset of windows, use the #removeWindows method.
3383 *
3384 * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
3385 */
3386 OO.ui.WindowManager.prototype.clearWindows = function () {
3387 return this.removeWindows( Object.keys( this.windows ) );
3388 };
3389
3390 /**
3391 * Set dialog size. In general, this method should not be called directly.
3392 *
3393 * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
3394 *
3395 * @chainable
3396 */
3397 OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) {
3398 // Bypass for non-current, and thus invisible, windows
3399 if ( win !== this.currentWindow ) {
3400 return;
3401 }
3402
3403 var viewport = OO.ui.Element.static.getDimensions( win.getElementWindow() ),
3404 sizes = this.constructor.static.sizes,
3405 size = win.getSize();
3406
3407 if ( !sizes[ size ] ) {
3408 size = this.constructor.static.defaultSize;
3409 }
3410 if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[ size ].width ) {
3411 size = 'full';
3412 }
3413
3414 this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', size === 'full' );
3415 this.$element.toggleClass( 'oo-ui-windowManager-floating', size !== 'full' );
3416 win.setDimensions( sizes[ size ] );
3417
3418 this.emit( 'resize', win );
3419
3420 return this;
3421 };
3422
3423 /**
3424 * Bind or unbind global events for scrolling.
3425 *
3426 * @private
3427 * @param {boolean} [on] Bind global events
3428 * @chainable
3429 */
3430 OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) {
3431 on = on === undefined ? !!this.globalEvents : !!on;
3432
3433 var scrollWidth, bodyMargin,
3434 $body = $( this.getElementDocument().body ),
3435 // We could have multiple window managers open so only modify
3436 // the body css at the bottom of the stack
3437 stackDepth = $body.data( 'windowManagerGlobalEvents' ) || 0 ;
3438
3439 if ( on ) {
3440 if ( !this.globalEvents ) {
3441 $( this.getElementWindow() ).on( {
3442 // Start listening for top-level window dimension changes
3443 'orientationchange resize': this.onWindowResizeHandler
3444 } );
3445 if ( stackDepth === 0 ) {
3446 scrollWidth = window.innerWidth - document.documentElement.clientWidth;
3447 bodyMargin = parseFloat( $body.css( 'margin-right' ) ) || 0;
3448 $body.css( {
3449 overflow: 'hidden',
3450 'margin-right': bodyMargin + scrollWidth
3451 } );
3452 }
3453 stackDepth++;
3454 this.globalEvents = true;
3455 }
3456 } else if ( this.globalEvents ) {
3457 $( this.getElementWindow() ).off( {
3458 // Stop listening for top-level window dimension changes
3459 'orientationchange resize': this.onWindowResizeHandler
3460 } );
3461 stackDepth--;
3462 if ( stackDepth === 0 ) {
3463 $body.css( {
3464 overflow: '',
3465 'margin-right': ''
3466 } );
3467 }
3468 this.globalEvents = false;
3469 }
3470 $body.data( 'windowManagerGlobalEvents', stackDepth );
3471
3472 return this;
3473 };
3474
3475 /**
3476 * Toggle screen reader visibility of content other than the window manager.
3477 *
3478 * @private
3479 * @param {boolean} [isolate] Make only the window manager visible to screen readers
3480 * @chainable
3481 */
3482 OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) {
3483 isolate = isolate === undefined ? !this.$ariaHidden : !!isolate;
3484
3485 if ( isolate ) {
3486 if ( !this.$ariaHidden ) {
3487 // Hide everything other than the window manager from screen readers
3488 this.$ariaHidden = $( 'body' )
3489 .children()
3490 .not( this.$element.parentsUntil( 'body' ).last() )
3491 .attr( 'aria-hidden', '' );
3492 }
3493 } else if ( this.$ariaHidden ) {
3494 // Restore screen reader visibility
3495 this.$ariaHidden.removeAttr( 'aria-hidden' );
3496 this.$ariaHidden = null;
3497 }
3498
3499 return this;
3500 };
3501
3502 /**
3503 * Destroy the window manager.
3504 *
3505 * Destroying the window manager ensures that it will no longer listen to events. If you would like to
3506 * continue using the window manager, but wish to remove all windows from it, use the #clearWindows method
3507 * instead.
3508 */
3509 OO.ui.WindowManager.prototype.destroy = function () {
3510 this.toggleGlobalEvents( false );
3511 this.toggleAriaIsolation( false );
3512 this.clearWindows();
3513 this.$element.remove();
3514 };
3515
3516 /**
3517 * Errors contain a required message (either a string or jQuery selection) that is used to describe what went wrong
3518 * in a {@link OO.ui.Process process}. The error's #recoverable and #warning configurations are used to customize the
3519 * appearance and functionality of the error interface.
3520 *
3521 * The basic error interface contains a formatted error message as well as two buttons: 'Dismiss' and 'Try again' (i.e., the error
3522 * is 'recoverable' by default). If the error is not recoverable, the 'Try again' button will not be rendered and the widget
3523 * that initiated the failed process will be disabled.
3524 *
3525 * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button, which will try the
3526 * process again.
3527 *
3528 * For an example of error interfaces, please see the [OOjs UI documentation on MediaWiki][1].
3529 *
3530 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Processes_and_errors
3531 *
3532 * @class
3533 *
3534 * @constructor
3535 * @param {string|jQuery} message Description of error
3536 * @param {Object} [config] Configuration options
3537 * @cfg {boolean} [recoverable=true] Error is recoverable.
3538 * By default, errors are recoverable, and users can try the process again.
3539 * @cfg {boolean} [warning=false] Error is a warning.
3540 * If the error is a warning, the error interface will include a
3541 * 'Dismiss' and a 'Continue' button. It is the responsibility of the developer to ensure that the warning
3542 * is not triggered a second time if the user chooses to continue.
3543 */
3544 OO.ui.Error = function OoUiError( message, config ) {
3545 // Allow passing positional parameters inside the config object
3546 if ( OO.isPlainObject( message ) && config === undefined ) {
3547 config = message;
3548 message = config.message;
3549 }
3550
3551 // Configuration initialization
3552 config = config || {};
3553
3554 // Properties
3555 this.message = message instanceof jQuery ? message : String( message );
3556 this.recoverable = config.recoverable === undefined || !!config.recoverable;
3557 this.warning = !!config.warning;
3558 };
3559
3560 /* Setup */
3561
3562 OO.initClass( OO.ui.Error );
3563
3564 /* Methods */
3565
3566 /**
3567 * Check if the error is recoverable.
3568 *
3569 * If the error is recoverable, users are able to try the process again.
3570 *
3571 * @return {boolean} Error is recoverable
3572 */
3573 OO.ui.Error.prototype.isRecoverable = function () {
3574 return this.recoverable;
3575 };
3576
3577 /**
3578 * Check if the error is a warning.
3579 *
3580 * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button.
3581 *
3582 * @return {boolean} Error is warning
3583 */
3584 OO.ui.Error.prototype.isWarning = function () {
3585 return this.warning;
3586 };
3587
3588 /**
3589 * Get error message as DOM nodes.
3590 *
3591 * @return {jQuery} Error message in DOM nodes
3592 */
3593 OO.ui.Error.prototype.getMessage = function () {
3594 return this.message instanceof jQuery ?
3595 this.message.clone() :
3596 $( '<div>' ).text( this.message ).contents();
3597 };
3598
3599 /**
3600 * Get the error message text.
3601 *
3602 * @return {string} Error message
3603 */
3604 OO.ui.Error.prototype.getMessageText = function () {
3605 return this.message instanceof jQuery ? this.message.text() : this.message;
3606 };
3607
3608 /**
3609 * Wraps an HTML snippet for use with configuration values which default
3610 * to strings. This bypasses the default html-escaping done to string
3611 * values.
3612 *
3613 * @class
3614 *
3615 * @constructor
3616 * @param {string} [content] HTML content
3617 */
3618 OO.ui.HtmlSnippet = function OoUiHtmlSnippet( content ) {
3619 // Properties
3620 this.content = content;
3621 };
3622
3623 /* Setup */
3624
3625 OO.initClass( OO.ui.HtmlSnippet );
3626
3627 /* Methods */
3628
3629 /**
3630 * Render into HTML.
3631 *
3632 * @return {string} Unchanged HTML snippet.
3633 */
3634 OO.ui.HtmlSnippet.prototype.toString = function () {
3635 return this.content;
3636 };
3637
3638 /**
3639 * A Process is a list of steps that are called in sequence. The step can be a number, a jQuery promise,
3640 * or a function:
3641 *
3642 * - **number**: the process will wait for the specified number of milliseconds before proceeding.
3643 * - **promise**: the process will continue to the next step when the promise is successfully resolved
3644 * or stop if the promise is rejected.
3645 * - **function**: the process will execute the function. The process will stop if the function returns
3646 * either a boolean `false` or a promise that is rejected; if the function returns a number, the process
3647 * will wait for that number of milliseconds before proceeding.
3648 *
3649 * If the process fails, an {@link OO.ui.Error error} is generated. Depending on how the error is
3650 * configured, users can dismiss the error and try the process again, or not. If a process is stopped,
3651 * its remaining steps will not be performed.
3652 *
3653 * @class
3654 *
3655 * @constructor
3656 * @param {number|jQuery.Promise|Function} step Number of miliseconds to wait before proceeding, promise
3657 * that must be resolved before proceeding, or a function to execute. See #createStep for more information. see #createStep for more information
3658 * @param {Object} [context=null] Execution context of the function. The context is ignored if the step is
3659 * a number or promise.
3660 * @return {Object} Step object, with `callback` and `context` properties
3661 */
3662 OO.ui.Process = function ( step, context ) {
3663 // Properties
3664 this.steps = [];
3665
3666 // Initialization
3667 if ( step !== undefined ) {
3668 this.next( step, context );
3669 }
3670 };
3671
3672 /* Setup */
3673
3674 OO.initClass( OO.ui.Process );
3675
3676 /* Methods */
3677
3678 /**
3679 * Start the process.
3680 *
3681 * @return {jQuery.Promise} Promise that is resolved when all steps have successfully completed.
3682 * If any of the steps return a promise that is rejected or a boolean false, this promise is rejected
3683 * and any remaining steps are not performed.
3684 */
3685 OO.ui.Process.prototype.execute = function () {
3686 var i, len, promise;
3687
3688 /**
3689 * Continue execution.
3690 *
3691 * @ignore
3692 * @param {Array} step A function and the context it should be called in
3693 * @return {Function} Function that continues the process
3694 */
3695 function proceed( step ) {
3696 return function () {
3697 // Execute step in the correct context
3698 var deferred,
3699 result = step.callback.call( step.context );
3700
3701 if ( result === false ) {
3702 // Use rejected promise for boolean false results
3703 return $.Deferred().reject( [] ).promise();
3704 }
3705 if ( typeof result === 'number' ) {
3706 if ( result < 0 ) {
3707 throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
3708 }
3709 // Use a delayed promise for numbers, expecting them to be in milliseconds
3710 deferred = $.Deferred();
3711 setTimeout( deferred.resolve, result );
3712 return deferred.promise();
3713 }
3714 if ( result instanceof OO.ui.Error ) {
3715 // Use rejected promise for error
3716 return $.Deferred().reject( [ result ] ).promise();
3717 }
3718 if ( Array.isArray( result ) && result.length && result[ 0 ] instanceof OO.ui.Error ) {
3719 // Use rejected promise for list of errors
3720 return $.Deferred().reject( result ).promise();
3721 }
3722 // Duck-type the object to see if it can produce a promise
3723 if ( result && $.isFunction( result.promise ) ) {
3724 // Use a promise generated from the result
3725 return result.promise();
3726 }
3727 // Use resolved promise for other results
3728 return $.Deferred().resolve().promise();
3729 };
3730 }
3731
3732 if ( this.steps.length ) {
3733 // Generate a chain reaction of promises
3734 promise = proceed( this.steps[ 0 ] )();
3735 for ( i = 1, len = this.steps.length; i < len; i++ ) {
3736 promise = promise.then( proceed( this.steps[ i ] ) );
3737 }
3738 } else {
3739 promise = $.Deferred().resolve().promise();
3740 }
3741
3742 return promise;
3743 };
3744
3745 /**
3746 * Create a process step.
3747 *
3748 * @private
3749 * @param {number|jQuery.Promise|Function} step
3750 *
3751 * - Number of milliseconds to wait before proceeding
3752 * - Promise that must be resolved before proceeding
3753 * - Function to execute
3754 * - If the function returns a boolean false the process will stop
3755 * - If the function returns a promise, the process will continue to the next
3756 * step when the promise is resolved or stop if the promise is rejected
3757 * - If the function returns a number, the process will wait for that number of
3758 * milliseconds before proceeding
3759 * @param {Object} [context=null] Execution context of the function. The context is
3760 * ignored if the step is a number or promise.
3761 * @return {Object} Step object, with `callback` and `context` properties
3762 */
3763 OO.ui.Process.prototype.createStep = function ( step, context ) {
3764 if ( typeof step === 'number' || $.isFunction( step.promise ) ) {
3765 return {
3766 callback: function () {
3767 return step;
3768 },
3769 context: null
3770 };
3771 }
3772 if ( $.isFunction( step ) ) {
3773 return {
3774 callback: step,
3775 context: context
3776 };
3777 }
3778 throw new Error( 'Cannot create process step: number, promise or function expected' );
3779 };
3780
3781 /**
3782 * Add step to the beginning of the process.
3783 *
3784 * @inheritdoc #createStep
3785 * @return {OO.ui.Process} this
3786 * @chainable
3787 */
3788 OO.ui.Process.prototype.first = function ( step, context ) {
3789 this.steps.unshift( this.createStep( step, context ) );
3790 return this;
3791 };
3792
3793 /**
3794 * Add step to the end of the process.
3795 *
3796 * @inheritdoc #createStep
3797 * @return {OO.ui.Process} this
3798 * @chainable
3799 */
3800 OO.ui.Process.prototype.next = function ( step, context ) {
3801 this.steps.push( this.createStep( step, context ) );
3802 return this;
3803 };
3804
3805 /**
3806 * A ToolFactory creates tools on demand. All tools ({@link OO.ui.Tool Tools}, {@link OO.ui.PopupTool PopupTools},
3807 * and {@link OO.ui.ToolGroupTool ToolGroupTools}) must be registered with a tool factory. Tools are
3808 * registered by their symbolic name. See {@link OO.ui.Toolbar toolbars} for an example.
3809 *
3810 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
3811 *
3812 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
3813 *
3814 * @class
3815 * @extends OO.Factory
3816 * @constructor
3817 */
3818 OO.ui.ToolFactory = function OoUiToolFactory() {
3819 // Parent constructor
3820 OO.ui.ToolFactory.super.call( this );
3821 };
3822
3823 /* Setup */
3824
3825 OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
3826
3827 /* Methods */
3828
3829 /**
3830 * Get tools from the factory
3831 *
3832 * @param {Array} include Included tools
3833 * @param {Array} exclude Excluded tools
3834 * @param {Array} promote Promoted tools
3835 * @param {Array} demote Demoted tools
3836 * @return {string[]} List of tools
3837 */
3838 OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
3839 var i, len, included, promoted, demoted,
3840 auto = [],
3841 used = {};
3842
3843 // Collect included and not excluded tools
3844 included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
3845
3846 // Promotion
3847 promoted = this.extract( promote, used );
3848 demoted = this.extract( demote, used );
3849
3850 // Auto
3851 for ( i = 0, len = included.length; i < len; i++ ) {
3852 if ( !used[ included[ i ] ] ) {
3853 auto.push( included[ i ] );
3854 }
3855 }
3856
3857 return promoted.concat( auto ).concat( demoted );
3858 };
3859
3860 /**
3861 * Get a flat list of names from a list of names or groups.
3862 *
3863 * Tools can be specified in the following ways:
3864 *
3865 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
3866 * - All tools in a group: `{ group: 'group-name' }`
3867 * - All tools: `'*'`
3868 *
3869 * @private
3870 * @param {Array|string} collection List of tools
3871 * @param {Object} [used] Object with names that should be skipped as properties; extracted
3872 * names will be added as properties
3873 * @return {string[]} List of extracted names
3874 */
3875 OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
3876 var i, len, item, name, tool,
3877 names = [];
3878
3879 if ( collection === '*' ) {
3880 for ( name in this.registry ) {
3881 tool = this.registry[ name ];
3882 if (
3883 // Only add tools by group name when auto-add is enabled
3884 tool.static.autoAddToCatchall &&
3885 // Exclude already used tools
3886 ( !used || !used[ name ] )
3887 ) {
3888 names.push( name );
3889 if ( used ) {
3890 used[ name ] = true;
3891 }
3892 }
3893 }
3894 } else if ( Array.isArray( collection ) ) {
3895 for ( i = 0, len = collection.length; i < len; i++ ) {
3896 item = collection[ i ];
3897 // Allow plain strings as shorthand for named tools
3898 if ( typeof item === 'string' ) {
3899 item = { name: item };
3900 }
3901 if ( OO.isPlainObject( item ) ) {
3902 if ( item.group ) {
3903 for ( name in this.registry ) {
3904 tool = this.registry[ name ];
3905 if (
3906 // Include tools with matching group
3907 tool.static.group === item.group &&
3908 // Only add tools by group name when auto-add is enabled
3909 tool.static.autoAddToGroup &&
3910 // Exclude already used tools
3911 ( !used || !used[ name ] )
3912 ) {
3913 names.push( name );
3914 if ( used ) {
3915 used[ name ] = true;
3916 }
3917 }
3918 }
3919 // Include tools with matching name and exclude already used tools
3920 } else if ( item.name && ( !used || !used[ item.name ] ) ) {
3921 names.push( item.name );
3922 if ( used ) {
3923 used[ item.name ] = true;
3924 }
3925 }
3926 }
3927 }
3928 }
3929 return names;
3930 };
3931
3932 /**
3933 * ToolGroupFactories create {@link OO.ui.ToolGroup toolgroups} on demand. The toolgroup classes must
3934 * specify a symbolic name and be registered with the factory. The following classes are registered by
3935 * default:
3936 *
3937 * - {@link OO.ui.BarToolGroup BarToolGroups} (‘bar’)
3938 * - {@link OO.ui.MenuToolGroup MenuToolGroups} (‘menu’)
3939 * - {@link OO.ui.ListToolGroup ListToolGroups} (‘list’)
3940 *
3941 * See {@link OO.ui.Toolbar toolbars} for an example.
3942 *
3943 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
3944 *
3945 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
3946 * @class
3947 * @extends OO.Factory
3948 * @constructor
3949 */
3950 OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() {
3951 // Parent constructor
3952 OO.Factory.call( this );
3953
3954 var i, l,
3955 defaultClasses = this.constructor.static.getDefaultClasses();
3956
3957 // Register default toolgroups
3958 for ( i = 0, l = defaultClasses.length; i < l; i++ ) {
3959 this.register( defaultClasses[ i ] );
3960 }
3961 };
3962
3963 /* Setup */
3964
3965 OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory );
3966
3967 /* Static Methods */
3968
3969 /**
3970 * Get a default set of classes to be registered on construction.
3971 *
3972 * @return {Function[]} Default classes
3973 */
3974 OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
3975 return [
3976 OO.ui.BarToolGroup,
3977 OO.ui.ListToolGroup,
3978 OO.ui.MenuToolGroup
3979 ];
3980 };
3981
3982 /**
3983 * Theme logic.
3984 *
3985 * @abstract
3986 * @class
3987 *
3988 * @constructor
3989 * @param {Object} [config] Configuration options
3990 */
3991 OO.ui.Theme = function OoUiTheme( config ) {
3992 // Configuration initialization
3993 config = config || {};
3994 };
3995
3996 /* Setup */
3997
3998 OO.initClass( OO.ui.Theme );
3999
4000 /* Methods */
4001
4002 /**
4003 * Get a list of classes to be applied to a widget.
4004 *
4005 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
4006 * otherwise state transitions will not work properly.
4007 *
4008 * @param {OO.ui.Element} element Element for which to get classes
4009 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
4010 */
4011 OO.ui.Theme.prototype.getElementClasses = function ( /* element */ ) {
4012 return { on: [], off: [] };
4013 };
4014
4015 /**
4016 * Update CSS classes provided by the theme.
4017 *
4018 * For elements with theme logic hooks, this should be called any time there's a state change.
4019 *
4020 * @param {OO.ui.Element} element Element for which to update classes
4021 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
4022 */
4023 OO.ui.Theme.prototype.updateElementClasses = function ( element ) {
4024 var classes = this.getElementClasses( element );
4025
4026 element.$element
4027 .removeClass( classes.off.join( ' ' ) )
4028 .addClass( classes.on.join( ' ' ) );
4029 };
4030
4031 /**
4032 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
4033 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
4034 * order in which users will navigate through the focusable elements via the "tab" key.
4035 *
4036 * @example
4037 * // TabIndexedElement is mixed into the ButtonWidget class
4038 * // to provide a tabIndex property.
4039 * var button1 = new OO.ui.ButtonWidget( {
4040 * label: 'fourth',
4041 * tabIndex: 4
4042 * } );
4043 * var button2 = new OO.ui.ButtonWidget( {
4044 * label: 'second',
4045 * tabIndex: 2
4046 * } );
4047 * var button3 = new OO.ui.ButtonWidget( {
4048 * label: 'third',
4049 * tabIndex: 3
4050 * } );
4051 * var button4 = new OO.ui.ButtonWidget( {
4052 * label: 'first',
4053 * tabIndex: 1
4054 * } );
4055 * $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
4056 *
4057 * @abstract
4058 * @class
4059 *
4060 * @constructor
4061 * @param {Object} [config] Configuration options
4062 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
4063 * the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
4064 * functionality will be applied to it instead.
4065 * @cfg {number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
4066 * order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
4067 * to remove the element from the tab-navigation flow.
4068 */
4069 OO.ui.mixin.TabIndexedElement = function OoUiMixinTabIndexedElement( config ) {
4070 // Configuration initialization
4071 config = $.extend( { tabIndex: 0 }, config );
4072
4073 // Properties
4074 this.$tabIndexed = null;
4075 this.tabIndex = null;
4076
4077 // Events
4078 this.connect( this, { disable: 'onTabIndexedElementDisable' } );
4079
4080 // Initialization
4081 this.setTabIndex( config.tabIndex );
4082 this.setTabIndexedElement( config.$tabIndexed || this.$element );
4083 };
4084
4085 /* Setup */
4086
4087 OO.initClass( OO.ui.mixin.TabIndexedElement );
4088
4089 /* Methods */
4090
4091 /**
4092 * Set the element that should use the tabindex functionality.
4093 *
4094 * This method is used to retarget a tabindex mixin so that its functionality applies
4095 * to the specified element. If an element is currently using the functionality, the mixin’s
4096 * effect on that element is removed before the new element is set up.
4097 *
4098 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
4099 * @chainable
4100 */
4101 OO.ui.mixin.TabIndexedElement.prototype.setTabIndexedElement = function ( $tabIndexed ) {
4102 var tabIndex = this.tabIndex;
4103 // Remove attributes from old $tabIndexed
4104 this.setTabIndex( null );
4105 // Force update of new $tabIndexed
4106 this.$tabIndexed = $tabIndexed;
4107 this.tabIndex = tabIndex;
4108 return this.updateTabIndex();
4109 };
4110
4111 /**
4112 * Set the value of the tabindex.
4113 *
4114 * @param {number|null} tabIndex Tabindex value, or `null` for no tabindex
4115 * @chainable
4116 */
4117 OO.ui.mixin.TabIndexedElement.prototype.setTabIndex = function ( tabIndex ) {
4118 tabIndex = typeof tabIndex === 'number' ? tabIndex : null;
4119
4120 if ( this.tabIndex !== tabIndex ) {
4121 this.tabIndex = tabIndex;
4122 this.updateTabIndex();
4123 }
4124
4125 return this;
4126 };
4127
4128 /**
4129 * Update the `tabindex` attribute, in case of changes to tab index or
4130 * disabled state.
4131 *
4132 * @private
4133 * @chainable
4134 */
4135 OO.ui.mixin.TabIndexedElement.prototype.updateTabIndex = function () {
4136 if ( this.$tabIndexed ) {
4137 if ( this.tabIndex !== null ) {
4138 // Do not index over disabled elements
4139 this.$tabIndexed.attr( {
4140 tabindex: this.isDisabled() ? -1 : this.tabIndex,
4141 // ChromeVox and NVDA do not seem to inherit this from parent elements
4142 'aria-disabled': this.isDisabled().toString()
4143 } );
4144 } else {
4145 this.$tabIndexed.removeAttr( 'tabindex aria-disabled' );
4146 }
4147 }
4148 return this;
4149 };
4150
4151 /**
4152 * Handle disable events.
4153 *
4154 * @private
4155 * @param {boolean} disabled Element is disabled
4156 */
4157 OO.ui.mixin.TabIndexedElement.prototype.onTabIndexedElementDisable = function () {
4158 this.updateTabIndex();
4159 };
4160
4161 /**
4162 * Get the value of the tabindex.
4163 *
4164 * @return {number|null} Tabindex value
4165 */
4166 OO.ui.mixin.TabIndexedElement.prototype.getTabIndex = function () {
4167 return this.tabIndex;
4168 };
4169
4170 /**
4171 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
4172 * interface element that can be configured with access keys for accessibility.
4173 * See the [OOjs UI documentation on MediaWiki] [1] for examples.
4174 *
4175 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
4176 * @abstract
4177 * @class
4178 *
4179 * @constructor
4180 * @param {Object} [config] Configuration options
4181 * @cfg {jQuery} [$button] The button element created by the class.
4182 * If this configuration is omitted, the button element will use a generated `<a>`.
4183 * @cfg {boolean} [framed=true] Render the button with a frame
4184 * @cfg {string} [accessKey] Button's access key
4185 */
4186 OO.ui.mixin.ButtonElement = function OoUiMixinButtonElement( config ) {
4187 // Configuration initialization
4188 config = config || {};
4189
4190 // Properties
4191 this.$button = null;
4192 this.framed = null;
4193 this.accessKey = null;
4194 this.active = false;
4195 this.onMouseUpHandler = this.onMouseUp.bind( this );
4196 this.onMouseDownHandler = this.onMouseDown.bind( this );
4197 this.onKeyDownHandler = this.onKeyDown.bind( this );
4198 this.onKeyUpHandler = this.onKeyUp.bind( this );
4199 this.onClickHandler = this.onClick.bind( this );
4200 this.onKeyPressHandler = this.onKeyPress.bind( this );
4201
4202 // Initialization
4203 this.$element.addClass( 'oo-ui-buttonElement' );
4204 this.toggleFramed( config.framed === undefined || config.framed );
4205 this.setAccessKey( config.accessKey );
4206 this.setButtonElement( config.$button || $( '<a>' ) );
4207 };
4208
4209 /* Setup */
4210
4211 OO.initClass( OO.ui.mixin.ButtonElement );
4212
4213 /* Static Properties */
4214
4215 /**
4216 * Cancel mouse down events.
4217 *
4218 * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
4219 * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
4220 * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
4221 * parent widget.
4222 *
4223 * @static
4224 * @inheritable
4225 * @property {boolean}
4226 */
4227 OO.ui.mixin.ButtonElement.static.cancelButtonMouseDownEvents = true;
4228
4229 /* Events */
4230
4231 /**
4232 * A 'click' event is emitted when the button element is clicked.
4233 *
4234 * @event click
4235 */
4236
4237 /* Methods */
4238
4239 /**
4240 * Set the button element.
4241 *
4242 * This method is used to retarget a button mixin so that its functionality applies to
4243 * the specified button element instead of the one created by the class. If a button element
4244 * is already set, the method will remove the mixin’s effect on that element.
4245 *
4246 * @param {jQuery} $button Element to use as button
4247 */
4248 OO.ui.mixin.ButtonElement.prototype.setButtonElement = function ( $button ) {
4249 if ( this.$button ) {
4250 this.$button
4251 .removeClass( 'oo-ui-buttonElement-button' )
4252 .removeAttr( 'role accesskey' )
4253 .off( {
4254 mousedown: this.onMouseDownHandler,
4255 keydown: this.onKeyDownHandler,
4256 click: this.onClickHandler,
4257 keypress: this.onKeyPressHandler
4258 } );
4259 }
4260
4261 this.$button = $button
4262 .addClass( 'oo-ui-buttonElement-button' )
4263 .attr( { role: 'button', accesskey: this.accessKey } )
4264 .on( {
4265 mousedown: this.onMouseDownHandler,
4266 keydown: this.onKeyDownHandler,
4267 click: this.onClickHandler,
4268 keypress: this.onKeyPressHandler
4269 } );
4270 };
4271
4272 /**
4273 * Handles mouse down events.
4274 *
4275 * @protected
4276 * @param {jQuery.Event} e Mouse down event
4277 */
4278 OO.ui.mixin.ButtonElement.prototype.onMouseDown = function ( e ) {
4279 if ( this.isDisabled() || e.which !== 1 ) {
4280 return;
4281 }
4282 this.$element.addClass( 'oo-ui-buttonElement-pressed' );
4283 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
4284 // reliably remove the pressed class
4285 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
4286 // Prevent change of focus unless specifically configured otherwise
4287 if ( this.constructor.static.cancelButtonMouseDownEvents ) {
4288 return false;
4289 }
4290 };
4291
4292 /**
4293 * Handles mouse up events.
4294 *
4295 * @protected
4296 * @param {jQuery.Event} e Mouse up event
4297 */
4298 OO.ui.mixin.ButtonElement.prototype.onMouseUp = function ( e ) {
4299 if ( this.isDisabled() || e.which !== 1 ) {
4300 return;
4301 }
4302 this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
4303 // Stop listening for mouseup, since we only needed this once
4304 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
4305 };
4306
4307 /**
4308 * Handles mouse click events.
4309 *
4310 * @protected
4311 * @param {jQuery.Event} e Mouse click event
4312 * @fires click
4313 */
4314 OO.ui.mixin.ButtonElement.prototype.onClick = function ( e ) {
4315 if ( !this.isDisabled() && e.which === 1 ) {
4316 if ( this.emit( 'click' ) ) {
4317 return false;
4318 }
4319 }
4320 };
4321
4322 /**
4323 * Handles key down events.
4324 *
4325 * @protected
4326 * @param {jQuery.Event} e Key down event
4327 */
4328 OO.ui.mixin.ButtonElement.prototype.onKeyDown = function ( e ) {
4329 if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
4330 return;
4331 }
4332 this.$element.addClass( 'oo-ui-buttonElement-pressed' );
4333 // Run the keyup handler no matter where the key is when the button is let go, so we can
4334 // reliably remove the pressed class
4335 this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler, true );
4336 };
4337
4338 /**
4339 * Handles key up events.
4340 *
4341 * @protected
4342 * @param {jQuery.Event} e Key up event
4343 */
4344 OO.ui.mixin.ButtonElement.prototype.onKeyUp = function ( e ) {
4345 if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
4346 return;
4347 }
4348 this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
4349 // Stop listening for keyup, since we only needed this once
4350 this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler, true );
4351 };
4352
4353 /**
4354 * Handles key press events.
4355 *
4356 * @protected
4357 * @param {jQuery.Event} e Key press event
4358 * @fires click
4359 */
4360 OO.ui.mixin.ButtonElement.prototype.onKeyPress = function ( e ) {
4361 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
4362 if ( this.emit( 'click' ) ) {
4363 return false;
4364 }
4365 }
4366 };
4367
4368 /**
4369 * Check if button has a frame.
4370 *
4371 * @return {boolean} Button is framed
4372 */
4373 OO.ui.mixin.ButtonElement.prototype.isFramed = function () {
4374 return this.framed;
4375 };
4376
4377 /**
4378 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
4379 *
4380 * @param {boolean} [framed] Make button framed, omit to toggle
4381 * @chainable
4382 */
4383 OO.ui.mixin.ButtonElement.prototype.toggleFramed = function ( framed ) {
4384 framed = framed === undefined ? !this.framed : !!framed;
4385 if ( framed !== this.framed ) {
4386 this.framed = framed;
4387 this.$element
4388 .toggleClass( 'oo-ui-buttonElement-frameless', !framed )
4389 .toggleClass( 'oo-ui-buttonElement-framed', framed );
4390 this.updateThemeClasses();
4391 }
4392
4393 return this;
4394 };
4395
4396 /**
4397 * Set the button's access key.
4398 *
4399 * @param {string} accessKey Button's access key, use empty string to remove
4400 * @chainable
4401 */
4402 OO.ui.mixin.ButtonElement.prototype.setAccessKey = function ( accessKey ) {
4403 accessKey = typeof accessKey === 'string' && accessKey.length ? accessKey : null;
4404
4405 if ( this.accessKey !== accessKey ) {
4406 if ( this.$button ) {
4407 if ( accessKey !== null ) {
4408 this.$button.attr( 'accesskey', accessKey );
4409 } else {
4410 this.$button.removeAttr( 'accesskey' );
4411 }
4412 }
4413 this.accessKey = accessKey;
4414 }
4415
4416 return this;
4417 };
4418
4419 /**
4420 * Set the button to its 'active' state.
4421 *
4422 * The active state occurs when a {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} or
4423 * a {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} is pressed. This method does nothing
4424 * for other button types.
4425 *
4426 * @param {boolean} [value] Make button active
4427 * @chainable
4428 */
4429 OO.ui.mixin.ButtonElement.prototype.setActive = function ( value ) {
4430 this.$element.toggleClass( 'oo-ui-buttonElement-active', !!value );
4431 return this;
4432 };
4433
4434 /**
4435 * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
4436 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
4437 * items from the group is done through the interface the class provides.
4438 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
4439 *
4440 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
4441 *
4442 * @abstract
4443 * @class
4444 *
4445 * @constructor
4446 * @param {Object} [config] Configuration options
4447 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
4448 * is omitted, the group element will use a generated `<div>`.
4449 */
4450 OO.ui.mixin.GroupElement = function OoUiMixinGroupElement( config ) {
4451 // Configuration initialization
4452 config = config || {};
4453
4454 // Properties
4455 this.$group = null;
4456 this.items = [];
4457 this.aggregateItemEvents = {};
4458
4459 // Initialization
4460 this.setGroupElement( config.$group || $( '<div>' ) );
4461 };
4462
4463 /* Methods */
4464
4465 /**
4466 * Set the group element.
4467 *
4468 * If an element is already set, items will be moved to the new element.
4469 *
4470 * @param {jQuery} $group Element to use as group
4471 */
4472 OO.ui.mixin.GroupElement.prototype.setGroupElement = function ( $group ) {
4473 var i, len;
4474
4475 this.$group = $group;
4476 for ( i = 0, len = this.items.length; i < len; i++ ) {
4477 this.$group.append( this.items[ i ].$element );
4478 }
4479 };
4480
4481 /**
4482 * Check if a group contains no items.
4483 *
4484 * @return {boolean} Group is empty
4485 */
4486 OO.ui.mixin.GroupElement.prototype.isEmpty = function () {
4487 return !this.items.length;
4488 };
4489
4490 /**
4491 * Get all items in the group.
4492 *
4493 * The method returns an array of item references (e.g., [button1, button2, button3]) and is useful
4494 * when synchronizing groups of items, or whenever the references are required (e.g., when removing items
4495 * from a group).
4496 *
4497 * @return {OO.ui.Element[]} An array of items.
4498 */
4499 OO.ui.mixin.GroupElement.prototype.getItems = function () {
4500 return this.items.slice( 0 );
4501 };
4502
4503 /**
4504 * Get an item by its data.
4505 *
4506 * Only the first item with matching data will be returned. To return all matching items,
4507 * use the #getItemsFromData method.
4508 *
4509 * @param {Object} data Item data to search for
4510 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
4511 */
4512 OO.ui.mixin.GroupElement.prototype.getItemFromData = function ( data ) {
4513 var i, len, item,
4514 hash = OO.getHash( data );
4515
4516 for ( i = 0, len = this.items.length; i < len; i++ ) {
4517 item = this.items[ i ];
4518 if ( hash === OO.getHash( item.getData() ) ) {
4519 return item;
4520 }
4521 }
4522
4523 return null;
4524 };
4525
4526 /**
4527 * Get items by their data.
4528 *
4529 * All items with matching data will be returned. To return only the first match, use the #getItemFromData method instead.
4530 *
4531 * @param {Object} data Item data to search for
4532 * @return {OO.ui.Element[]} Items with equivalent data
4533 */
4534 OO.ui.mixin.GroupElement.prototype.getItemsFromData = function ( data ) {
4535 var i, len, item,
4536 hash = OO.getHash( data ),
4537 items = [];
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 items.push( item );
4543 }
4544 }
4545
4546 return items;
4547 };
4548
4549 /**
4550 * Aggregate the events emitted by the group.
4551 *
4552 * When events are aggregated, the group will listen to all contained items for the event,
4553 * and then emit the event under a new name. The new event will contain an additional leading
4554 * parameter containing the item that emitted the original event. Other arguments emitted from
4555 * the original event are passed through.
4556 *
4557 * @param {Object.<string,string|null>} events An object keyed by the name of the event that should be
4558 * aggregated (e.g., ‘click’) and the value of the new name to use (e.g., ‘groupClick’).
4559 * A `null` value will remove aggregated events.
4560
4561 * @throws {Error} An error is thrown if aggregation already exists.
4562 */
4563 OO.ui.mixin.GroupElement.prototype.aggregate = function ( events ) {
4564 var i, len, item, add, remove, itemEvent, groupEvent;
4565
4566 for ( itemEvent in events ) {
4567 groupEvent = events[ itemEvent ];
4568
4569 // Remove existing aggregated event
4570 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4571 // Don't allow duplicate aggregations
4572 if ( groupEvent ) {
4573 throw new Error( 'Duplicate item event aggregation for ' + itemEvent );
4574 }
4575 // Remove event aggregation from existing items
4576 for ( i = 0, len = this.items.length; i < len; i++ ) {
4577 item = this.items[ i ];
4578 if ( item.connect && item.disconnect ) {
4579 remove = {};
4580 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[itemEvent], item ];
4581 item.disconnect( this, remove );
4582 }
4583 }
4584 // Prevent future items from aggregating event
4585 delete this.aggregateItemEvents[ itemEvent ];
4586 }
4587
4588 // Add new aggregate event
4589 if ( groupEvent ) {
4590 // Make future items aggregate event
4591 this.aggregateItemEvents[ itemEvent ] = groupEvent;
4592 // Add event aggregation to existing items
4593 for ( i = 0, len = this.items.length; i < len; i++ ) {
4594 item = this.items[ i ];
4595 if ( item.connect && item.disconnect ) {
4596 add = {};
4597 add[ itemEvent ] = [ 'emit', groupEvent, item ];
4598 item.connect( this, add );
4599 }
4600 }
4601 }
4602 }
4603 };
4604
4605 /**
4606 * Add items to the group.
4607 *
4608 * Items will be added to the end of the group array unless the optional `index` parameter specifies
4609 * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
4610 *
4611 * @param {OO.ui.Element[]} items An array of items to add to the group
4612 * @param {number} [index] Index of the insertion point
4613 * @chainable
4614 */
4615 OO.ui.mixin.GroupElement.prototype.addItems = function ( items, index ) {
4616 var i, len, item, event, events, currentIndex,
4617 itemElements = [];
4618
4619 for ( i = 0, len = items.length; i < len; i++ ) {
4620 item = items[ i ];
4621
4622 // Check if item exists then remove it first, effectively "moving" it
4623 currentIndex = $.inArray( item, this.items );
4624 if ( currentIndex >= 0 ) {
4625 this.removeItems( [ item ] );
4626 // Adjust index to compensate for removal
4627 if ( currentIndex < index ) {
4628 index--;
4629 }
4630 }
4631 // Add the item
4632 if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
4633 events = {};
4634 for ( event in this.aggregateItemEvents ) {
4635 events[ event ] = [ 'emit', this.aggregateItemEvents[ event ], item ];
4636 }
4637 item.connect( this, events );
4638 }
4639 item.setElementGroup( this );
4640 itemElements.push( item.$element.get( 0 ) );
4641 }
4642
4643 if ( index === undefined || index < 0 || index >= this.items.length ) {
4644 this.$group.append( itemElements );
4645 this.items.push.apply( this.items, items );
4646 } else if ( index === 0 ) {
4647 this.$group.prepend( itemElements );
4648 this.items.unshift.apply( this.items, items );
4649 } else {
4650 this.items[ index ].$element.before( itemElements );
4651 this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
4652 }
4653
4654 return this;
4655 };
4656
4657 /**
4658 * Remove the specified items from a group.
4659 *
4660 * Removed items are detached (not removed) from the DOM so that they may be reused.
4661 * To remove all items from a group, you may wish to use the #clearItems method instead.
4662 *
4663 * @param {OO.ui.Element[]} items An array of items to remove
4664 * @chainable
4665 */
4666 OO.ui.mixin.GroupElement.prototype.removeItems = function ( items ) {
4667 var i, len, item, index, remove, itemEvent;
4668
4669 // Remove specific items
4670 for ( i = 0, len = items.length; i < len; i++ ) {
4671 item = items[ i ];
4672 index = $.inArray( item, this.items );
4673 if ( index !== -1 ) {
4674 if (
4675 item.connect && item.disconnect &&
4676 !$.isEmptyObject( this.aggregateItemEvents )
4677 ) {
4678 remove = {};
4679 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4680 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
4681 }
4682 item.disconnect( this, remove );
4683 }
4684 item.setElementGroup( null );
4685 this.items.splice( index, 1 );
4686 item.$element.detach();
4687 }
4688 }
4689
4690 return this;
4691 };
4692
4693 /**
4694 * Clear all items from the group.
4695 *
4696 * Cleared items are detached from the DOM, not removed, so that they may be reused.
4697 * To remove only a subset of items from a group, use the #removeItems method.
4698 *
4699 * @chainable
4700 */
4701 OO.ui.mixin.GroupElement.prototype.clearItems = function () {
4702 var i, len, item, remove, itemEvent;
4703
4704 // Remove all items
4705 for ( i = 0, len = this.items.length; i < len; i++ ) {
4706 item = this.items[ i ];
4707 if (
4708 item.connect && item.disconnect &&
4709 !$.isEmptyObject( this.aggregateItemEvents )
4710 ) {
4711 remove = {};
4712 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4713 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
4714 }
4715 item.disconnect( this, remove );
4716 }
4717 item.setElementGroup( null );
4718 item.$element.detach();
4719 }
4720
4721 this.items = [];
4722 return this;
4723 };
4724
4725 /**
4726 * DraggableElement is a mixin class used to create elements that can be clicked
4727 * and dragged by a mouse to a new position within a group. This class must be used
4728 * in conjunction with OO.ui.mixin.DraggableGroupElement, which provides a container for
4729 * the draggable elements.
4730 *
4731 * @abstract
4732 * @class
4733 *
4734 * @constructor
4735 */
4736 OO.ui.mixin.DraggableElement = function OoUiMixinDraggableElement() {
4737 // Properties
4738 this.index = null;
4739
4740 // Initialize and events
4741 this.$element
4742 .attr( 'draggable', true )
4743 .addClass( 'oo-ui-draggableElement' )
4744 .on( {
4745 dragstart: this.onDragStart.bind( this ),
4746 dragover: this.onDragOver.bind( this ),
4747 dragend: this.onDragEnd.bind( this ),
4748 drop: this.onDrop.bind( this )
4749 } );
4750 };
4751
4752 OO.initClass( OO.ui.mixin.DraggableElement );
4753
4754 /* Events */
4755
4756 /**
4757 * @event dragstart
4758 *
4759 * A dragstart event is emitted when the user clicks and begins dragging an item.
4760 * @param {OO.ui.mixin.DraggableElement} item The item the user has clicked and is dragging with the mouse.
4761 */
4762
4763 /**
4764 * @event dragend
4765 * A dragend event is emitted when the user drags an item and releases the mouse,
4766 * thus terminating the drag operation.
4767 */
4768
4769 /**
4770 * @event drop
4771 * A drop event is emitted when the user drags an item and then releases the mouse button
4772 * over a valid target.
4773 */
4774
4775 /* Static Properties */
4776
4777 /**
4778 * @inheritdoc OO.ui.mixin.ButtonElement
4779 */
4780 OO.ui.mixin.DraggableElement.static.cancelButtonMouseDownEvents = false;
4781
4782 /* Methods */
4783
4784 /**
4785 * Respond to dragstart event.
4786 *
4787 * @private
4788 * @param {jQuery.Event} event jQuery event
4789 * @fires dragstart
4790 */
4791 OO.ui.mixin.DraggableElement.prototype.onDragStart = function ( e ) {
4792 var dataTransfer = e.originalEvent.dataTransfer;
4793 // Define drop effect
4794 dataTransfer.dropEffect = 'none';
4795 dataTransfer.effectAllowed = 'move';
4796 // We must set up a dataTransfer data property or Firefox seems to
4797 // ignore the fact the element is draggable.
4798 try {
4799 dataTransfer.setData( 'application-x/OOjs-UI-draggable', this.getIndex() );
4800 } catch ( err ) {
4801 // The above is only for firefox. No need to set a catch clause
4802 // if it fails, move on.
4803 }
4804 // Add dragging class
4805 this.$element.addClass( 'oo-ui-draggableElement-dragging' );
4806 // Emit event
4807 this.emit( 'dragstart', this );
4808 return true;
4809 };
4810
4811 /**
4812 * Respond to dragend event.
4813 *
4814 * @private
4815 * @fires dragend
4816 */
4817 OO.ui.mixin.DraggableElement.prototype.onDragEnd = function () {
4818 this.$element.removeClass( 'oo-ui-draggableElement-dragging' );
4819 this.emit( 'dragend' );
4820 };
4821
4822 /**
4823 * Handle drop event.
4824 *
4825 * @private
4826 * @param {jQuery.Event} event jQuery event
4827 * @fires drop
4828 */
4829 OO.ui.mixin.DraggableElement.prototype.onDrop = function ( e ) {
4830 e.preventDefault();
4831 this.emit( 'drop', e );
4832 };
4833
4834 /**
4835 * In order for drag/drop to work, the dragover event must
4836 * return false and stop propogation.
4837 *
4838 * @private
4839 */
4840 OO.ui.mixin.DraggableElement.prototype.onDragOver = function ( e ) {
4841 e.preventDefault();
4842 };
4843
4844 /**
4845 * Set item index.
4846 * Store it in the DOM so we can access from the widget drag event
4847 *
4848 * @private
4849 * @param {number} Item index
4850 */
4851 OO.ui.mixin.DraggableElement.prototype.setIndex = function ( index ) {
4852 if ( this.index !== index ) {
4853 this.index = index;
4854 this.$element.data( 'index', index );
4855 }
4856 };
4857
4858 /**
4859 * Get item index
4860 *
4861 * @private
4862 * @return {number} Item index
4863 */
4864 OO.ui.mixin.DraggableElement.prototype.getIndex = function () {
4865 return this.index;
4866 };
4867
4868 /**
4869 * DraggableGroupElement is a mixin class used to create a group element to
4870 * contain draggable elements, which are items that can be clicked and dragged by a mouse.
4871 * The class is used with OO.ui.mixin.DraggableElement.
4872 *
4873 * @abstract
4874 * @class
4875 * @mixins OO.ui.mixin.GroupElement
4876 *
4877 * @constructor
4878 * @param {Object} [config] Configuration options
4879 * @cfg {string} [orientation] Item orientation: 'horizontal' or 'vertical'. The orientation
4880 * should match the layout of the items. Items displayed in a single row
4881 * or in several rows should use horizontal orientation. The vertical orientation should only be
4882 * used when the items are displayed in a single column. Defaults to 'vertical'
4883 */
4884 OO.ui.mixin.DraggableGroupElement = function OoUiMixinDraggableGroupElement( config ) {
4885 // Configuration initialization
4886 config = config || {};
4887
4888 // Parent constructor
4889 OO.ui.mixin.GroupElement.call( this, config );
4890
4891 // Properties
4892 this.orientation = config.orientation || 'vertical';
4893 this.dragItem = null;
4894 this.itemDragOver = null;
4895 this.itemKeys = {};
4896 this.sideInsertion = '';
4897
4898 // Events
4899 this.aggregate( {
4900 dragstart: 'itemDragStart',
4901 dragend: 'itemDragEnd',
4902 drop: 'itemDrop'
4903 } );
4904 this.connect( this, {
4905 itemDragStart: 'onItemDragStart',
4906 itemDrop: 'onItemDrop',
4907 itemDragEnd: 'onItemDragEnd'
4908 } );
4909 this.$element.on( {
4910 dragover: $.proxy( this.onDragOver, this ),
4911 dragleave: $.proxy( this.onDragLeave, this )
4912 } );
4913
4914 // Initialize
4915 if ( Array.isArray( config.items ) ) {
4916 this.addItems( config.items );
4917 }
4918 this.$placeholder = $( '<div>' )
4919 .addClass( 'oo-ui-draggableGroupElement-placeholder' );
4920 this.$element
4921 .addClass( 'oo-ui-draggableGroupElement' )
4922 .append( this.$status )
4923 .toggleClass( 'oo-ui-draggableGroupElement-horizontal', this.orientation === 'horizontal' )
4924 .prepend( this.$placeholder );
4925 };
4926
4927 /* Setup */
4928 OO.mixinClass( OO.ui.mixin.DraggableGroupElement, OO.ui.mixin.GroupElement );
4929
4930 /* Events */
4931
4932 /**
4933 * A 'reorder' event is emitted when the order of items in the group changes.
4934 *
4935 * @event reorder
4936 * @param {OO.ui.mixin.DraggableElement} item Reordered item
4937 * @param {number} [newIndex] New index for the item
4938 */
4939
4940 /* Methods */
4941
4942 /**
4943 * Respond to item drag start event
4944 *
4945 * @private
4946 * @param {OO.ui.mixin.DraggableElement} item Dragged item
4947 */
4948 OO.ui.mixin.DraggableGroupElement.prototype.onItemDragStart = function ( item ) {
4949 var i, len;
4950
4951 // Map the index of each object
4952 for ( i = 0, len = this.items.length; i < len; i++ ) {
4953 this.items[ i ].setIndex( i );
4954 }
4955
4956 if ( this.orientation === 'horizontal' ) {
4957 // Set the height of the indicator
4958 this.$placeholder.css( {
4959 height: item.$element.outerHeight(),
4960 width: 2
4961 } );
4962 } else {
4963 // Set the width of the indicator
4964 this.$placeholder.css( {
4965 height: 2,
4966 width: item.$element.outerWidth()
4967 } );
4968 }
4969 this.setDragItem( item );
4970 };
4971
4972 /**
4973 * Respond to item drag end event
4974 *
4975 * @private
4976 */
4977 OO.ui.mixin.DraggableGroupElement.prototype.onItemDragEnd = function () {
4978 this.unsetDragItem();
4979 return false;
4980 };
4981
4982 /**
4983 * Handle drop event and switch the order of the items accordingly
4984 *
4985 * @private
4986 * @param {OO.ui.mixin.DraggableElement} item Dropped item
4987 * @fires reorder
4988 */
4989 OO.ui.mixin.DraggableGroupElement.prototype.onItemDrop = function ( item ) {
4990 var toIndex = item.getIndex();
4991 // Check if the dropped item is from the current group
4992 // TODO: Figure out a way to configure a list of legally droppable
4993 // elements even if they are not yet in the list
4994 if ( this.getDragItem() ) {
4995 // If the insertion point is 'after', the insertion index
4996 // is shifted to the right (or to the left in RTL, hence 'after')
4997 if ( this.sideInsertion === 'after' ) {
4998 toIndex++;
4999 }
5000 // Emit change event
5001 this.emit( 'reorder', this.getDragItem(), toIndex );
5002 }
5003 this.unsetDragItem();
5004 // Return false to prevent propogation
5005 return false;
5006 };
5007
5008 /**
5009 * Handle dragleave event.
5010 *
5011 * @private
5012 */
5013 OO.ui.mixin.DraggableGroupElement.prototype.onDragLeave = function () {
5014 // This means the item was dragged outside the widget
5015 this.$placeholder
5016 .css( 'left', 0 )
5017 .addClass( 'oo-ui-element-hidden' );
5018 };
5019
5020 /**
5021 * Respond to dragover event
5022 *
5023 * @private
5024 * @param {jQuery.Event} event Event details
5025 */
5026 OO.ui.mixin.DraggableGroupElement.prototype.onDragOver = function ( e ) {
5027 var dragOverObj, $optionWidget, itemOffset, itemMidpoint, itemBoundingRect,
5028 itemSize, cssOutput, dragPosition, itemIndex, itemPosition,
5029 clientX = e.originalEvent.clientX,
5030 clientY = e.originalEvent.clientY;
5031
5032 // Get the OptionWidget item we are dragging over
5033 dragOverObj = this.getElementDocument().elementFromPoint( clientX, clientY );
5034 $optionWidget = $( dragOverObj ).closest( '.oo-ui-draggableElement' );
5035 if ( $optionWidget[ 0 ] ) {
5036 itemOffset = $optionWidget.offset();
5037 itemBoundingRect = $optionWidget[ 0 ].getBoundingClientRect();
5038 itemPosition = $optionWidget.position();
5039 itemIndex = $optionWidget.data( 'index' );
5040 }
5041
5042 if (
5043 itemOffset &&
5044 this.isDragging() &&
5045 itemIndex !== this.getDragItem().getIndex()
5046 ) {
5047 if ( this.orientation === 'horizontal' ) {
5048 // Calculate where the mouse is relative to the item width
5049 itemSize = itemBoundingRect.width;
5050 itemMidpoint = itemBoundingRect.left + itemSize / 2;
5051 dragPosition = clientX;
5052 // Which side of the item we hover over will dictate
5053 // where the placeholder will appear, on the left or
5054 // on the right
5055 cssOutput = {
5056 left: dragPosition < itemMidpoint ? itemPosition.left : itemPosition.left + itemSize,
5057 top: itemPosition.top
5058 };
5059 } else {
5060 // Calculate where the mouse is relative to the item height
5061 itemSize = itemBoundingRect.height;
5062 itemMidpoint = itemBoundingRect.top + itemSize / 2;
5063 dragPosition = clientY;
5064 // Which side of the item we hover over will dictate
5065 // where the placeholder will appear, on the top or
5066 // on the bottom
5067 cssOutput = {
5068 top: dragPosition < itemMidpoint ? itemPosition.top : itemPosition.top + itemSize,
5069 left: itemPosition.left
5070 };
5071 }
5072 // Store whether we are before or after an item to rearrange
5073 // For horizontal layout, we need to account for RTL, as this is flipped
5074 if ( this.orientation === 'horizontal' && this.$element.css( 'direction' ) === 'rtl' ) {
5075 this.sideInsertion = dragPosition < itemMidpoint ? 'after' : 'before';
5076 } else {
5077 this.sideInsertion = dragPosition < itemMidpoint ? 'before' : 'after';
5078 }
5079 // Add drop indicator between objects
5080 this.$placeholder
5081 .css( cssOutput )
5082 .removeClass( 'oo-ui-element-hidden' );
5083 } else {
5084 // This means the item was dragged outside the widget
5085 this.$placeholder
5086 .css( 'left', 0 )
5087 .addClass( 'oo-ui-element-hidden' );
5088 }
5089 // Prevent default
5090 e.preventDefault();
5091 };
5092
5093 /**
5094 * Set a dragged item
5095 *
5096 * @param {OO.ui.mixin.DraggableElement} item Dragged item
5097 */
5098 OO.ui.mixin.DraggableGroupElement.prototype.setDragItem = function ( item ) {
5099 this.dragItem = item;
5100 };
5101
5102 /**
5103 * Unset the current dragged item
5104 */
5105 OO.ui.mixin.DraggableGroupElement.prototype.unsetDragItem = function () {
5106 this.dragItem = null;
5107 this.itemDragOver = null;
5108 this.$placeholder.addClass( 'oo-ui-element-hidden' );
5109 this.sideInsertion = '';
5110 };
5111
5112 /**
5113 * Get the item that is currently being dragged.
5114 *
5115 * @return {OO.ui.mixin.DraggableElement|null} The currently dragged item, or `null` if no item is being dragged
5116 */
5117 OO.ui.mixin.DraggableGroupElement.prototype.getDragItem = function () {
5118 return this.dragItem;
5119 };
5120
5121 /**
5122 * Check if an item in the group is currently being dragged.
5123 *
5124 * @return {Boolean} Item is being dragged
5125 */
5126 OO.ui.mixin.DraggableGroupElement.prototype.isDragging = function () {
5127 return this.getDragItem() !== null;
5128 };
5129
5130 /**
5131 * IconElement is often mixed into other classes to generate an icon.
5132 * Icons are graphics, about the size of normal text. They are used to aid the user
5133 * in locating a control or to convey information in a space-efficient way. See the
5134 * [OOjs UI documentation on MediaWiki] [1] for a list of icons
5135 * included in the library.
5136 *
5137 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
5138 *
5139 * @abstract
5140 * @class
5141 *
5142 * @constructor
5143 * @param {Object} [config] Configuration options
5144 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
5145 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
5146 * the icon element be set to an existing icon instead of the one generated by this class, set a
5147 * value using a jQuery selection. For example:
5148 *
5149 * // Use a <div> tag instead of a <span>
5150 * $icon: $("<div>")
5151 * // Use an existing icon element instead of the one generated by the class
5152 * $icon: this.$element
5153 * // Use an icon element from a child widget
5154 * $icon: this.childwidget.$element
5155 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
5156 * symbolic names. A map is used for i18n purposes and contains a `default` icon
5157 * name and additional names keyed by language code. The `default` name is used when no icon is keyed
5158 * by the user's language.
5159 *
5160 * Example of an i18n map:
5161 *
5162 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
5163 * See the [OOjs UI documentation on MediaWiki] [2] for a list of icons included in the library.
5164 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
5165 * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
5166 * text. The icon title is displayed when users move the mouse over the icon.
5167 */
5168 OO.ui.mixin.IconElement = function OoUiMixinIconElement( config ) {
5169 // Configuration initialization
5170 config = config || {};
5171
5172 // Properties
5173 this.$icon = null;
5174 this.icon = null;
5175 this.iconTitle = null;
5176
5177 // Initialization
5178 this.setIcon( config.icon || this.constructor.static.icon );
5179 this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle );
5180 this.setIconElement( config.$icon || $( '<span>' ) );
5181 };
5182
5183 /* Setup */
5184
5185 OO.initClass( OO.ui.mixin.IconElement );
5186
5187 /* Static Properties */
5188
5189 /**
5190 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
5191 * for i18n purposes and contains a `default` icon name and additional names keyed by
5192 * language code. The `default` name is used when no icon is keyed by the user's language.
5193 *
5194 * Example of an i18n map:
5195 *
5196 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
5197 *
5198 * Note: the static property will be overridden if the #icon configuration is used.
5199 *
5200 * @static
5201 * @inheritable
5202 * @property {Object|string}
5203 */
5204 OO.ui.mixin.IconElement.static.icon = null;
5205
5206 /**
5207 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
5208 * function that returns title text, or `null` for no title.
5209 *
5210 * The static property will be overridden if the #iconTitle configuration is used.
5211 *
5212 * @static
5213 * @inheritable
5214 * @property {string|Function|null}
5215 */
5216 OO.ui.mixin.IconElement.static.iconTitle = null;
5217
5218 /* Methods */
5219
5220 /**
5221 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
5222 * applies to the specified icon element instead of the one created by the class. If an icon
5223 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
5224 * and mixin methods will no longer affect the element.
5225 *
5226 * @param {jQuery} $icon Element to use as icon
5227 */
5228 OO.ui.mixin.IconElement.prototype.setIconElement = function ( $icon ) {
5229 if ( this.$icon ) {
5230 this.$icon
5231 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon )
5232 .removeAttr( 'title' );
5233 }
5234
5235 this.$icon = $icon
5236 .addClass( 'oo-ui-iconElement-icon' )
5237 .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon );
5238 if ( this.iconTitle !== null ) {
5239 this.$icon.attr( 'title', this.iconTitle );
5240 }
5241 };
5242
5243 /**
5244 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
5245 * The icon parameter can also be set to a map of icon names. See the #icon config setting
5246 * for an example.
5247 *
5248 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
5249 * by language code, or `null` to remove the icon.
5250 * @chainable
5251 */
5252 OO.ui.mixin.IconElement.prototype.setIcon = function ( icon ) {
5253 icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
5254 icon = typeof icon === 'string' && icon.trim().length ? icon.trim() : null;
5255
5256 if ( this.icon !== icon ) {
5257 if ( this.$icon ) {
5258 if ( this.icon !== null ) {
5259 this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
5260 }
5261 if ( icon !== null ) {
5262 this.$icon.addClass( 'oo-ui-icon-' + icon );
5263 }
5264 }
5265 this.icon = icon;
5266 }
5267
5268 this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon );
5269 this.updateThemeClasses();
5270
5271 return this;
5272 };
5273
5274 /**
5275 * Set the icon title. Use `null` to remove the title.
5276 *
5277 * @param {string|Function|null} iconTitle A text string used as the icon title,
5278 * a function that returns title text, or `null` for no title.
5279 * @chainable
5280 */
5281 OO.ui.mixin.IconElement.prototype.setIconTitle = function ( iconTitle ) {
5282 iconTitle = typeof iconTitle === 'function' ||
5283 ( typeof iconTitle === 'string' && iconTitle.length ) ?
5284 OO.ui.resolveMsg( iconTitle ) : null;
5285
5286 if ( this.iconTitle !== iconTitle ) {
5287 this.iconTitle = iconTitle;
5288 if ( this.$icon ) {
5289 if ( this.iconTitle !== null ) {
5290 this.$icon.attr( 'title', iconTitle );
5291 } else {
5292 this.$icon.removeAttr( 'title' );
5293 }
5294 }
5295 }
5296
5297 return this;
5298 };
5299
5300 /**
5301 * Get the symbolic name of the icon.
5302 *
5303 * @return {string} Icon name
5304 */
5305 OO.ui.mixin.IconElement.prototype.getIcon = function () {
5306 return this.icon;
5307 };
5308
5309 /**
5310 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
5311 *
5312 * @return {string} Icon title text
5313 */
5314 OO.ui.mixin.IconElement.prototype.getIconTitle = function () {
5315 return this.iconTitle;
5316 };
5317
5318 /**
5319 * IndicatorElement is often mixed into other classes to generate an indicator.
5320 * Indicators are small graphics that are generally used in two ways:
5321 *
5322 * - To draw attention to the status of an item. For example, an indicator might be
5323 * used to show that an item in a list has errors that need to be resolved.
5324 * - To clarify the function of a control that acts in an exceptional way (a button
5325 * that opens a menu instead of performing an action directly, for example).
5326 *
5327 * For a list of indicators included in the library, please see the
5328 * [OOjs UI documentation on MediaWiki] [1].
5329 *
5330 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
5331 *
5332 * @abstract
5333 * @class
5334 *
5335 * @constructor
5336 * @param {Object} [config] Configuration options
5337 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
5338 * configuration is omitted, the indicator element will use a generated `<span>`.
5339 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
5340 * See the [OOjs UI documentation on MediaWiki][2] for a list of indicators included
5341 * in the library.
5342 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
5343 * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
5344 * or a function that returns title text. The indicator title is displayed when users move
5345 * the mouse over the indicator.
5346 */
5347 OO.ui.mixin.IndicatorElement = function OoUiMixinIndicatorElement( config ) {
5348 // Configuration initialization
5349 config = config || {};
5350
5351 // Properties
5352 this.$indicator = null;
5353 this.indicator = null;
5354 this.indicatorTitle = null;
5355
5356 // Initialization
5357 this.setIndicator( config.indicator || this.constructor.static.indicator );
5358 this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle );
5359 this.setIndicatorElement( config.$indicator || $( '<span>' ) );
5360 };
5361
5362 /* Setup */
5363
5364 OO.initClass( OO.ui.mixin.IndicatorElement );
5365
5366 /* Static Properties */
5367
5368 /**
5369 * Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
5370 * The static property will be overridden if the #indicator configuration is used.
5371 *
5372 * @static
5373 * @inheritable
5374 * @property {string|null}
5375 */
5376 OO.ui.mixin.IndicatorElement.static.indicator = null;
5377
5378 /**
5379 * A text string used as the indicator title, a function that returns title text, or `null`
5380 * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
5381 *
5382 * @static
5383 * @inheritable
5384 * @property {string|Function|null}
5385 */
5386 OO.ui.mixin.IndicatorElement.static.indicatorTitle = null;
5387
5388 /* Methods */
5389
5390 /**
5391 * Set the indicator element.
5392 *
5393 * If an element is already set, it will be cleaned up before setting up the new element.
5394 *
5395 * @param {jQuery} $indicator Element to use as indicator
5396 */
5397 OO.ui.mixin.IndicatorElement.prototype.setIndicatorElement = function ( $indicator ) {
5398 if ( this.$indicator ) {
5399 this.$indicator
5400 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator )
5401 .removeAttr( 'title' );
5402 }
5403
5404 this.$indicator = $indicator
5405 .addClass( 'oo-ui-indicatorElement-indicator' )
5406 .toggleClass( 'oo-ui-indicator-' + this.indicator, !!this.indicator );
5407 if ( this.indicatorTitle !== null ) {
5408 this.$indicator.attr( 'title', this.indicatorTitle );
5409 }
5410 };
5411
5412 /**
5413 * Set the indicator by its symbolic name: ‘alert’, ‘down’, ‘next’, ‘previous’, ‘required’, ‘up’. Use `null` to remove the indicator.
5414 *
5415 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
5416 * @chainable
5417 */
5418 OO.ui.mixin.IndicatorElement.prototype.setIndicator = function ( indicator ) {
5419 indicator = typeof indicator === 'string' && indicator.length ? indicator.trim() : null;
5420
5421 if ( this.indicator !== indicator ) {
5422 if ( this.$indicator ) {
5423 if ( this.indicator !== null ) {
5424 this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
5425 }
5426 if ( indicator !== null ) {
5427 this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
5428 }
5429 }
5430 this.indicator = indicator;
5431 }
5432
5433 this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator );
5434 this.updateThemeClasses();
5435
5436 return this;
5437 };
5438
5439 /**
5440 * Set the indicator title.
5441 *
5442 * The title is displayed when a user moves the mouse over the indicator.
5443 *
5444 * @param {string|Function|null} indicator Indicator title text, a function that returns text, or
5445 * `null` for no indicator title
5446 * @chainable
5447 */
5448 OO.ui.mixin.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
5449 indicatorTitle = typeof indicatorTitle === 'function' ||
5450 ( typeof indicatorTitle === 'string' && indicatorTitle.length ) ?
5451 OO.ui.resolveMsg( indicatorTitle ) : null;
5452
5453 if ( this.indicatorTitle !== indicatorTitle ) {
5454 this.indicatorTitle = indicatorTitle;
5455 if ( this.$indicator ) {
5456 if ( this.indicatorTitle !== null ) {
5457 this.$indicator.attr( 'title', indicatorTitle );
5458 } else {
5459 this.$indicator.removeAttr( 'title' );
5460 }
5461 }
5462 }
5463
5464 return this;
5465 };
5466
5467 /**
5468 * Get the symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
5469 *
5470 * @return {string} Symbolic name of indicator
5471 */
5472 OO.ui.mixin.IndicatorElement.prototype.getIndicator = function () {
5473 return this.indicator;
5474 };
5475
5476 /**
5477 * Get the indicator title.
5478 *
5479 * The title is displayed when a user moves the mouse over the indicator.
5480 *
5481 * @return {string} Indicator title text
5482 */
5483 OO.ui.mixin.IndicatorElement.prototype.getIndicatorTitle = function () {
5484 return this.indicatorTitle;
5485 };
5486
5487 /**
5488 * LabelElement is often mixed into other classes to generate a label, which
5489 * helps identify the function of an interface element.
5490 * See the [OOjs UI documentation on MediaWiki] [1] for more information.
5491 *
5492 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
5493 *
5494 * @abstract
5495 * @class
5496 *
5497 * @constructor
5498 * @param {Object} [config] Configuration options
5499 * @cfg {jQuery} [$label] The label element created by the class. If this
5500 * configuration is omitted, the label element will use a generated `<span>`.
5501 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified
5502 * as a plaintext string, a jQuery selection of elements, or a function that will produce a string
5503 * in the future. See the [OOjs UI documentation on MediaWiki] [2] for examples.
5504 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
5505 * @cfg {boolean} [autoFitLabel=true] Fit the label to the width of the parent element.
5506 * The label will be truncated to fit if necessary.
5507 */
5508 OO.ui.mixin.LabelElement = function OoUiMixinLabelElement( config ) {
5509 // Configuration initialization
5510 config = config || {};
5511
5512 // Properties
5513 this.$label = null;
5514 this.label = null;
5515 this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel;
5516
5517 // Initialization
5518 this.setLabel( config.label || this.constructor.static.label );
5519 this.setLabelElement( config.$label || $( '<span>' ) );
5520 };
5521
5522 /* Setup */
5523
5524 OO.initClass( OO.ui.mixin.LabelElement );
5525
5526 /* Events */
5527
5528 /**
5529 * @event labelChange
5530 * @param {string} value
5531 */
5532
5533 /* Static Properties */
5534
5535 /**
5536 * The label text. The label can be specified as a plaintext string, a function that will
5537 * produce a string in the future, or `null` for no label. The static value will
5538 * be overridden if a label is specified with the #label config option.
5539 *
5540 * @static
5541 * @inheritable
5542 * @property {string|Function|null}
5543 */
5544 OO.ui.mixin.LabelElement.static.label = null;
5545
5546 /* Methods */
5547
5548 /**
5549 * Set the label element.
5550 *
5551 * If an element is already set, it will be cleaned up before setting up the new element.
5552 *
5553 * @param {jQuery} $label Element to use as label
5554 */
5555 OO.ui.mixin.LabelElement.prototype.setLabelElement = function ( $label ) {
5556 if ( this.$label ) {
5557 this.$label.removeClass( 'oo-ui-labelElement-label' ).empty();
5558 }
5559
5560 this.$label = $label.addClass( 'oo-ui-labelElement-label' );
5561 this.setLabelContent( this.label );
5562 };
5563
5564 /**
5565 * Set the label.
5566 *
5567 * An empty string will result in the label being hidden. A string containing only whitespace will
5568 * be converted to a single `&nbsp;`.
5569 *
5570 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
5571 * text; or null for no label
5572 * @chainable
5573 */
5574 OO.ui.mixin.LabelElement.prototype.setLabel = function ( label ) {
5575 label = typeof label === 'function' ? OO.ui.resolveMsg( label ) : label;
5576 label = ( ( typeof label === 'string' && label.length ) || label instanceof jQuery || label instanceof OO.ui.HtmlSnippet ) ? label : null;
5577
5578 this.$element.toggleClass( 'oo-ui-labelElement', !!label );
5579
5580 if ( this.label !== label ) {
5581 if ( this.$label ) {
5582 this.setLabelContent( label );
5583 }
5584 this.label = label;
5585 this.emit( 'labelChange' );
5586 }
5587
5588 return this;
5589 };
5590
5591 /**
5592 * Get the label.
5593 *
5594 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
5595 * text; or null for no label
5596 */
5597 OO.ui.mixin.LabelElement.prototype.getLabel = function () {
5598 return this.label;
5599 };
5600
5601 /**
5602 * Fit the label.
5603 *
5604 * @chainable
5605 */
5606 OO.ui.mixin.LabelElement.prototype.fitLabel = function () {
5607 if ( this.$label && this.$label.autoEllipsis && this.autoFitLabel ) {
5608 this.$label.autoEllipsis( { hasSpan: false, tooltip: true } );
5609 }
5610
5611 return this;
5612 };
5613
5614 /**
5615 * Set the content of the label.
5616 *
5617 * Do not call this method until after the label element has been set by #setLabelElement.
5618 *
5619 * @private
5620 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
5621 * text; or null for no label
5622 */
5623 OO.ui.mixin.LabelElement.prototype.setLabelContent = function ( label ) {
5624 if ( typeof label === 'string' ) {
5625 if ( label.match( /^\s*$/ ) ) {
5626 // Convert whitespace only string to a single non-breaking space
5627 this.$label.html( '&nbsp;' );
5628 } else {
5629 this.$label.text( label );
5630 }
5631 } else if ( label instanceof OO.ui.HtmlSnippet ) {
5632 this.$label.html( label.toString() );
5633 } else if ( label instanceof jQuery ) {
5634 this.$label.empty().append( label );
5635 } else {
5636 this.$label.empty();
5637 }
5638 };
5639
5640 /**
5641 * LookupElement is a mixin that creates a {@link OO.ui.TextInputMenuSelectWidget menu} of suggested values for
5642 * a {@link OO.ui.TextInputWidget text input widget}. Suggested values are based on the characters the user types
5643 * into the text input field and, in general, the menu is only displayed when the user types. If a suggested value is chosen
5644 * from the lookup menu, that value becomes the value of the input field.
5645 *
5646 * Note that a new menu of suggested items is displayed when a value is chosen from the lookup menu. If this is
5647 * not the desired behavior, disable lookup menus with the #setLookupsDisabled method, then set the value, then
5648 * re-enable lookups.
5649 *
5650 * See the [OOjs UI demos][1] for an example.
5651 *
5652 * [1]: https://tools.wmflabs.org/oojs-ui/oojs-ui/demos/index.html#widgets-apex-vector-ltr
5653 *
5654 * @class
5655 * @abstract
5656 *
5657 * @constructor
5658 * @param {Object} [config] Configuration options
5659 * @cfg {jQuery} [$overlay] Overlay for the lookup menu; defaults to relative positioning
5660 * @cfg {jQuery} [$container=this.$element] The container element. The lookup menu is rendered beneath the specified element.
5661 * @cfg {boolean} [allowSuggestionsWhenEmpty=false] Request and display a lookup menu when the text input is empty.
5662 * By default, the lookup menu is not generated and displayed until the user begins to type.
5663 */
5664 OO.ui.mixin.LookupElement = function OoUiMixinLookupElement( config ) {
5665 // Configuration initialization
5666 config = config || {};
5667
5668 // Properties
5669 this.$overlay = config.$overlay || this.$element;
5670 this.lookupMenu = new OO.ui.TextInputMenuSelectWidget( this, {
5671 widget: this,
5672 input: this,
5673 $container: config.$container
5674 } );
5675
5676 this.allowSuggestionsWhenEmpty = config.allowSuggestionsWhenEmpty || false;
5677
5678 this.lookupCache = {};
5679 this.lookupQuery = null;
5680 this.lookupRequest = null;
5681 this.lookupsDisabled = false;
5682 this.lookupInputFocused = false;
5683
5684 // Events
5685 this.$input.on( {
5686 focus: this.onLookupInputFocus.bind( this ),
5687 blur: this.onLookupInputBlur.bind( this ),
5688 mousedown: this.onLookupInputMouseDown.bind( this )
5689 } );
5690 this.connect( this, { change: 'onLookupInputChange' } );
5691 this.lookupMenu.connect( this, {
5692 toggle: 'onLookupMenuToggle',
5693 choose: 'onLookupMenuItemChoose'
5694 } );
5695
5696 // Initialization
5697 this.$element.addClass( 'oo-ui-lookupElement' );
5698 this.lookupMenu.$element.addClass( 'oo-ui-lookupElement-menu' );
5699 this.$overlay.append( this.lookupMenu.$element );
5700 };
5701
5702 /* Methods */
5703
5704 /**
5705 * Handle input focus event.
5706 *
5707 * @protected
5708 * @param {jQuery.Event} e Input focus event
5709 */
5710 OO.ui.mixin.LookupElement.prototype.onLookupInputFocus = function () {
5711 this.lookupInputFocused = true;
5712 this.populateLookupMenu();
5713 };
5714
5715 /**
5716 * Handle input blur event.
5717 *
5718 * @protected
5719 * @param {jQuery.Event} e Input blur event
5720 */
5721 OO.ui.mixin.LookupElement.prototype.onLookupInputBlur = function () {
5722 this.closeLookupMenu();
5723 this.lookupInputFocused = false;
5724 };
5725
5726 /**
5727 * Handle input mouse down event.
5728 *
5729 * @protected
5730 * @param {jQuery.Event} e Input mouse down event
5731 */
5732 OO.ui.mixin.LookupElement.prototype.onLookupInputMouseDown = function () {
5733 // Only open the menu if the input was already focused.
5734 // This way we allow the user to open the menu again after closing it with Esc
5735 // by clicking in the input. Opening (and populating) the menu when initially
5736 // clicking into the input is handled by the focus handler.
5737 if ( this.lookupInputFocused && !this.lookupMenu.isVisible() ) {
5738 this.populateLookupMenu();
5739 }
5740 };
5741
5742 /**
5743 * Handle input change event.
5744 *
5745 * @protected
5746 * @param {string} value New input value
5747 */
5748 OO.ui.mixin.LookupElement.prototype.onLookupInputChange = function () {
5749 if ( this.lookupInputFocused ) {
5750 this.populateLookupMenu();
5751 }
5752 };
5753
5754 /**
5755 * Handle the lookup menu being shown/hidden.
5756 *
5757 * @protected
5758 * @param {boolean} visible Whether the lookup menu is now visible.
5759 */
5760 OO.ui.mixin.LookupElement.prototype.onLookupMenuToggle = function ( visible ) {
5761 if ( !visible ) {
5762 // When the menu is hidden, abort any active request and clear the menu.
5763 // This has to be done here in addition to closeLookupMenu(), because
5764 // MenuSelectWidget will close itself when the user presses Esc.
5765 this.abortLookupRequest();
5766 this.lookupMenu.clearItems();
5767 }
5768 };
5769
5770 /**
5771 * Handle menu item 'choose' event, updating the text input value to the value of the clicked item.
5772 *
5773 * @protected
5774 * @param {OO.ui.MenuOptionWidget} item Selected item
5775 */
5776 OO.ui.mixin.LookupElement.prototype.onLookupMenuItemChoose = function ( item ) {
5777 this.setValue( item.getData() );
5778 };
5779
5780 /**
5781 * Get lookup menu.
5782 *
5783 * @private
5784 * @return {OO.ui.TextInputMenuSelectWidget}
5785 */
5786 OO.ui.mixin.LookupElement.prototype.getLookupMenu = function () {
5787 return this.lookupMenu;
5788 };
5789
5790 /**
5791 * Disable or re-enable lookups.
5792 *
5793 * When lookups are disabled, calls to #populateLookupMenu will be ignored.
5794 *
5795 * @param {boolean} disabled Disable lookups
5796 */
5797 OO.ui.mixin.LookupElement.prototype.setLookupsDisabled = function ( disabled ) {
5798 this.lookupsDisabled = !!disabled;
5799 };
5800
5801 /**
5802 * Open the menu. If there are no entries in the menu, this does nothing.
5803 *
5804 * @private
5805 * @chainable
5806 */
5807 OO.ui.mixin.LookupElement.prototype.openLookupMenu = function () {
5808 if ( !this.lookupMenu.isEmpty() ) {
5809 this.lookupMenu.toggle( true );
5810 }
5811 return this;
5812 };
5813
5814 /**
5815 * Close the menu, empty it, and abort any pending request.
5816 *
5817 * @private
5818 * @chainable
5819 */
5820 OO.ui.mixin.LookupElement.prototype.closeLookupMenu = function () {
5821 this.lookupMenu.toggle( false );
5822 this.abortLookupRequest();
5823 this.lookupMenu.clearItems();
5824 return this;
5825 };
5826
5827 /**
5828 * Request menu items based on the input's current value, and when they arrive,
5829 * populate the menu with these items and show the menu.
5830 *
5831 * If lookups have been disabled with #setLookupsDisabled, this function does nothing.
5832 *
5833 * @private
5834 * @chainable
5835 */
5836 OO.ui.mixin.LookupElement.prototype.populateLookupMenu = function () {
5837 var widget = this,
5838 value = this.getValue();
5839
5840 if ( this.lookupsDisabled ) {
5841 return;
5842 }
5843
5844 // If the input is empty, clear the menu, unless suggestions when empty are allowed.
5845 if ( !this.allowSuggestionsWhenEmpty && value === '' ) {
5846 this.closeLookupMenu();
5847 // Skip population if there is already a request pending for the current value
5848 } else if ( value !== this.lookupQuery ) {
5849 this.getLookupMenuItems()
5850 .done( function ( items ) {
5851 widget.lookupMenu.clearItems();
5852 if ( items.length ) {
5853 widget.lookupMenu
5854 .addItems( items )
5855 .toggle( true );
5856 widget.initializeLookupMenuSelection();
5857 } else {
5858 widget.lookupMenu.toggle( false );
5859 }
5860 } )
5861 .fail( function () {
5862 widget.lookupMenu.clearItems();
5863 } );
5864 }
5865
5866 return this;
5867 };
5868
5869 /**
5870 * Highlight the first selectable item in the menu.
5871 *
5872 * @private
5873 * @chainable
5874 */
5875 OO.ui.mixin.LookupElement.prototype.initializeLookupMenuSelection = function () {
5876 if ( !this.lookupMenu.getSelectedItem() ) {
5877 this.lookupMenu.highlightItem( this.lookupMenu.getFirstSelectableItem() );
5878 }
5879 };
5880
5881 /**
5882 * Get lookup menu items for the current query.
5883 *
5884 * @private
5885 * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument of
5886 * the done event. If the request was aborted to make way for a subsequent request, this promise
5887 * will not be rejected: it will remain pending forever.
5888 */
5889 OO.ui.mixin.LookupElement.prototype.getLookupMenuItems = function () {
5890 var widget = this,
5891 value = this.getValue(),
5892 deferred = $.Deferred(),
5893 ourRequest;
5894
5895 this.abortLookupRequest();
5896 if ( Object.prototype.hasOwnProperty.call( this.lookupCache, value ) ) {
5897 deferred.resolve( this.getLookupMenuOptionsFromData( this.lookupCache[ value ] ) );
5898 } else {
5899 this.pushPending();
5900 this.lookupQuery = value;
5901 ourRequest = this.lookupRequest = this.getLookupRequest();
5902 ourRequest
5903 .always( function () {
5904 // We need to pop pending even if this is an old request, otherwise
5905 // the widget will remain pending forever.
5906 // TODO: this assumes that an aborted request will fail or succeed soon after
5907 // being aborted, or at least eventually. It would be nice if we could popPending()
5908 // at abort time, but only if we knew that we hadn't already called popPending()
5909 // for that request.
5910 widget.popPending();
5911 } )
5912 .done( function ( response ) {
5913 // If this is an old request (and aborting it somehow caused it to still succeed),
5914 // ignore its success completely
5915 if ( ourRequest === widget.lookupRequest ) {
5916 widget.lookupQuery = null;
5917 widget.lookupRequest = null;
5918 widget.lookupCache[ value ] = widget.getLookupCacheDataFromResponse( response );
5919 deferred.resolve( widget.getLookupMenuOptionsFromData( widget.lookupCache[ value ] ) );
5920 }
5921 } )
5922 .fail( function () {
5923 // If this is an old request (or a request failing because it's being aborted),
5924 // ignore its failure completely
5925 if ( ourRequest === widget.lookupRequest ) {
5926 widget.lookupQuery = null;
5927 widget.lookupRequest = null;
5928 deferred.reject();
5929 }
5930 } );
5931 }
5932 return deferred.promise();
5933 };
5934
5935 /**
5936 * Abort the currently pending lookup request, if any.
5937 *
5938 * @private
5939 */
5940 OO.ui.mixin.LookupElement.prototype.abortLookupRequest = function () {
5941 var oldRequest = this.lookupRequest;
5942 if ( oldRequest ) {
5943 // First unset this.lookupRequest to the fail handler will notice
5944 // that the request is no longer current
5945 this.lookupRequest = null;
5946 this.lookupQuery = null;
5947 oldRequest.abort();
5948 }
5949 };
5950
5951 /**
5952 * Get a new request object of the current lookup query value.
5953 *
5954 * @protected
5955 * @abstract
5956 * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
5957 */
5958 OO.ui.mixin.LookupElement.prototype.getLookupRequest = function () {
5959 // Stub, implemented in subclass
5960 return null;
5961 };
5962
5963 /**
5964 * Pre-process data returned by the request from #getLookupRequest.
5965 *
5966 * The return value of this function will be cached, and any further queries for the given value
5967 * will use the cache rather than doing API requests.
5968 *
5969 * @protected
5970 * @abstract
5971 * @param {Mixed} response Response from server
5972 * @return {Mixed} Cached result data
5973 */
5974 OO.ui.mixin.LookupElement.prototype.getLookupCacheDataFromResponse = function () {
5975 // Stub, implemented in subclass
5976 return [];
5977 };
5978
5979 /**
5980 * Get a list of menu option widgets from the (possibly cached) data returned by
5981 * #getLookupCacheDataFromResponse.
5982 *
5983 * @protected
5984 * @abstract
5985 * @param {Mixed} data Cached result data, usually an array
5986 * @return {OO.ui.MenuOptionWidget[]} Menu items
5987 */
5988 OO.ui.mixin.LookupElement.prototype.getLookupMenuOptionsFromData = function () {
5989 // Stub, implemented in subclass
5990 return [];
5991 };
5992
5993 /**
5994 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
5995 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
5996 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
5997 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
5998 *
5999 * @abstract
6000 * @class
6001 *
6002 * @constructor
6003 * @param {Object} [config] Configuration options
6004 * @cfg {Object} [popup] Configuration to pass to popup
6005 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
6006 */
6007 OO.ui.mixin.PopupElement = function OoUiMixinPopupElement( config ) {
6008 // Configuration initialization
6009 config = config || {};
6010
6011 // Properties
6012 this.popup = new OO.ui.PopupWidget( $.extend(
6013 { autoClose: true },
6014 config.popup,
6015 { $autoCloseIgnore: this.$element }
6016 ) );
6017 };
6018
6019 /* Methods */
6020
6021 /**
6022 * Get popup.
6023 *
6024 * @return {OO.ui.PopupWidget} Popup widget
6025 */
6026 OO.ui.mixin.PopupElement.prototype.getPopup = function () {
6027 return this.popup;
6028 };
6029
6030 /**
6031 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
6032 * additional functionality to an element created by another class. The class provides
6033 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
6034 * which are used to customize the look and feel of a widget to better describe its
6035 * importance and functionality.
6036 *
6037 * The library currently contains the following styling flags for general use:
6038 *
6039 * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
6040 * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
6041 * - **constructive**: Constructive styling is applied to convey that the widget will create something.
6042 *
6043 * The flags affect the appearance of the buttons:
6044 *
6045 * @example
6046 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
6047 * var button1 = new OO.ui.ButtonWidget( {
6048 * label: 'Constructive',
6049 * flags: 'constructive'
6050 * } );
6051 * var button2 = new OO.ui.ButtonWidget( {
6052 * label: 'Destructive',
6053 * flags: 'destructive'
6054 * } );
6055 * var button3 = new OO.ui.ButtonWidget( {
6056 * label: 'Progressive',
6057 * flags: 'progressive'
6058 * } );
6059 * $( 'body' ).append( button1.$element, button2.$element, button3.$element );
6060 *
6061 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
6062 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
6063 *
6064 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
6065 *
6066 * @abstract
6067 * @class
6068 *
6069 * @constructor
6070 * @param {Object} [config] Configuration options
6071 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'constructive' or 'primary') to apply.
6072 * Please see the [OOjs UI documentation on MediaWiki] [2] for more information about available flags.
6073 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
6074 * @cfg {jQuery} [$flagged] The flagged element. By default,
6075 * the flagged functionality is applied to the element created by the class ($element).
6076 * If a different element is specified, the flagged functionality will be applied to it instead.
6077 */
6078 OO.ui.mixin.FlaggedElement = function OoUiMixinFlaggedElement( config ) {
6079 // Configuration initialization
6080 config = config || {};
6081
6082 // Properties
6083 this.flags = {};
6084 this.$flagged = null;
6085
6086 // Initialization
6087 this.setFlags( config.flags );
6088 this.setFlaggedElement( config.$flagged || this.$element );
6089 };
6090
6091 /* Events */
6092
6093 /**
6094 * @event flag
6095 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
6096 * parameter contains the name of each modified flag and indicates whether it was
6097 * added or removed.
6098 *
6099 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
6100 * that the flag was added, `false` that the flag was removed.
6101 */
6102
6103 /* Methods */
6104
6105 /**
6106 * Set the flagged element.
6107 *
6108 * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
6109 * If an element is already set, the method will remove the mixin’s effect on that element.
6110 *
6111 * @param {jQuery} $flagged Element that should be flagged
6112 */
6113 OO.ui.mixin.FlaggedElement.prototype.setFlaggedElement = function ( $flagged ) {
6114 var classNames = Object.keys( this.flags ).map( function ( flag ) {
6115 return 'oo-ui-flaggedElement-' + flag;
6116 } ).join( ' ' );
6117
6118 if ( this.$flagged ) {
6119 this.$flagged.removeClass( classNames );
6120 }
6121
6122 this.$flagged = $flagged.addClass( classNames );
6123 };
6124
6125 /**
6126 * Check if the specified flag is set.
6127 *
6128 * @param {string} flag Name of flag
6129 * @return {boolean} The flag is set
6130 */
6131 OO.ui.mixin.FlaggedElement.prototype.hasFlag = function ( flag ) {
6132 return flag in this.flags;
6133 };
6134
6135 /**
6136 * Get the names of all flags set.
6137 *
6138 * @return {string[]} Flag names
6139 */
6140 OO.ui.mixin.FlaggedElement.prototype.getFlags = function () {
6141 return Object.keys( this.flags );
6142 };
6143
6144 /**
6145 * Clear all flags.
6146 *
6147 * @chainable
6148 * @fires flag
6149 */
6150 OO.ui.mixin.FlaggedElement.prototype.clearFlags = function () {
6151 var flag, className,
6152 changes = {},
6153 remove = [],
6154 classPrefix = 'oo-ui-flaggedElement-';
6155
6156 for ( flag in this.flags ) {
6157 className = classPrefix + flag;
6158 changes[ flag ] = false;
6159 delete this.flags[ flag ];
6160 remove.push( className );
6161 }
6162
6163 if ( this.$flagged ) {
6164 this.$flagged.removeClass( remove.join( ' ' ) );
6165 }
6166
6167 this.updateThemeClasses();
6168 this.emit( 'flag', changes );
6169
6170 return this;
6171 };
6172
6173 /**
6174 * Add one or more flags.
6175 *
6176 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
6177 * or an object keyed by flag name with a boolean value that indicates whether the flag should
6178 * be added (`true`) or removed (`false`).
6179 * @chainable
6180 * @fires flag
6181 */
6182 OO.ui.mixin.FlaggedElement.prototype.setFlags = function ( flags ) {
6183 var i, len, flag, className,
6184 changes = {},
6185 add = [],
6186 remove = [],
6187 classPrefix = 'oo-ui-flaggedElement-';
6188
6189 if ( typeof flags === 'string' ) {
6190 className = classPrefix + flags;
6191 // Set
6192 if ( !this.flags[ flags ] ) {
6193 this.flags[ flags ] = true;
6194 add.push( className );
6195 }
6196 } else if ( Array.isArray( flags ) ) {
6197 for ( i = 0, len = flags.length; i < len; i++ ) {
6198 flag = flags[ i ];
6199 className = classPrefix + flag;
6200 // Set
6201 if ( !this.flags[ flag ] ) {
6202 changes[ flag ] = true;
6203 this.flags[ flag ] = true;
6204 add.push( className );
6205 }
6206 }
6207 } else if ( OO.isPlainObject( flags ) ) {
6208 for ( flag in flags ) {
6209 className = classPrefix + flag;
6210 if ( flags[ flag ] ) {
6211 // Set
6212 if ( !this.flags[ flag ] ) {
6213 changes[ flag ] = true;
6214 this.flags[ flag ] = true;
6215 add.push( className );
6216 }
6217 } else {
6218 // Remove
6219 if ( this.flags[ flag ] ) {
6220 changes[ flag ] = false;
6221 delete this.flags[ flag ];
6222 remove.push( className );
6223 }
6224 }
6225 }
6226 }
6227
6228 if ( this.$flagged ) {
6229 this.$flagged
6230 .addClass( add.join( ' ' ) )
6231 .removeClass( remove.join( ' ' ) );
6232 }
6233
6234 this.updateThemeClasses();
6235 this.emit( 'flag', changes );
6236
6237 return this;
6238 };
6239
6240 /**
6241 * TitledElement is mixed into other classes to provide a `title` attribute.
6242 * Titles are rendered by the browser and are made visible when the user moves
6243 * the mouse over the element. Titles are not visible on touch devices.
6244 *
6245 * @example
6246 * // TitledElement provides a 'title' attribute to the
6247 * // ButtonWidget class
6248 * var button = new OO.ui.ButtonWidget( {
6249 * label: 'Button with Title',
6250 * title: 'I am a button'
6251 * } );
6252 * $( 'body' ).append( button.$element );
6253 *
6254 * @abstract
6255 * @class
6256 *
6257 * @constructor
6258 * @param {Object} [config] Configuration options
6259 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
6260 * If this config is omitted, the title functionality is applied to $element, the
6261 * element created by the class.
6262 * @cfg {string|Function} [title] The title text or a function that returns text. If
6263 * this config is omitted, the value of the {@link #static-title static title} property is used.
6264 */
6265 OO.ui.mixin.TitledElement = function OoUiMixinTitledElement( config ) {
6266 // Configuration initialization
6267 config = config || {};
6268
6269 // Properties
6270 this.$titled = null;
6271 this.title = null;
6272
6273 // Initialization
6274 this.setTitle( config.title || this.constructor.static.title );
6275 this.setTitledElement( config.$titled || this.$element );
6276 };
6277
6278 /* Setup */
6279
6280 OO.initClass( OO.ui.mixin.TitledElement );
6281
6282 /* Static Properties */
6283
6284 /**
6285 * The title text, a function that returns text, or `null` for no title. The value of the static property
6286 * is overridden if the #title config option is used.
6287 *
6288 * @static
6289 * @inheritable
6290 * @property {string|Function|null}
6291 */
6292 OO.ui.mixin.TitledElement.static.title = null;
6293
6294 /* Methods */
6295
6296 /**
6297 * Set the titled element.
6298 *
6299 * This method is used to retarget a titledElement mixin so that its functionality applies to the specified element.
6300 * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
6301 *
6302 * @param {jQuery} $titled Element that should use the 'titled' functionality
6303 */
6304 OO.ui.mixin.TitledElement.prototype.setTitledElement = function ( $titled ) {
6305 if ( this.$titled ) {
6306 this.$titled.removeAttr( 'title' );
6307 }
6308
6309 this.$titled = $titled;
6310 if ( this.title ) {
6311 this.$titled.attr( 'title', this.title );
6312 }
6313 };
6314
6315 /**
6316 * Set title.
6317 *
6318 * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
6319 * @chainable
6320 */
6321 OO.ui.mixin.TitledElement.prototype.setTitle = function ( title ) {
6322 title = typeof title === 'string' ? OO.ui.resolveMsg( title ) : null;
6323
6324 if ( this.title !== title ) {
6325 if ( this.$titled ) {
6326 if ( title !== null ) {
6327 this.$titled.attr( 'title', title );
6328 } else {
6329 this.$titled.removeAttr( 'title' );
6330 }
6331 }
6332 this.title = title;
6333 }
6334
6335 return this;
6336 };
6337
6338 /**
6339 * Get title.
6340 *
6341 * @return {string} Title string
6342 */
6343 OO.ui.mixin.TitledElement.prototype.getTitle = function () {
6344 return this.title;
6345 };
6346
6347 /**
6348 * Element that can be automatically clipped to visible boundaries.
6349 *
6350 * Whenever the element's natural height changes, you have to call
6351 * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
6352 * clipping correctly.
6353 *
6354 * @abstract
6355 * @class
6356 *
6357 * @constructor
6358 * @param {Object} [config] Configuration options
6359 * @cfg {jQuery} [$clippable] Nodes to clip, assigned to #$clippable, omit to use #$element
6360 */
6361 OO.ui.mixin.ClippableElement = function OoUiMixinClippableElement( config ) {
6362 // Configuration initialization
6363 config = config || {};
6364
6365 // Properties
6366 this.$clippable = null;
6367 this.clipping = false;
6368 this.clippedHorizontally = false;
6369 this.clippedVertically = false;
6370 this.$clippableContainer = null;
6371 this.$clippableScroller = null;
6372 this.$clippableWindow = null;
6373 this.idealWidth = null;
6374 this.idealHeight = null;
6375 this.onClippableContainerScrollHandler = this.clip.bind( this );
6376 this.onClippableWindowResizeHandler = this.clip.bind( this );
6377
6378 // Initialization
6379 this.setClippableElement( config.$clippable || this.$element );
6380 };
6381
6382 /* Methods */
6383
6384 /**
6385 * Set clippable element.
6386 *
6387 * If an element is already set, it will be cleaned up before setting up the new element.
6388 *
6389 * @param {jQuery} $clippable Element to make clippable
6390 */
6391 OO.ui.mixin.ClippableElement.prototype.setClippableElement = function ( $clippable ) {
6392 if ( this.$clippable ) {
6393 this.$clippable.removeClass( 'oo-ui-clippableElement-clippable' );
6394 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
6395 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
6396 }
6397
6398 this.$clippable = $clippable.addClass( 'oo-ui-clippableElement-clippable' );
6399 this.clip();
6400 };
6401
6402 /**
6403 * Toggle clipping.
6404 *
6405 * Do not turn clipping on until after the element is attached to the DOM and visible.
6406 *
6407 * @param {boolean} [clipping] Enable clipping, omit to toggle
6408 * @chainable
6409 */
6410 OO.ui.mixin.ClippableElement.prototype.toggleClipping = function ( clipping ) {
6411 clipping = clipping === undefined ? !this.clipping : !!clipping;
6412
6413 if ( this.clipping !== clipping ) {
6414 this.clipping = clipping;
6415 if ( clipping ) {
6416 this.$clippableContainer = $( this.getClosestScrollableElementContainer() );
6417 // If the clippable container is the root, we have to listen to scroll events and check
6418 // jQuery.scrollTop on the window because of browser inconsistencies
6419 this.$clippableScroller = this.$clippableContainer.is( 'html, body' ) ?
6420 $( OO.ui.Element.static.getWindow( this.$clippableContainer ) ) :
6421 this.$clippableContainer;
6422 this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler );
6423 this.$clippableWindow = $( this.getElementWindow() )
6424 .on( 'resize', this.onClippableWindowResizeHandler );
6425 // Initial clip after visible
6426 this.clip();
6427 } else {
6428 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
6429 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
6430
6431 this.$clippableContainer = null;
6432 this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler );
6433 this.$clippableScroller = null;
6434 this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
6435 this.$clippableWindow = null;
6436 }
6437 }
6438
6439 return this;
6440 };
6441
6442 /**
6443 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
6444 *
6445 * @return {boolean} Element will be clipped to the visible area
6446 */
6447 OO.ui.mixin.ClippableElement.prototype.isClipping = function () {
6448 return this.clipping;
6449 };
6450
6451 /**
6452 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
6453 *
6454 * @return {boolean} Part of the element is being clipped
6455 */
6456 OO.ui.mixin.ClippableElement.prototype.isClipped = function () {
6457 return this.clippedHorizontally || this.clippedVertically;
6458 };
6459
6460 /**
6461 * Check if the right of the element is being clipped by the nearest scrollable container.
6462 *
6463 * @return {boolean} Part of the element is being clipped
6464 */
6465 OO.ui.mixin.ClippableElement.prototype.isClippedHorizontally = function () {
6466 return this.clippedHorizontally;
6467 };
6468
6469 /**
6470 * Check if the bottom of the element is being clipped by the nearest scrollable container.
6471 *
6472 * @return {boolean} Part of the element is being clipped
6473 */
6474 OO.ui.mixin.ClippableElement.prototype.isClippedVertically = function () {
6475 return this.clippedVertically;
6476 };
6477
6478 /**
6479 * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
6480 *
6481 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
6482 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
6483 */
6484 OO.ui.mixin.ClippableElement.prototype.setIdealSize = function ( width, height ) {
6485 this.idealWidth = width;
6486 this.idealHeight = height;
6487
6488 if ( !this.clipping ) {
6489 // Update dimensions
6490 this.$clippable.css( { width: width, height: height } );
6491 }
6492 // While clipping, idealWidth and idealHeight are not considered
6493 };
6494
6495 /**
6496 * Clip element to visible boundaries and allow scrolling when needed. Call this method when
6497 * the element's natural height changes.
6498 *
6499 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
6500 * overlapped by, the visible area of the nearest scrollable container.
6501 *
6502 * @chainable
6503 */
6504 OO.ui.mixin.ClippableElement.prototype.clip = function () {
6505 if ( !this.clipping ) {
6506 // this.$clippableContainer and this.$clippableWindow are null, so the below will fail
6507 return this;
6508 }
6509
6510 var buffer = 7, // Chosen by fair dice roll
6511 cOffset = this.$clippable.offset(),
6512 $container = this.$clippableContainer.is( 'html, body' ) ?
6513 this.$clippableWindow : this.$clippableContainer,
6514 ccOffset = $container.offset() || { top: 0, left: 0 },
6515 ccHeight = $container.innerHeight() - buffer,
6516 ccWidth = $container.innerWidth() - buffer,
6517 cWidth = this.$clippable.outerWidth() + buffer,
6518 scrollTop = this.$clippableScroller[0] === this.$clippableWindow[0] ? this.$clippableScroller.scrollTop() : 0,
6519 scrollLeft = this.$clippableScroller.scrollLeft(),
6520 desiredWidth = cOffset.left < 0 ?
6521 cWidth + cOffset.left :
6522 ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left,
6523 desiredHeight = ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top,
6524 naturalWidth = this.$clippable.prop( 'scrollWidth' ),
6525 naturalHeight = this.$clippable.prop( 'scrollHeight' ),
6526 clipWidth = desiredWidth < naturalWidth,
6527 clipHeight = desiredHeight < naturalHeight;
6528
6529 if ( clipWidth ) {
6530 this.$clippable.css( { overflowX: 'scroll', width: desiredWidth } );
6531 } else {
6532 this.$clippable.css( { width: this.idealWidth || '', overflowX: '' } );
6533 }
6534 if ( clipHeight ) {
6535 this.$clippable.css( { overflowY: 'scroll', height: desiredHeight } );
6536 } else {
6537 this.$clippable.css( { height: this.idealHeight || '', overflowY: '' } );
6538 }
6539
6540 // If we stopped clipping in at least one of the dimensions
6541 if ( !clipWidth || !clipHeight ) {
6542 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
6543 }
6544
6545 this.clippedHorizontally = clipWidth;
6546 this.clippedVertically = clipHeight;
6547
6548 return this;
6549 };
6550
6551 /**
6552 * Tools, together with {@link OO.ui.ToolGroup toolgroups}, constitute {@link OO.ui.Toolbar toolbars}.
6553 * Each tool is configured with a static name, title, and icon and is customized with the command to carry
6554 * out when the tool is selected. Tools must also be registered with a {@link OO.ui.ToolFactory tool factory},
6555 * which creates the tools on demand.
6556 *
6557 * Tools are added to toolgroups ({@link OO.ui.ListToolGroup ListToolGroup},
6558 * {@link OO.ui.BarToolGroup BarToolGroup}, or {@link OO.ui.MenuToolGroup MenuToolGroup}), which determine how
6559 * the tool is displayed in the toolbar. See {@link OO.ui.Toolbar toolbars} for an example.
6560 *
6561 * For more information, please see the [OOjs UI documentation on MediaWiki][1].
6562 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
6563 *
6564 * @abstract
6565 * @class
6566 * @extends OO.ui.Widget
6567 * @mixins OO.ui.mixin.IconElement
6568 * @mixins OO.ui.mixin.FlaggedElement
6569 * @mixins OO.ui.mixin.TabIndexedElement
6570 *
6571 * @constructor
6572 * @param {OO.ui.ToolGroup} toolGroup
6573 * @param {Object} [config] Configuration options
6574 * @cfg {string|Function} [title] Title text or a function that returns text. If this config is omitted, the value of
6575 * the {@link #static-title static title} property is used.
6576 *
6577 * The title is used in different ways depending on the type of toolgroup that contains the tool. The
6578 * 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
6579 * part of a {@link OO.ui.ListToolGroup list} or {@link OO.ui.MenuToolGroup menu} toolgroup.
6580 *
6581 * For bar toolgroups, a description of the accelerator key is appended to the title if an accelerator key
6582 * is associated with an action by the same name as the tool and accelerator functionality has been added to the application.
6583 * To add accelerator key functionality, you must subclass OO.ui.Toolbar and override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method.
6584 */
6585 OO.ui.Tool = function OoUiTool( toolGroup, config ) {
6586 // Allow passing positional parameters inside the config object
6587 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
6588 config = toolGroup;
6589 toolGroup = config.toolGroup;
6590 }
6591
6592 // Configuration initialization
6593 config = config || {};
6594
6595 // Parent constructor
6596 OO.ui.Tool.super.call( this, config );
6597
6598 // Properties
6599 this.toolGroup = toolGroup;
6600 this.toolbar = this.toolGroup.getToolbar();
6601 this.active = false;
6602 this.$title = $( '<span>' );
6603 this.$accel = $( '<span>' );
6604 this.$link = $( '<a>' );
6605 this.title = null;
6606
6607 // Mixin constructors
6608 OO.ui.mixin.IconElement.call( this, config );
6609 OO.ui.mixin.FlaggedElement.call( this, config );
6610 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$link } ) );
6611
6612 // Events
6613 this.toolbar.connect( this, { updateState: 'onUpdateState' } );
6614
6615 // Initialization
6616 this.$title.addClass( 'oo-ui-tool-title' );
6617 this.$accel
6618 .addClass( 'oo-ui-tool-accel' )
6619 .prop( {
6620 // This may need to be changed if the key names are ever localized,
6621 // but for now they are essentially written in English
6622 dir: 'ltr',
6623 lang: 'en'
6624 } );
6625 this.$link
6626 .addClass( 'oo-ui-tool-link' )
6627 .append( this.$icon, this.$title, this.$accel )
6628 .attr( 'role', 'button' );
6629 this.$element
6630 .data( 'oo-ui-tool', this )
6631 .addClass(
6632 'oo-ui-tool ' + 'oo-ui-tool-name-' +
6633 this.constructor.static.name.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' )
6634 )
6635 .toggleClass( 'oo-ui-tool-with-label', this.constructor.static.displayBothIconAndLabel )
6636 .append( this.$link );
6637 this.setTitle( config.title || this.constructor.static.title );
6638 };
6639
6640 /* Setup */
6641
6642 OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
6643 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.IconElement );
6644 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.FlaggedElement );
6645 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.TabIndexedElement );
6646
6647 /* Static Properties */
6648
6649 /**
6650 * @static
6651 * @inheritdoc
6652 */
6653 OO.ui.Tool.static.tagName = 'span';
6654
6655 /**
6656 * Symbolic name of tool.
6657 *
6658 * The symbolic name is used internally to register the tool with a {@link OO.ui.ToolFactory ToolFactory}. It can
6659 * also be used when adding tools to toolgroups.
6660 *
6661 * @abstract
6662 * @static
6663 * @inheritable
6664 * @property {string}
6665 */
6666 OO.ui.Tool.static.name = '';
6667
6668 /**
6669 * Symbolic name of the group.
6670 *
6671 * The group name is used to associate tools with each other so that they can be selected later by
6672 * a {@link OO.ui.ToolGroup toolgroup}.
6673 *
6674 * @abstract
6675 * @static
6676 * @inheritable
6677 * @property {string}
6678 */
6679 OO.ui.Tool.static.group = '';
6680
6681 /**
6682 * 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.
6683 *
6684 * @abstract
6685 * @static
6686 * @inheritable
6687 * @property {string|Function}
6688 */
6689 OO.ui.Tool.static.title = '';
6690
6691 /**
6692 * Display both icon and label when the tool is used in a {@link OO.ui.BarToolGroup bar} toolgroup.
6693 * Normally only the icon is displayed, or only the label if no icon is given.
6694 *
6695 * @static
6696 * @inheritable
6697 * @property {boolean}
6698 */
6699 OO.ui.Tool.static.displayBothIconAndLabel = false;
6700
6701 /**
6702 * Add tool to catch-all groups automatically.
6703 *
6704 * A catch-all group, which contains all tools that do not currently belong to a toolgroup,
6705 * can be included in a toolgroup using the wildcard selector, an asterisk (*).
6706 *
6707 * @static
6708 * @inheritable
6709 * @property {boolean}
6710 */
6711 OO.ui.Tool.static.autoAddToCatchall = true;
6712
6713 /**
6714 * Add tool to named groups automatically.
6715 *
6716 * By default, tools that are configured with a static ‘group’ property are added
6717 * to that group and will be selected when the symbolic name of the group is specified (e.g., when
6718 * toolgroups include tools by group name).
6719 *
6720 * @static
6721 * @property {boolean}
6722 * @inheritable
6723 */
6724 OO.ui.Tool.static.autoAddToGroup = true;
6725
6726 /**
6727 * Check if this tool is compatible with given data.
6728 *
6729 * This is a stub that can be overriden to provide support for filtering tools based on an
6730 * arbitrary piece of information (e.g., where the cursor is in a document). The implementation
6731 * must also call this method so that the compatibility check can be performed.
6732 *
6733 * @static
6734 * @inheritable
6735 * @param {Mixed} data Data to check
6736 * @return {boolean} Tool can be used with data
6737 */
6738 OO.ui.Tool.static.isCompatibleWith = function () {
6739 return false;
6740 };
6741
6742 /* Methods */
6743
6744 /**
6745 * Handle the toolbar state being updated.
6746 *
6747 * This is an abstract method that must be overridden in a concrete subclass.
6748 *
6749 * @protected
6750 * @abstract
6751 */
6752 OO.ui.Tool.prototype.onUpdateState = function () {
6753 throw new Error(
6754 'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor
6755 );
6756 };
6757
6758 /**
6759 * Handle the tool being selected.
6760 *
6761 * This is an abstract method that must be overridden in a concrete subclass.
6762 *
6763 * @protected
6764 * @abstract
6765 */
6766 OO.ui.Tool.prototype.onSelect = function () {
6767 throw new Error(
6768 'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor
6769 );
6770 };
6771
6772 /**
6773 * Check if the tool is active.
6774 *
6775 * Tools become active when their #onSelect or #onUpdateState handlers change them to appear pressed
6776 * with the #setActive method. Additional CSS is applied to the tool to reflect the active state.
6777 *
6778 * @return {boolean} Tool is active
6779 */
6780 OO.ui.Tool.prototype.isActive = function () {
6781 return this.active;
6782 };
6783
6784 /**
6785 * Make the tool appear active or inactive.
6786 *
6787 * This method should be called within #onSelect or #onUpdateState event handlers to make the tool
6788 * appear pressed or not.
6789 *
6790 * @param {boolean} state Make tool appear active
6791 */
6792 OO.ui.Tool.prototype.setActive = function ( state ) {
6793 this.active = !!state;
6794 if ( this.active ) {
6795 this.$element.addClass( 'oo-ui-tool-active' );
6796 } else {
6797 this.$element.removeClass( 'oo-ui-tool-active' );
6798 }
6799 };
6800
6801 /**
6802 * Set the tool #title.
6803 *
6804 * @param {string|Function} title Title text or a function that returns text
6805 * @chainable
6806 */
6807 OO.ui.Tool.prototype.setTitle = function ( title ) {
6808 this.title = OO.ui.resolveMsg( title );
6809 this.updateTitle();
6810 return this;
6811 };
6812
6813 /**
6814 * Get the tool #title.
6815 *
6816 * @return {string} Title text
6817 */
6818 OO.ui.Tool.prototype.getTitle = function () {
6819 return this.title;
6820 };
6821
6822 /**
6823 * Get the tool's symbolic name.
6824 *
6825 * @return {string} Symbolic name of tool
6826 */
6827 OO.ui.Tool.prototype.getName = function () {
6828 return this.constructor.static.name;
6829 };
6830
6831 /**
6832 * Update the title.
6833 */
6834 OO.ui.Tool.prototype.updateTitle = function () {
6835 var titleTooltips = this.toolGroup.constructor.static.titleTooltips,
6836 accelTooltips = this.toolGroup.constructor.static.accelTooltips,
6837 accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
6838 tooltipParts = [];
6839
6840 this.$title.text( this.title );
6841 this.$accel.text( accel );
6842
6843 if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
6844 tooltipParts.push( this.title );
6845 }
6846 if ( accelTooltips && typeof accel === 'string' && accel.length ) {
6847 tooltipParts.push( accel );
6848 }
6849 if ( tooltipParts.length ) {
6850 this.$link.attr( 'title', tooltipParts.join( ' ' ) );
6851 } else {
6852 this.$link.removeAttr( 'title' );
6853 }
6854 };
6855
6856 /**
6857 * Destroy tool.
6858 *
6859 * Destroying the tool removes all event handlers and the tool’s DOM elements.
6860 * Call this method whenever you are done using a tool.
6861 */
6862 OO.ui.Tool.prototype.destroy = function () {
6863 this.toolbar.disconnect( this );
6864 this.$element.remove();
6865 };
6866
6867 /**
6868 * Toolbars are complex interface components that permit users to easily access a variety
6869 * of {@link OO.ui.Tool tools} (e.g., formatting commands) and actions, which are additional commands that are
6870 * part of the toolbar, but not configured as tools.
6871 *
6872 * Individual tools are customized and then registered with a {@link OO.ui.ToolFactory tool factory}, which creates
6873 * the tools on demand. Each tool has a symbolic name (used when registering the tool), a title (e.g., ‘Insert
6874 * picture’), and an icon.
6875 *
6876 * Individual tools are organized in {@link OO.ui.ToolGroup toolgroups}, which can be {@link OO.ui.MenuToolGroup menus}
6877 * of tools, {@link OO.ui.ListToolGroup lists} of tools, or a single {@link OO.ui.BarToolGroup bar} of tools.
6878 * The arrangement and order of the toolgroups is customized when the toolbar is set up. Tools can be presented in
6879 * any order, but each can only appear once in the toolbar.
6880 *
6881 * The following is an example of a basic toolbar.
6882 *
6883 * @example
6884 * // Example of a toolbar
6885 * // Create the toolbar
6886 * var toolFactory = new OO.ui.ToolFactory();
6887 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
6888 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
6889 *
6890 * // We will be placing status text in this element when tools are used
6891 * var $area = $( '<p>' ).text( 'Toolbar example' );
6892 *
6893 * // Define the tools that we're going to place in our toolbar
6894 *
6895 * // Create a class inheriting from OO.ui.Tool
6896 * function PictureTool() {
6897 * PictureTool.super.apply( this, arguments );
6898 * }
6899 * OO.inheritClass( PictureTool, OO.ui.Tool );
6900 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
6901 * // of 'icon' and 'title' (displayed icon and text).
6902 * PictureTool.static.name = 'picture';
6903 * PictureTool.static.icon = 'picture';
6904 * PictureTool.static.title = 'Insert picture';
6905 * // Defines the action that will happen when this tool is selected (clicked).
6906 * PictureTool.prototype.onSelect = function () {
6907 * $area.text( 'Picture tool clicked!' );
6908 * // Never display this tool as "active" (selected).
6909 * this.setActive( false );
6910 * };
6911 * // Make this tool available in our toolFactory and thus our toolbar
6912 * toolFactory.register( PictureTool );
6913 *
6914 * // Register two more tools, nothing interesting here
6915 * function SettingsTool() {
6916 * SettingsTool.super.apply( this, arguments );
6917 * }
6918 * OO.inheritClass( SettingsTool, OO.ui.Tool );
6919 * SettingsTool.static.name = 'settings';
6920 * SettingsTool.static.icon = 'settings';
6921 * SettingsTool.static.title = 'Change settings';
6922 * SettingsTool.prototype.onSelect = function () {
6923 * $area.text( 'Settings tool clicked!' );
6924 * this.setActive( false );
6925 * };
6926 * toolFactory.register( SettingsTool );
6927 *
6928 * // Register two more tools, nothing interesting here
6929 * function StuffTool() {
6930 * StuffTool.super.apply( this, arguments );
6931 * }
6932 * OO.inheritClass( StuffTool, OO.ui.Tool );
6933 * StuffTool.static.name = 'stuff';
6934 * StuffTool.static.icon = 'ellipsis';
6935 * StuffTool.static.title = 'More stuff';
6936 * StuffTool.prototype.onSelect = function () {
6937 * $area.text( 'More stuff tool clicked!' );
6938 * this.setActive( false );
6939 * };
6940 * toolFactory.register( StuffTool );
6941 *
6942 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
6943 * // little popup window (a PopupWidget).
6944 * function HelpTool( toolGroup, config ) {
6945 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
6946 * padded: true,
6947 * label: 'Help',
6948 * head: true
6949 * } }, config ) );
6950 * this.popup.$body.append( '<p>I am helpful!</p>' );
6951 * }
6952 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
6953 * HelpTool.static.name = 'help';
6954 * HelpTool.static.icon = 'help';
6955 * HelpTool.static.title = 'Help';
6956 * toolFactory.register( HelpTool );
6957 *
6958 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
6959 * // used once (but not all defined tools must be used).
6960 * toolbar.setup( [
6961 * {
6962 * // 'bar' tool groups display tools' icons only, side-by-side.
6963 * type: 'bar',
6964 * include: [ 'picture', 'help' ]
6965 * },
6966 * {
6967 * // 'list' tool groups display both the titles and icons, in a dropdown list.
6968 * type: 'list',
6969 * indicator: 'down',
6970 * label: 'More',
6971 * include: [ 'settings', 'stuff' ]
6972 * }
6973 * // Note how the tools themselves are toolgroup-agnostic - the same tool can be displayed
6974 * // either in a 'list' or a 'bar'. There is a 'menu' tool group too, not showcased here,
6975 * // since it's more complicated to use. (See the next example snippet on this page.)
6976 * ] );
6977 *
6978 * // Create some UI around the toolbar and place it in the document
6979 * var frame = new OO.ui.PanelLayout( {
6980 * expanded: false,
6981 * framed: true
6982 * } );
6983 * var contentFrame = new OO.ui.PanelLayout( {
6984 * expanded: false,
6985 * padded: true
6986 * } );
6987 * frame.$element.append(
6988 * toolbar.$element,
6989 * contentFrame.$element.append( $area )
6990 * );
6991 * $( 'body' ).append( frame.$element );
6992 *
6993 * // Here is where the toolbar is actually built. This must be done after inserting it into the
6994 * // document.
6995 * toolbar.initialize();
6996 *
6997 * The following example extends the previous one to illustrate 'menu' toolgroups and the usage of
6998 * 'updateState' event.
6999 *
7000 * @example
7001 * // Create the toolbar
7002 * var toolFactory = new OO.ui.ToolFactory();
7003 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
7004 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
7005 *
7006 * // We will be placing status text in this element when tools are used
7007 * var $area = $( '<p>' ).text( 'Toolbar example' );
7008 *
7009 * // Define the tools that we're going to place in our toolbar
7010 *
7011 * // Create a class inheriting from OO.ui.Tool
7012 * function PictureTool() {
7013 * PictureTool.super.apply( this, arguments );
7014 * }
7015 * OO.inheritClass( PictureTool, OO.ui.Tool );
7016 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
7017 * // of 'icon' and 'title' (displayed icon and text).
7018 * PictureTool.static.name = 'picture';
7019 * PictureTool.static.icon = 'picture';
7020 * PictureTool.static.title = 'Insert picture';
7021 * // Defines the action that will happen when this tool is selected (clicked).
7022 * PictureTool.prototype.onSelect = function () {
7023 * $area.text( 'Picture tool clicked!' );
7024 * // Never display this tool as "active" (selected).
7025 * this.setActive( false );
7026 * };
7027 * // The toolbar can be synchronized with the state of some external stuff, like a text
7028 * // editor's editing area, highlighting the tools (e.g. a 'bold' tool would be shown as active
7029 * // when the text cursor was inside bolded text). Here we simply disable this feature.
7030 * PictureTool.prototype.onUpdateState = function () {
7031 * };
7032 * // Make this tool available in our toolFactory and thus our toolbar
7033 * toolFactory.register( PictureTool );
7034 *
7035 * // Register two more tools, nothing interesting here
7036 * function SettingsTool() {
7037 * SettingsTool.super.apply( this, arguments );
7038 * this.reallyActive = false;
7039 * }
7040 * OO.inheritClass( SettingsTool, OO.ui.Tool );
7041 * SettingsTool.static.name = 'settings';
7042 * SettingsTool.static.icon = 'settings';
7043 * SettingsTool.static.title = 'Change settings';
7044 * SettingsTool.prototype.onSelect = function () {
7045 * $area.text( 'Settings tool clicked!' );
7046 * // Toggle the active state on each click
7047 * this.reallyActive = !this.reallyActive;
7048 * this.setActive( this.reallyActive );
7049 * // To update the menu label
7050 * this.toolbar.emit( 'updateState' );
7051 * };
7052 * SettingsTool.prototype.onUpdateState = function () {
7053 * };
7054 * toolFactory.register( SettingsTool );
7055 *
7056 * // Register two more tools, nothing interesting here
7057 * function StuffTool() {
7058 * StuffTool.super.apply( this, arguments );
7059 * this.reallyActive = false;
7060 * }
7061 * OO.inheritClass( StuffTool, OO.ui.Tool );
7062 * StuffTool.static.name = 'stuff';
7063 * StuffTool.static.icon = 'ellipsis';
7064 * StuffTool.static.title = 'More stuff';
7065 * StuffTool.prototype.onSelect = function () {
7066 * $area.text( 'More stuff tool clicked!' );
7067 * // Toggle the active state on each click
7068 * this.reallyActive = !this.reallyActive;
7069 * this.setActive( this.reallyActive );
7070 * // To update the menu label
7071 * this.toolbar.emit( 'updateState' );
7072 * };
7073 * StuffTool.prototype.onUpdateState = function () {
7074 * };
7075 * toolFactory.register( StuffTool );
7076 *
7077 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
7078 * // little popup window (a PopupWidget). 'onUpdateState' is also already implemented.
7079 * function HelpTool( toolGroup, config ) {
7080 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
7081 * padded: true,
7082 * label: 'Help',
7083 * head: true
7084 * } }, config ) );
7085 * this.popup.$body.append( '<p>I am helpful!</p>' );
7086 * }
7087 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
7088 * HelpTool.static.name = 'help';
7089 * HelpTool.static.icon = 'help';
7090 * HelpTool.static.title = 'Help';
7091 * toolFactory.register( HelpTool );
7092 *
7093 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
7094 * // used once (but not all defined tools must be used).
7095 * toolbar.setup( [
7096 * {
7097 * // 'bar' tool groups display tools' icons only, side-by-side.
7098 * type: 'bar',
7099 * include: [ 'picture', 'help' ]
7100 * },
7101 * {
7102 * // 'menu' tool groups display both the titles and icons, in a dropdown menu.
7103 * // Menu label indicates which items are selected.
7104 * type: 'menu',
7105 * indicator: 'down',
7106 * include: [ 'settings', 'stuff' ]
7107 * }
7108 * ] );
7109 *
7110 * // Create some UI around the toolbar and place it in the document
7111 * var frame = new OO.ui.PanelLayout( {
7112 * expanded: false,
7113 * framed: true
7114 * } );
7115 * var contentFrame = new OO.ui.PanelLayout( {
7116 * expanded: false,
7117 * padded: true
7118 * } );
7119 * frame.$element.append(
7120 * toolbar.$element,
7121 * contentFrame.$element.append( $area )
7122 * );
7123 * $( 'body' ).append( frame.$element );
7124 *
7125 * // Here is where the toolbar is actually built. This must be done after inserting it into the
7126 * // document.
7127 * toolbar.initialize();
7128 * toolbar.emit( 'updateState' );
7129 *
7130 * @class
7131 * @extends OO.ui.Element
7132 * @mixins OO.EventEmitter
7133 * @mixins OO.ui.mixin.GroupElement
7134 *
7135 * @constructor
7136 * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
7137 * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating toolgroups
7138 * @param {Object} [config] Configuration options
7139 * @cfg {boolean} [actions] Add an actions section to the toolbar. Actions are commands that are included
7140 * in the toolbar, but are not configured as tools. By default, actions are displayed on the right side of
7141 * the toolbar.
7142 * @cfg {boolean} [shadow] Add a shadow below the toolbar.
7143 */
7144 OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
7145 // Allow passing positional parameters inside the config object
7146 if ( OO.isPlainObject( toolFactory ) && config === undefined ) {
7147 config = toolFactory;
7148 toolFactory = config.toolFactory;
7149 toolGroupFactory = config.toolGroupFactory;
7150 }
7151
7152 // Configuration initialization
7153 config = config || {};
7154
7155 // Parent constructor
7156 OO.ui.Toolbar.super.call( this, config );
7157
7158 // Mixin constructors
7159 OO.EventEmitter.call( this );
7160 OO.ui.mixin.GroupElement.call( this, config );
7161
7162 // Properties
7163 this.toolFactory = toolFactory;
7164 this.toolGroupFactory = toolGroupFactory;
7165 this.groups = [];
7166 this.tools = {};
7167 this.$bar = $( '<div>' );
7168 this.$actions = $( '<div>' );
7169 this.initialized = false;
7170 this.onWindowResizeHandler = this.onWindowResize.bind( this );
7171
7172 // Events
7173 this.$element
7174 .add( this.$bar ).add( this.$group ).add( this.$actions )
7175 .on( 'mousedown keydown', this.onPointerDown.bind( this ) );
7176
7177 // Initialization
7178 this.$group.addClass( 'oo-ui-toolbar-tools' );
7179 if ( config.actions ) {
7180 this.$bar.append( this.$actions.addClass( 'oo-ui-toolbar-actions' ) );
7181 }
7182 this.$bar
7183 .addClass( 'oo-ui-toolbar-bar' )
7184 .append( this.$group, '<div style="clear:both"></div>' );
7185 if ( config.shadow ) {
7186 this.$bar.append( '<div class="oo-ui-toolbar-shadow"></div>' );
7187 }
7188 this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar );
7189 };
7190
7191 /* Setup */
7192
7193 OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
7194 OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
7195 OO.mixinClass( OO.ui.Toolbar, OO.ui.mixin.GroupElement );
7196
7197 /* Methods */
7198
7199 /**
7200 * Get the tool factory.
7201 *
7202 * @return {OO.ui.ToolFactory} Tool factory
7203 */
7204 OO.ui.Toolbar.prototype.getToolFactory = function () {
7205 return this.toolFactory;
7206 };
7207
7208 /**
7209 * Get the toolgroup factory.
7210 *
7211 * @return {OO.Factory} Toolgroup factory
7212 */
7213 OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
7214 return this.toolGroupFactory;
7215 };
7216
7217 /**
7218 * Handles mouse down events.
7219 *
7220 * @private
7221 * @param {jQuery.Event} e Mouse down event
7222 */
7223 OO.ui.Toolbar.prototype.onPointerDown = function ( e ) {
7224 var $closestWidgetToEvent = $( e.target ).closest( '.oo-ui-widget' ),
7225 $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
7226 if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[ 0 ] === $closestWidgetToToolbar[ 0 ] ) {
7227 return false;
7228 }
7229 };
7230
7231 /**
7232 * Handle window resize event.
7233 *
7234 * @private
7235 * @param {jQuery.Event} e Window resize event
7236 */
7237 OO.ui.Toolbar.prototype.onWindowResize = function () {
7238 this.$element.toggleClass(
7239 'oo-ui-toolbar-narrow',
7240 this.$bar.width() <= this.narrowThreshold
7241 );
7242 };
7243
7244 /**
7245 * Sets up handles and preloads required information for the toolbar to work.
7246 * This must be called after it is attached to a visible document and before doing anything else.
7247 */
7248 OO.ui.Toolbar.prototype.initialize = function () {
7249 this.initialized = true;
7250 this.narrowThreshold = this.$group.width() + this.$actions.width();
7251 $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
7252 this.onWindowResize();
7253 };
7254
7255 /**
7256 * Set up the toolbar.
7257 *
7258 * The toolbar is set up with a list of toolgroup configurations that specify the type of
7259 * toolgroup ({@link OO.ui.BarToolGroup bar}, {@link OO.ui.MenuToolGroup menu}, or {@link OO.ui.ListToolGroup list})
7260 * to add and which tools to include, exclude, promote, or demote within that toolgroup. Please
7261 * see {@link OO.ui.ToolGroup toolgroups} for more information about including tools in toolgroups.
7262 *
7263 * @param {Object.<string,Array>} groups List of toolgroup configurations
7264 * @param {Array|string} [groups.include] Tools to include in the toolgroup
7265 * @param {Array|string} [groups.exclude] Tools to exclude from the toolgroup
7266 * @param {Array|string} [groups.promote] Tools to promote to the beginning of the toolgroup
7267 * @param {Array|string} [groups.demote] Tools to demote to the end of the toolgroup
7268 */
7269 OO.ui.Toolbar.prototype.setup = function ( groups ) {
7270 var i, len, type, group,
7271 items = [],
7272 defaultType = 'bar';
7273
7274 // Cleanup previous groups
7275 this.reset();
7276
7277 // Build out new groups
7278 for ( i = 0, len = groups.length; i < len; i++ ) {
7279 group = groups[ i ];
7280 if ( group.include === '*' ) {
7281 // Apply defaults to catch-all groups
7282 if ( group.type === undefined ) {
7283 group.type = 'list';
7284 }
7285 if ( group.label === undefined ) {
7286 group.label = OO.ui.msg( 'ooui-toolbar-more' );
7287 }
7288 }
7289 // Check type has been registered
7290 type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType;
7291 items.push(
7292 this.getToolGroupFactory().create( type, this, group )
7293 );
7294 }
7295 this.addItems( items );
7296 };
7297
7298 /**
7299 * Remove all tools and toolgroups from the toolbar.
7300 */
7301 OO.ui.Toolbar.prototype.reset = function () {
7302 var i, len;
7303
7304 this.groups = [];
7305 this.tools = {};
7306 for ( i = 0, len = this.items.length; i < len; i++ ) {
7307 this.items[ i ].destroy();
7308 }
7309 this.clearItems();
7310 };
7311
7312 /**
7313 * Destroy the toolbar.
7314 *
7315 * Destroying the toolbar removes all event handlers and DOM elements that constitute the toolbar. Call
7316 * this method whenever you are done using a toolbar.
7317 */
7318 OO.ui.Toolbar.prototype.destroy = function () {
7319 $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
7320 this.reset();
7321 this.$element.remove();
7322 };
7323
7324 /**
7325 * Check if the tool is available.
7326 *
7327 * Available tools are ones that have not yet been added to the toolbar.
7328 *
7329 * @param {string} name Symbolic name of tool
7330 * @return {boolean} Tool is available
7331 */
7332 OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
7333 return !this.tools[ name ];
7334 };
7335
7336 /**
7337 * Prevent tool from being used again.
7338 *
7339 * @param {OO.ui.Tool} tool Tool to reserve
7340 */
7341 OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
7342 this.tools[ tool.getName() ] = tool;
7343 };
7344
7345 /**
7346 * Allow tool to be used again.
7347 *
7348 * @param {OO.ui.Tool} tool Tool to release
7349 */
7350 OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
7351 delete this.tools[ tool.getName() ];
7352 };
7353
7354 /**
7355 * Get accelerator label for tool.
7356 *
7357 * The OOjs UI library does not contain an accelerator system, but this is the hook for one. To
7358 * use an accelerator system, subclass the toolbar and override this method, which is meant to return a label
7359 * that describes the accelerator keys for the tool passed (by symbolic name) to the method.
7360 *
7361 * @param {string} name Symbolic name of tool
7362 * @return {string|undefined} Tool accelerator label if available
7363 */
7364 OO.ui.Toolbar.prototype.getToolAccelerator = function () {
7365 return undefined;
7366 };
7367
7368 /**
7369 * ToolGroups are collections of {@link OO.ui.Tool tools} that are used in a {@link OO.ui.Toolbar toolbar}.
7370 * The type of toolgroup ({@link OO.ui.ListToolGroup list}, {@link OO.ui.BarToolGroup bar}, or {@link OO.ui.MenuToolGroup menu})
7371 * to which a tool belongs determines how the tool is arranged and displayed in the toolbar. Toolgroups
7372 * themselves are created on demand with a {@link OO.ui.ToolGroupFactory toolgroup factory}.
7373 *
7374 * Toolgroups can contain individual tools, groups of tools, or all available tools:
7375 *
7376 * To include an individual tool (or array of individual tools), specify tools by symbolic name:
7377 *
7378 * include: [ 'tool-name' ] or [ { name: 'tool-name' }]
7379 *
7380 * 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.)
7381 *
7382 * include: [ { group: 'group-name' } ]
7383 *
7384 * To include all tools that are not yet assigned to a toolgroup, use the catch-all selector, an asterisk (*):
7385 *
7386 * include: '*'
7387 *
7388 * See {@link OO.ui.Toolbar toolbars} for a full example. For more information about toolbars in general,
7389 * please see the [OOjs UI documentation on MediaWiki][1].
7390 *
7391 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
7392 *
7393 * @abstract
7394 * @class
7395 * @extends OO.ui.Widget
7396 * @mixins OO.ui.mixin.GroupElement
7397 *
7398 * @constructor
7399 * @param {OO.ui.Toolbar} toolbar
7400 * @param {Object} [config] Configuration options
7401 * @cfg {Array|string} [include=[]] List of tools to include in the toolgroup.
7402 * @cfg {Array|string} [exclude=[]] List of tools to exclude from the toolgroup.
7403 * @cfg {Array|string} [promote=[]] List of tools to promote to the beginning of the toolgroup.
7404 * @cfg {Array|string} [demote=[]] List of tools to demote to the end of the toolgroup.
7405 * This setting is particularly useful when tools have been added to the toolgroup
7406 * en masse (e.g., via the catch-all selector).
7407 */
7408 OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
7409 // Allow passing positional parameters inside the config object
7410 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
7411 config = toolbar;
7412 toolbar = config.toolbar;
7413 }
7414
7415 // Configuration initialization
7416 config = config || {};
7417
7418 // Parent constructor
7419 OO.ui.ToolGroup.super.call( this, config );
7420
7421 // Mixin constructors
7422 OO.ui.mixin.GroupElement.call( this, config );
7423
7424 // Properties
7425 this.toolbar = toolbar;
7426 this.tools = {};
7427 this.pressed = null;
7428 this.autoDisabled = false;
7429 this.include = config.include || [];
7430 this.exclude = config.exclude || [];
7431 this.promote = config.promote || [];
7432 this.demote = config.demote || [];
7433 this.onCapturedMouseKeyUpHandler = this.onCapturedMouseKeyUp.bind( this );
7434
7435 // Events
7436 this.$element.on( {
7437 mousedown: this.onMouseKeyDown.bind( this ),
7438 mouseup: this.onMouseKeyUp.bind( this ),
7439 keydown: this.onMouseKeyDown.bind( this ),
7440 keyup: this.onMouseKeyUp.bind( this ),
7441 focus: this.onMouseOverFocus.bind( this ),
7442 blur: this.onMouseOutBlur.bind( this ),
7443 mouseover: this.onMouseOverFocus.bind( this ),
7444 mouseout: this.onMouseOutBlur.bind( this )
7445 } );
7446 this.toolbar.getToolFactory().connect( this, { register: 'onToolFactoryRegister' } );
7447 this.aggregate( { disable: 'itemDisable' } );
7448 this.connect( this, { itemDisable: 'updateDisabled' } );
7449
7450 // Initialization
7451 this.$group.addClass( 'oo-ui-toolGroup-tools' );
7452 this.$element
7453 .addClass( 'oo-ui-toolGroup' )
7454 .append( this.$group );
7455 this.populate();
7456 };
7457
7458 /* Setup */
7459
7460 OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget );
7461 OO.mixinClass( OO.ui.ToolGroup, OO.ui.mixin.GroupElement );
7462
7463 /* Events */
7464
7465 /**
7466 * @event update
7467 */
7468
7469 /* Static Properties */
7470
7471 /**
7472 * Show labels in tooltips.
7473 *
7474 * @static
7475 * @inheritable
7476 * @property {boolean}
7477 */
7478 OO.ui.ToolGroup.static.titleTooltips = false;
7479
7480 /**
7481 * Show acceleration labels in tooltips.
7482 *
7483 * Note: The OOjs UI library does not include an accelerator system, but does contain
7484 * a hook for one. To use an accelerator system, subclass the {@link OO.ui.Toolbar toolbar} and
7485 * override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method, which is
7486 * meant to return a label that describes the accelerator keys for a given tool (e.g., 'Ctrl + M').
7487 *
7488 * @static
7489 * @inheritable
7490 * @property {boolean}
7491 */
7492 OO.ui.ToolGroup.static.accelTooltips = false;
7493
7494 /**
7495 * Automatically disable the toolgroup when all tools are disabled
7496 *
7497 * @static
7498 * @inheritable
7499 * @property {boolean}
7500 */
7501 OO.ui.ToolGroup.static.autoDisable = true;
7502
7503 /* Methods */
7504
7505 /**
7506 * @inheritdoc
7507 */
7508 OO.ui.ToolGroup.prototype.isDisabled = function () {
7509 return this.autoDisabled || OO.ui.ToolGroup.super.prototype.isDisabled.apply( this, arguments );
7510 };
7511
7512 /**
7513 * @inheritdoc
7514 */
7515 OO.ui.ToolGroup.prototype.updateDisabled = function () {
7516 var i, item, allDisabled = true;
7517
7518 if ( this.constructor.static.autoDisable ) {
7519 for ( i = this.items.length - 1; i >= 0; i-- ) {
7520 item = this.items[ i ];
7521 if ( !item.isDisabled() ) {
7522 allDisabled = false;
7523 break;
7524 }
7525 }
7526 this.autoDisabled = allDisabled;
7527 }
7528 OO.ui.ToolGroup.super.prototype.updateDisabled.apply( this, arguments );
7529 };
7530
7531 /**
7532 * Handle mouse down and key down events.
7533 *
7534 * @protected
7535 * @param {jQuery.Event} e Mouse down or key down event
7536 */
7537 OO.ui.ToolGroup.prototype.onMouseKeyDown = function ( e ) {
7538 if (
7539 !this.isDisabled() &&
7540 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
7541 ) {
7542 this.pressed = this.getTargetTool( e );
7543 if ( this.pressed ) {
7544 this.pressed.setActive( true );
7545 this.getElementDocument().addEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler, true );
7546 this.getElementDocument().addEventListener( 'keyup', this.onCapturedMouseKeyUpHandler, true );
7547 }
7548 return false;
7549 }
7550 };
7551
7552 /**
7553 * Handle captured mouse up and key up events.
7554 *
7555 * @protected
7556 * @param {Event} e Mouse up or key up event
7557 */
7558 OO.ui.ToolGroup.prototype.onCapturedMouseKeyUp = function ( e ) {
7559 this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler, true );
7560 this.getElementDocument().removeEventListener( 'keyup', this.onCapturedMouseKeyUpHandler, true );
7561 // onMouseKeyUp may be called a second time, depending on where the mouse is when the button is
7562 // released, but since `this.pressed` will no longer be true, the second call will be ignored.
7563 this.onMouseKeyUp( e );
7564 };
7565
7566 /**
7567 * Handle mouse up and key up events.
7568 *
7569 * @protected
7570 * @param {jQuery.Event} e Mouse up or key up event
7571 */
7572 OO.ui.ToolGroup.prototype.onMouseKeyUp = function ( e ) {
7573 var tool = this.getTargetTool( e );
7574
7575 if (
7576 !this.isDisabled() && this.pressed && this.pressed === tool &&
7577 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
7578 ) {
7579 this.pressed.onSelect();
7580 this.pressed = null;
7581 return false;
7582 }
7583
7584 this.pressed = null;
7585 };
7586
7587 /**
7588 * Handle mouse over and focus events.
7589 *
7590 * @protected
7591 * @param {jQuery.Event} e Mouse over or focus event
7592 */
7593 OO.ui.ToolGroup.prototype.onMouseOverFocus = function ( e ) {
7594 var tool = this.getTargetTool( e );
7595
7596 if ( this.pressed && this.pressed === tool ) {
7597 this.pressed.setActive( true );
7598 }
7599 };
7600
7601 /**
7602 * Handle mouse out and blur events.
7603 *
7604 * @protected
7605 * @param {jQuery.Event} e Mouse out or blur event
7606 */
7607 OO.ui.ToolGroup.prototype.onMouseOutBlur = function ( e ) {
7608 var tool = this.getTargetTool( e );
7609
7610 if ( this.pressed && this.pressed === tool ) {
7611 this.pressed.setActive( false );
7612 }
7613 };
7614
7615 /**
7616 * Get the closest tool to a jQuery.Event.
7617 *
7618 * Only tool links are considered, which prevents other elements in the tool such as popups from
7619 * triggering tool group interactions.
7620 *
7621 * @private
7622 * @param {jQuery.Event} e
7623 * @return {OO.ui.Tool|null} Tool, `null` if none was found
7624 */
7625 OO.ui.ToolGroup.prototype.getTargetTool = function ( e ) {
7626 var tool,
7627 $item = $( e.target ).closest( '.oo-ui-tool-link' );
7628
7629 if ( $item.length ) {
7630 tool = $item.parent().data( 'oo-ui-tool' );
7631 }
7632
7633 return tool && !tool.isDisabled() ? tool : null;
7634 };
7635
7636 /**
7637 * Handle tool registry register events.
7638 *
7639 * If a tool is registered after the group is created, we must repopulate the list to account for:
7640 *
7641 * - a tool being added that may be included
7642 * - a tool already included being overridden
7643 *
7644 * @protected
7645 * @param {string} name Symbolic name of tool
7646 */
7647 OO.ui.ToolGroup.prototype.onToolFactoryRegister = function () {
7648 this.populate();
7649 };
7650
7651 /**
7652 * Get the toolbar that contains the toolgroup.
7653 *
7654 * @return {OO.ui.Toolbar} Toolbar that contains the toolgroup
7655 */
7656 OO.ui.ToolGroup.prototype.getToolbar = function () {
7657 return this.toolbar;
7658 };
7659
7660 /**
7661 * Add and remove tools based on configuration.
7662 */
7663 OO.ui.ToolGroup.prototype.populate = function () {
7664 var i, len, name, tool,
7665 toolFactory = this.toolbar.getToolFactory(),
7666 names = {},
7667 add = [],
7668 remove = [],
7669 list = this.toolbar.getToolFactory().getTools(
7670 this.include, this.exclude, this.promote, this.demote
7671 );
7672
7673 // Build a list of needed tools
7674 for ( i = 0, len = list.length; i < len; i++ ) {
7675 name = list[ i ];
7676 if (
7677 // Tool exists
7678 toolFactory.lookup( name ) &&
7679 // Tool is available or is already in this group
7680 ( this.toolbar.isToolAvailable( name ) || this.tools[ name ] )
7681 ) {
7682 // Hack to prevent infinite recursion via ToolGroupTool. We need to reserve the tool before
7683 // creating it, but we can't call reserveTool() yet because we haven't created the tool.
7684 this.toolbar.tools[ name ] = true;
7685 tool = this.tools[ name ];
7686 if ( !tool ) {
7687 // Auto-initialize tools on first use
7688 this.tools[ name ] = tool = toolFactory.create( name, this );
7689 tool.updateTitle();
7690 }
7691 this.toolbar.reserveTool( tool );
7692 add.push( tool );
7693 names[ name ] = true;
7694 }
7695 }
7696 // Remove tools that are no longer needed
7697 for ( name in this.tools ) {
7698 if ( !names[ name ] ) {
7699 this.tools[ name ].destroy();
7700 this.toolbar.releaseTool( this.tools[ name ] );
7701 remove.push( this.tools[ name ] );
7702 delete this.tools[ name ];
7703 }
7704 }
7705 if ( remove.length ) {
7706 this.removeItems( remove );
7707 }
7708 // Update emptiness state
7709 if ( add.length ) {
7710 this.$element.removeClass( 'oo-ui-toolGroup-empty' );
7711 } else {
7712 this.$element.addClass( 'oo-ui-toolGroup-empty' );
7713 }
7714 // Re-add tools (moving existing ones to new locations)
7715 this.addItems( add );
7716 // Disabled state may depend on items
7717 this.updateDisabled();
7718 };
7719
7720 /**
7721 * Destroy toolgroup.
7722 */
7723 OO.ui.ToolGroup.prototype.destroy = function () {
7724 var name;
7725
7726 this.clearItems();
7727 this.toolbar.getToolFactory().disconnect( this );
7728 for ( name in this.tools ) {
7729 this.toolbar.releaseTool( this.tools[ name ] );
7730 this.tools[ name ].disconnect( this ).destroy();
7731 delete this.tools[ name ];
7732 }
7733 this.$element.remove();
7734 };
7735
7736 /**
7737 * MessageDialogs display a confirmation or alert message. By default, the rendered dialog box
7738 * consists of a header that contains the dialog title, a body with the message, and a footer that
7739 * contains any {@link OO.ui.ActionWidget action widgets}. The MessageDialog class is the only type
7740 * of {@link OO.ui.Dialog dialog} that is usually instantiated directly.
7741 *
7742 * There are two basic types of message dialogs, confirmation and alert:
7743 *
7744 * - **confirmation**: the dialog title describes what a progressive action will do and the message provides
7745 * more details about the consequences.
7746 * - **alert**: the dialog title describes which event occurred and the message provides more information
7747 * about why the event occurred.
7748 *
7749 * The MessageDialog class specifies two actions: ‘accept’, the primary
7750 * action (e.g., ‘ok’) and ‘reject,’ the safe action (e.g., ‘cancel’). Both will close the window,
7751 * passing along the selected action.
7752 *
7753 * For more information and examples, please see the [OOjs UI documentation on MediaWiki][1].
7754 *
7755 * @example
7756 * // Example: Creating and opening a message dialog window.
7757 * var messageDialog = new OO.ui.MessageDialog();
7758 *
7759 * // Create and append a window manager.
7760 * var windowManager = new OO.ui.WindowManager();
7761 * $( 'body' ).append( windowManager.$element );
7762 * windowManager.addWindows( [ messageDialog ] );
7763 * // Open the window.
7764 * windowManager.openWindow( messageDialog, {
7765 * title: 'Basic message dialog',
7766 * message: 'This is the message'
7767 * } );
7768 *
7769 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Message_Dialogs
7770 *
7771 * @class
7772 * @extends OO.ui.Dialog
7773 *
7774 * @constructor
7775 * @param {Object} [config] Configuration options
7776 */
7777 OO.ui.MessageDialog = function OoUiMessageDialog( config ) {
7778 // Parent constructor
7779 OO.ui.MessageDialog.super.call( this, config );
7780
7781 // Properties
7782 this.verticalActionLayout = null;
7783
7784 // Initialization
7785 this.$element.addClass( 'oo-ui-messageDialog' );
7786 };
7787
7788 /* Inheritance */
7789
7790 OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog );
7791
7792 /* Static Properties */
7793
7794 OO.ui.MessageDialog.static.name = 'message';
7795
7796 OO.ui.MessageDialog.static.size = 'small';
7797
7798 OO.ui.MessageDialog.static.verbose = false;
7799
7800 /**
7801 * Dialog title.
7802 *
7803 * The title of a confirmation dialog describes what a progressive action will do. The
7804 * title of an alert dialog describes which event occurred.
7805 *
7806 * @static
7807 * @inheritable
7808 * @property {jQuery|string|Function|null}
7809 */
7810 OO.ui.MessageDialog.static.title = null;
7811
7812 /**
7813 * The message displayed in the dialog body.
7814 *
7815 * A confirmation message describes the consequences of a progressive action. An alert
7816 * message describes why an event occurred.
7817 *
7818 * @static
7819 * @inheritable
7820 * @property {jQuery|string|Function|null}
7821 */
7822 OO.ui.MessageDialog.static.message = null;
7823
7824 OO.ui.MessageDialog.static.actions = [
7825 { action: 'accept', label: OO.ui.deferMsg( 'ooui-dialog-message-accept' ), flags: 'primary' },
7826 { action: 'reject', label: OO.ui.deferMsg( 'ooui-dialog-message-reject' ), flags: 'safe' }
7827 ];
7828
7829 /* Methods */
7830
7831 /**
7832 * @inheritdoc
7833 */
7834 OO.ui.MessageDialog.prototype.setManager = function ( manager ) {
7835 OO.ui.MessageDialog.super.prototype.setManager.call( this, manager );
7836
7837 // Events
7838 this.manager.connect( this, {
7839 resize: 'onResize'
7840 } );
7841
7842 return this;
7843 };
7844
7845 /**
7846 * @inheritdoc
7847 */
7848 OO.ui.MessageDialog.prototype.onActionResize = function ( action ) {
7849 this.fitActions();
7850 return OO.ui.MessageDialog.super.prototype.onActionResize.call( this, action );
7851 };
7852
7853 /**
7854 * Handle window resized events.
7855 *
7856 * @private
7857 */
7858 OO.ui.MessageDialog.prototype.onResize = function () {
7859 var dialog = this;
7860 dialog.fitActions();
7861 // Wait for CSS transition to finish and do it again :(
7862 setTimeout( function () {
7863 dialog.fitActions();
7864 }, 300 );
7865 };
7866
7867 /**
7868 * Toggle action layout between vertical and horizontal.
7869 *
7870 *
7871 * @private
7872 * @param {boolean} [value] Layout actions vertically, omit to toggle
7873 * @chainable
7874 */
7875 OO.ui.MessageDialog.prototype.toggleVerticalActionLayout = function ( value ) {
7876 value = value === undefined ? !this.verticalActionLayout : !!value;
7877
7878 if ( value !== this.verticalActionLayout ) {
7879 this.verticalActionLayout = value;
7880 this.$actions
7881 .toggleClass( 'oo-ui-messageDialog-actions-vertical', value )
7882 .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value );
7883 }
7884
7885 return this;
7886 };
7887
7888 /**
7889 * @inheritdoc
7890 */
7891 OO.ui.MessageDialog.prototype.getActionProcess = function ( action ) {
7892 if ( action ) {
7893 return new OO.ui.Process( function () {
7894 this.close( { action: action } );
7895 }, this );
7896 }
7897 return OO.ui.MessageDialog.super.prototype.getActionProcess.call( this, action );
7898 };
7899
7900 /**
7901 * @inheritdoc
7902 *
7903 * @param {Object} [data] Dialog opening data
7904 * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
7905 * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
7906 * @param {boolean} [data.verbose] Message is verbose and should be styled as a long message
7907 * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each
7908 * action item
7909 */
7910 OO.ui.MessageDialog.prototype.getSetupProcess = function ( data ) {
7911 data = data || {};
7912
7913 // Parent method
7914 return OO.ui.MessageDialog.super.prototype.getSetupProcess.call( this, data )
7915 .next( function () {
7916 this.title.setLabel(
7917 data.title !== undefined ? data.title : this.constructor.static.title
7918 );
7919 this.message.setLabel(
7920 data.message !== undefined ? data.message : this.constructor.static.message
7921 );
7922 this.message.$element.toggleClass(
7923 'oo-ui-messageDialog-message-verbose',
7924 data.verbose !== undefined ? data.verbose : this.constructor.static.verbose
7925 );
7926 }, this );
7927 };
7928
7929 /**
7930 * @inheritdoc
7931 */
7932 OO.ui.MessageDialog.prototype.getBodyHeight = function () {
7933 var bodyHeight, oldOverflow,
7934 $scrollable = this.container.$element;
7935
7936 oldOverflow = $scrollable[ 0 ].style.overflow;
7937 $scrollable[ 0 ].style.overflow = 'hidden';
7938
7939 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
7940
7941 bodyHeight = this.text.$element.outerHeight( true );
7942 $scrollable[ 0 ].style.overflow = oldOverflow;
7943
7944 return bodyHeight;
7945 };
7946
7947 /**
7948 * @inheritdoc
7949 */
7950 OO.ui.MessageDialog.prototype.setDimensions = function ( dim ) {
7951 var $scrollable = this.container.$element;
7952 OO.ui.MessageDialog.super.prototype.setDimensions.call( this, dim );
7953
7954 // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
7955 // Need to do it after transition completes (250ms), add 50ms just in case.
7956 setTimeout( function () {
7957 var oldOverflow = $scrollable[ 0 ].style.overflow;
7958 $scrollable[ 0 ].style.overflow = 'hidden';
7959
7960 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
7961
7962 $scrollable[ 0 ].style.overflow = oldOverflow;
7963 }, 300 );
7964
7965 return this;
7966 };
7967
7968 /**
7969 * @inheritdoc
7970 */
7971 OO.ui.MessageDialog.prototype.initialize = function () {
7972 // Parent method
7973 OO.ui.MessageDialog.super.prototype.initialize.call( this );
7974
7975 // Properties
7976 this.$actions = $( '<div>' );
7977 this.container = new OO.ui.PanelLayout( {
7978 scrollable: true, classes: [ 'oo-ui-messageDialog-container' ]
7979 } );
7980 this.text = new OO.ui.PanelLayout( {
7981 padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ]
7982 } );
7983 this.message = new OO.ui.LabelWidget( {
7984 classes: [ 'oo-ui-messageDialog-message' ]
7985 } );
7986
7987 // Initialization
7988 this.title.$element.addClass( 'oo-ui-messageDialog-title' );
7989 this.$content.addClass( 'oo-ui-messageDialog-content' );
7990 this.container.$element.append( this.text.$element );
7991 this.text.$element.append( this.title.$element, this.message.$element );
7992 this.$body.append( this.container.$element );
7993 this.$actions.addClass( 'oo-ui-messageDialog-actions' );
7994 this.$foot.append( this.$actions );
7995 };
7996
7997 /**
7998 * @inheritdoc
7999 */
8000 OO.ui.MessageDialog.prototype.attachActions = function () {
8001 var i, len, other, special, others;
8002
8003 // Parent method
8004 OO.ui.MessageDialog.super.prototype.attachActions.call( this );
8005
8006 special = this.actions.getSpecial();
8007 others = this.actions.getOthers();
8008 if ( special.safe ) {
8009 this.$actions.append( special.safe.$element );
8010 special.safe.toggleFramed( false );
8011 }
8012 if ( others.length ) {
8013 for ( i = 0, len = others.length; i < len; i++ ) {
8014 other = others[ i ];
8015 this.$actions.append( other.$element );
8016 other.toggleFramed( false );
8017 }
8018 }
8019 if ( special.primary ) {
8020 this.$actions.append( special.primary.$element );
8021 special.primary.toggleFramed( false );
8022 }
8023
8024 if ( !this.isOpening() ) {
8025 // If the dialog is currently opening, this will be called automatically soon.
8026 // This also calls #fitActions.
8027 this.updateSize();
8028 }
8029 };
8030
8031 /**
8032 * Fit action actions into columns or rows.
8033 *
8034 * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
8035 *
8036 * @private
8037 */
8038 OO.ui.MessageDialog.prototype.fitActions = function () {
8039 var i, len, action,
8040 previous = this.verticalActionLayout,
8041 actions = this.actions.get();
8042
8043 // Detect clipping
8044 this.toggleVerticalActionLayout( false );
8045 for ( i = 0, len = actions.length; i < len; i++ ) {
8046 action = actions[ i ];
8047 if ( action.$element.innerWidth() < action.$label.outerWidth( true ) ) {
8048 this.toggleVerticalActionLayout( true );
8049 break;
8050 }
8051 }
8052
8053 // Move the body out of the way of the foot
8054 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
8055
8056 if ( this.verticalActionLayout !== previous ) {
8057 // We changed the layout, window height might need to be updated.
8058 this.updateSize();
8059 }
8060 };
8061
8062 /**
8063 * ProcessDialog windows encapsulate a {@link OO.ui.Process process} and all of the code necessary
8064 * to complete it. If the process terminates with an error, a customizable {@link OO.ui.Error error
8065 * interface} alerts users to the trouble, permitting the user to dismiss the error and try again when
8066 * relevant. The ProcessDialog class is always extended and customized with the actions and content
8067 * required for each process.
8068 *
8069 * The process dialog box consists of a header that visually represents the ‘working’ state of long
8070 * processes with an animation. The header contains the dialog title as well as
8071 * two {@link OO.ui.ActionWidget action widgets}: a ‘safe’ action on the left (e.g., ‘Cancel’) and
8072 * a ‘primary’ action on the right (e.g., ‘Done’).
8073 *
8074 * Like other windows, the process dialog is managed by a {@link OO.ui.WindowManager window manager}.
8075 * Please see the [OOjs UI documentation on MediaWiki][1] for more information and examples.
8076 *
8077 * @example
8078 * // Example: Creating and opening a process dialog window.
8079 * function MyProcessDialog( config ) {
8080 * MyProcessDialog.super.call( this, config );
8081 * }
8082 * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
8083 *
8084 * MyProcessDialog.static.title = 'Process dialog';
8085 * MyProcessDialog.static.actions = [
8086 * { action: 'save', label: 'Done', flags: 'primary' },
8087 * { label: 'Cancel', flags: 'safe' }
8088 * ];
8089 *
8090 * MyProcessDialog.prototype.initialize = function () {
8091 * MyProcessDialog.super.prototype.initialize.apply( this, arguments );
8092 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
8093 * 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>' );
8094 * this.$body.append( this.content.$element );
8095 * };
8096 * MyProcessDialog.prototype.getActionProcess = function ( action ) {
8097 * var dialog = this;
8098 * if ( action ) {
8099 * return new OO.ui.Process( function () {
8100 * dialog.close( { action: action } );
8101 * } );
8102 * }
8103 * return MyProcessDialog.super.prototype.getActionProcess.call( this, action );
8104 * };
8105 *
8106 * var windowManager = new OO.ui.WindowManager();
8107 * $( 'body' ).append( windowManager.$element );
8108 *
8109 * var dialog = new MyProcessDialog();
8110 * windowManager.addWindows( [ dialog ] );
8111 * windowManager.openWindow( dialog );
8112 *
8113 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
8114 *
8115 * @abstract
8116 * @class
8117 * @extends OO.ui.Dialog
8118 *
8119 * @constructor
8120 * @param {Object} [config] Configuration options
8121 */
8122 OO.ui.ProcessDialog = function OoUiProcessDialog( config ) {
8123 // Parent constructor
8124 OO.ui.ProcessDialog.super.call( this, config );
8125
8126 // Initialization
8127 this.$element.addClass( 'oo-ui-processDialog' );
8128 };
8129
8130 /* Setup */
8131
8132 OO.inheritClass( OO.ui.ProcessDialog, OO.ui.Dialog );
8133
8134 /* Methods */
8135
8136 /**
8137 * Handle dismiss button click events.
8138 *
8139 * Hides errors.
8140 *
8141 * @private
8142 */
8143 OO.ui.ProcessDialog.prototype.onDismissErrorButtonClick = function () {
8144 this.hideErrors();
8145 };
8146
8147 /**
8148 * Handle retry button click events.
8149 *
8150 * Hides errors and then tries again.
8151 *
8152 * @private
8153 */
8154 OO.ui.ProcessDialog.prototype.onRetryButtonClick = function () {
8155 this.hideErrors();
8156 this.executeAction( this.currentAction );
8157 };
8158
8159 /**
8160 * @inheritdoc
8161 */
8162 OO.ui.ProcessDialog.prototype.onActionResize = function ( action ) {
8163 if ( this.actions.isSpecial( action ) ) {
8164 this.fitLabel();
8165 }
8166 return OO.ui.ProcessDialog.super.prototype.onActionResize.call( this, action );
8167 };
8168
8169 /**
8170 * @inheritdoc
8171 */
8172 OO.ui.ProcessDialog.prototype.initialize = function () {
8173 // Parent method
8174 OO.ui.ProcessDialog.super.prototype.initialize.call( this );
8175
8176 // Properties
8177 this.$navigation = $( '<div>' );
8178 this.$location = $( '<div>' );
8179 this.$safeActions = $( '<div>' );
8180 this.$primaryActions = $( '<div>' );
8181 this.$otherActions = $( '<div>' );
8182 this.dismissButton = new OO.ui.ButtonWidget( {
8183 label: OO.ui.msg( 'ooui-dialog-process-dismiss' )
8184 } );
8185 this.retryButton = new OO.ui.ButtonWidget();
8186 this.$errors = $( '<div>' );
8187 this.$errorsTitle = $( '<div>' );
8188
8189 // Events
8190 this.dismissButton.connect( this, { click: 'onDismissErrorButtonClick' } );
8191 this.retryButton.connect( this, { click: 'onRetryButtonClick' } );
8192
8193 // Initialization
8194 this.title.$element.addClass( 'oo-ui-processDialog-title' );
8195 this.$location
8196 .append( this.title.$element )
8197 .addClass( 'oo-ui-processDialog-location' );
8198 this.$safeActions.addClass( 'oo-ui-processDialog-actions-safe' );
8199 this.$primaryActions.addClass( 'oo-ui-processDialog-actions-primary' );
8200 this.$otherActions.addClass( 'oo-ui-processDialog-actions-other' );
8201 this.$errorsTitle
8202 .addClass( 'oo-ui-processDialog-errors-title' )
8203 .text( OO.ui.msg( 'ooui-dialog-process-error' ) );
8204 this.$errors
8205 .addClass( 'oo-ui-processDialog-errors oo-ui-element-hidden' )
8206 .append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element );
8207 this.$content
8208 .addClass( 'oo-ui-processDialog-content' )
8209 .append( this.$errors );
8210 this.$navigation
8211 .addClass( 'oo-ui-processDialog-navigation' )
8212 .append( this.$safeActions, this.$location, this.$primaryActions );
8213 this.$head.append( this.$navigation );
8214 this.$foot.append( this.$otherActions );
8215 };
8216
8217 /**
8218 * @inheritdoc
8219 */
8220 OO.ui.ProcessDialog.prototype.getActionWidgets = function ( actions ) {
8221 var i, len, widgets = [];
8222 for ( i = 0, len = actions.length; i < len; i++ ) {
8223 widgets.push(
8224 new OO.ui.ActionWidget( $.extend( { framed: true }, actions[ i ] ) )
8225 );
8226 }
8227 return widgets;
8228 };
8229
8230 /**
8231 * @inheritdoc
8232 */
8233 OO.ui.ProcessDialog.prototype.attachActions = function () {
8234 var i, len, other, special, others;
8235
8236 // Parent method
8237 OO.ui.ProcessDialog.super.prototype.attachActions.call( this );
8238
8239 special = this.actions.getSpecial();
8240 others = this.actions.getOthers();
8241 if ( special.primary ) {
8242 this.$primaryActions.append( special.primary.$element );
8243 }
8244 for ( i = 0, len = others.length; i < len; i++ ) {
8245 other = others[ i ];
8246 this.$otherActions.append( other.$element );
8247 }
8248 if ( special.safe ) {
8249 this.$safeActions.append( special.safe.$element );
8250 }
8251
8252 this.fitLabel();
8253 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
8254 };
8255
8256 /**
8257 * @inheritdoc
8258 */
8259 OO.ui.ProcessDialog.prototype.executeAction = function ( action ) {
8260 var process = this;
8261 return OO.ui.ProcessDialog.super.prototype.executeAction.call( this, action )
8262 .fail( function ( errors ) {
8263 process.showErrors( errors || [] );
8264 } );
8265 };
8266
8267 /**
8268 * Fit label between actions.
8269 *
8270 * @private
8271 * @chainable
8272 */
8273 OO.ui.ProcessDialog.prototype.fitLabel = function () {
8274 var safeWidth, primaryWidth, biggerWidth, labelWidth, navigationWidth, leftWidth, rightWidth;
8275
8276 safeWidth = this.$safeActions.is( ':visible' ) ? this.$safeActions.width() : 0;
8277 primaryWidth = this.$primaryActions.is( ':visible' ) ? this.$primaryActions.width() : 0;
8278 biggerWidth = Math.max( safeWidth, primaryWidth );
8279
8280 labelWidth = this.title.$element.width();
8281 // Is there a better way to calculate this?
8282 navigationWidth = OO.ui.WindowManager.static.sizes[ this.getSize() ].width - 20;
8283
8284 if ( 2 * biggerWidth + labelWidth < navigationWidth ) {
8285 // We have enough space to center the label
8286 leftWidth = rightWidth = biggerWidth;
8287 } else {
8288 // Let's hope we at least have enough space not to overlap, because we can't wrap the label…
8289 if ( this.getDir() === 'ltr' ) {
8290 leftWidth = safeWidth;
8291 rightWidth = primaryWidth;
8292 } else {
8293 leftWidth = primaryWidth;
8294 rightWidth = safeWidth;
8295 }
8296 }
8297
8298 this.$location.css( { paddingLeft: leftWidth, paddingRight: rightWidth } );
8299
8300 return this;
8301 };
8302
8303 /**
8304 * Handle errors that occurred during accept or reject processes.
8305 *
8306 * @private
8307 * @param {OO.ui.Error[]|OO.ui.Error} errors Errors to be handled
8308 */
8309 OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
8310 var i, len, $item, actions,
8311 items = [],
8312 abilities = {},
8313 recoverable = true,
8314 warning = false;
8315
8316 if ( errors instanceof OO.ui.Error ) {
8317 errors = [ errors ];
8318 }
8319
8320 for ( i = 0, len = errors.length; i < len; i++ ) {
8321 if ( !errors[ i ].isRecoverable() ) {
8322 recoverable = false;
8323 }
8324 if ( errors[ i ].isWarning() ) {
8325 warning = true;
8326 }
8327 $item = $( '<div>' )
8328 .addClass( 'oo-ui-processDialog-error' )
8329 .append( errors[ i ].getMessage() );
8330 items.push( $item[ 0 ] );
8331 }
8332 this.$errorItems = $( items );
8333 if ( recoverable ) {
8334 abilities[this.currentAction] = true;
8335 // Copy the flags from the first matching action
8336 actions = this.actions.get( { actions: this.currentAction } );
8337 if ( actions.length ) {
8338 this.retryButton.clearFlags().setFlags( actions[0].getFlags() );
8339 }
8340 } else {
8341 abilities[this.currentAction] = false;
8342 this.actions.setAbilities( abilities );
8343 }
8344 if ( warning ) {
8345 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-continue' ) );
8346 } else {
8347 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-retry' ) );
8348 }
8349 this.retryButton.toggle( recoverable );
8350 this.$errorsTitle.after( this.$errorItems );
8351 this.$errors.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 );
8352 };
8353
8354 /**
8355 * Hide errors.
8356 *
8357 * @private
8358 */
8359 OO.ui.ProcessDialog.prototype.hideErrors = function () {
8360 this.$errors.addClass( 'oo-ui-element-hidden' );
8361 if ( this.$errorItems ) {
8362 this.$errorItems.remove();
8363 this.$errorItems = null;
8364 }
8365 };
8366
8367 /**
8368 * @inheritdoc
8369 */
8370 OO.ui.ProcessDialog.prototype.getTeardownProcess = function ( data ) {
8371 // Parent method
8372 return OO.ui.ProcessDialog.super.prototype.getTeardownProcess.call( this, data )
8373 .first( function () {
8374 // Make sure to hide errors
8375 this.hideErrors();
8376 }, this );
8377 };
8378
8379 /**
8380 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
8381 * which is a widget that is specified by reference before any optional configuration settings.
8382 *
8383 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
8384 *
8385 * - **left**: The label is placed before the field-widget and aligned with the left margin.
8386 * A left-alignment is used for forms with many fields.
8387 * - **right**: The label is placed before the field-widget and aligned to the right margin.
8388 * A right-alignment is used for long but familiar forms which users tab through,
8389 * verifying the current field with a quick glance at the label.
8390 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
8391 * that users fill out from top to bottom.
8392 * - **inline**: The label is placed after the field-widget and aligned to the left.
8393 * An inline-alignment is best used with checkboxes or radio buttons.
8394 *
8395 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
8396 * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
8397 *
8398 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
8399 * @class
8400 * @extends OO.ui.Layout
8401 * @mixins OO.ui.mixin.LabelElement
8402 *
8403 * @constructor
8404 * @param {OO.ui.Widget} fieldWidget Field widget
8405 * @param {Object} [config] Configuration options
8406 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
8407 * @cfg {string} [help] Help text. When help text is specified, a help icon will appear
8408 * in the upper-right corner of the rendered field.
8409 */
8410 OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
8411 // Allow passing positional parameters inside the config object
8412 if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
8413 config = fieldWidget;
8414 fieldWidget = config.fieldWidget;
8415 }
8416
8417 var hasInputWidget = fieldWidget.constructor.static.supportsSimpleLabel;
8418
8419 // Configuration initialization
8420 config = $.extend( { align: 'left' }, config );
8421
8422 // Parent constructor
8423 OO.ui.FieldLayout.super.call( this, config );
8424
8425 // Mixin constructors
8426 OO.ui.mixin.LabelElement.call( this, config );
8427
8428 // Properties
8429 this.fieldWidget = fieldWidget;
8430 this.$field = $( '<div>' );
8431 this.$body = $( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
8432 this.align = null;
8433 if ( config.help ) {
8434 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
8435 classes: [ 'oo-ui-fieldLayout-help' ],
8436 framed: false,
8437 icon: 'info'
8438 } );
8439
8440 this.popupButtonWidget.getPopup().$body.append(
8441 $( '<div>' )
8442 .text( config.help )
8443 .addClass( 'oo-ui-fieldLayout-help-content' )
8444 );
8445 this.$help = this.popupButtonWidget.$element;
8446 } else {
8447 this.$help = $( [] );
8448 }
8449
8450 // Events
8451 if ( hasInputWidget ) {
8452 this.$label.on( 'click', this.onLabelClick.bind( this ) );
8453 }
8454 this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
8455
8456 // Initialization
8457 this.$element
8458 .addClass( 'oo-ui-fieldLayout' )
8459 .append( this.$help, this.$body );
8460 this.$body.addClass( 'oo-ui-fieldLayout-body' );
8461 this.$field
8462 .addClass( 'oo-ui-fieldLayout-field' )
8463 .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget.isDisabled() )
8464 .append( this.fieldWidget.$element );
8465
8466 this.setAlignment( config.align );
8467 };
8468
8469 /* Setup */
8470
8471 OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
8472 OO.mixinClass( OO.ui.FieldLayout, OO.ui.mixin.LabelElement );
8473
8474 /* Methods */
8475
8476 /**
8477 * Handle field disable events.
8478 *
8479 * @private
8480 * @param {boolean} value Field is disabled
8481 */
8482 OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
8483 this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
8484 };
8485
8486 /**
8487 * Handle label mouse click events.
8488 *
8489 * @private
8490 * @param {jQuery.Event} e Mouse click event
8491 */
8492 OO.ui.FieldLayout.prototype.onLabelClick = function () {
8493 this.fieldWidget.simulateLabelClick();
8494 return false;
8495 };
8496
8497 /**
8498 * Get the widget contained by the field.
8499 *
8500 * @return {OO.ui.Widget} Field widget
8501 */
8502 OO.ui.FieldLayout.prototype.getField = function () {
8503 return this.fieldWidget;
8504 };
8505
8506 /**
8507 * Set the field alignment mode.
8508 *
8509 * @private
8510 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
8511 * @chainable
8512 */
8513 OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
8514 if ( value !== this.align ) {
8515 // Default to 'left'
8516 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) {
8517 value = 'left';
8518 }
8519 // Reorder elements
8520 if ( value === 'inline' ) {
8521 this.$body.append( this.$field, this.$label );
8522 } else {
8523 this.$body.append( this.$label, this.$field );
8524 }
8525 // Set classes. The following classes can be used here:
8526 // * oo-ui-fieldLayout-align-left
8527 // * oo-ui-fieldLayout-align-right
8528 // * oo-ui-fieldLayout-align-top
8529 // * oo-ui-fieldLayout-align-inline
8530 if ( this.align ) {
8531 this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
8532 }
8533 this.$element.addClass( 'oo-ui-fieldLayout-align-' + value );
8534 this.align = value;
8535 }
8536
8537 return this;
8538 };
8539
8540 /**
8541 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
8542 * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
8543 * is required and is specified before any optional configuration settings.
8544 *
8545 * Labels can be aligned in one of four ways:
8546 *
8547 * - **left**: The label is placed before the field-widget and aligned with the left margin.
8548 * A left-alignment is used for forms with many fields.
8549 * - **right**: The label is placed before the field-widget and aligned to the right margin.
8550 * A right-alignment is used for long but familiar forms which users tab through,
8551 * verifying the current field with a quick glance at the label.
8552 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
8553 * that users fill out from top to bottom.
8554 * - **inline**: The label is placed after the field-widget and aligned to the left.
8555 * An inline-alignment is best used with checkboxes or radio buttons.
8556 *
8557 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
8558 * text is specified.
8559 *
8560 * @example
8561 * // Example of an ActionFieldLayout
8562 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
8563 * new OO.ui.TextInputWidget( {
8564 * placeholder: 'Field widget'
8565 * } ),
8566 * new OO.ui.ButtonWidget( {
8567 * label: 'Button'
8568 * } ),
8569 * {
8570 * label: 'An ActionFieldLayout. This label is aligned top',
8571 * align: 'top',
8572 * help: 'This is help text'
8573 * }
8574 * );
8575 *
8576 * $( 'body' ).append( actionFieldLayout.$element );
8577 *
8578 *
8579 * @class
8580 * @extends OO.ui.FieldLayout
8581 *
8582 * @constructor
8583 * @param {OO.ui.Widget} fieldWidget Field widget
8584 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
8585 */
8586 OO.ui.ActionFieldLayout = function OoUiActionFieldLayout( fieldWidget, buttonWidget, config ) {
8587 // Allow passing positional parameters inside the config object
8588 if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
8589 config = fieldWidget;
8590 fieldWidget = config.fieldWidget;
8591 buttonWidget = config.buttonWidget;
8592 }
8593
8594 // Parent constructor
8595 OO.ui.ActionFieldLayout.super.call( this, fieldWidget, config );
8596
8597 // Properties
8598 this.buttonWidget = buttonWidget;
8599 this.$button = $( '<div>' );
8600 this.$input = $( '<div>' );
8601
8602 // Initialization
8603 this.$element
8604 .addClass( 'oo-ui-actionFieldLayout' );
8605 this.$button
8606 .addClass( 'oo-ui-actionFieldLayout-button' )
8607 .append( this.buttonWidget.$element );
8608 this.$input
8609 .addClass( 'oo-ui-actionFieldLayout-input' )
8610 .append( this.fieldWidget.$element );
8611 this.$field
8612 .append( this.$input, this.$button );
8613 };
8614
8615 /* Setup */
8616
8617 OO.inheritClass( OO.ui.ActionFieldLayout, OO.ui.FieldLayout );
8618
8619 /**
8620 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
8621 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
8622 * configured with a label as well. For more information and examples,
8623 * please see the [OOjs UI documentation on MediaWiki][1].
8624 *
8625 * @example
8626 * // Example of a fieldset layout
8627 * var input1 = new OO.ui.TextInputWidget( {
8628 * placeholder: 'A text input field'
8629 * } );
8630 *
8631 * var input2 = new OO.ui.TextInputWidget( {
8632 * placeholder: 'A text input field'
8633 * } );
8634 *
8635 * var fieldset = new OO.ui.FieldsetLayout( {
8636 * label: 'Example of a fieldset layout'
8637 * } );
8638 *
8639 * fieldset.addItems( [
8640 * new OO.ui.FieldLayout( input1, {
8641 * label: 'Field One'
8642 * } ),
8643 * new OO.ui.FieldLayout( input2, {
8644 * label: 'Field Two'
8645 * } )
8646 * ] );
8647 * $( 'body' ).append( fieldset.$element );
8648 *
8649 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
8650 *
8651 * @class
8652 * @extends OO.ui.Layout
8653 * @mixins OO.ui.mixin.IconElement
8654 * @mixins OO.ui.mixin.LabelElement
8655 * @mixins OO.ui.mixin.GroupElement
8656 *
8657 * @constructor
8658 * @param {Object} [config] Configuration options
8659 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
8660 */
8661 OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
8662 // Configuration initialization
8663 config = config || {};
8664
8665 // Parent constructor
8666 OO.ui.FieldsetLayout.super.call( this, config );
8667
8668 // Mixin constructors
8669 OO.ui.mixin.IconElement.call( this, config );
8670 OO.ui.mixin.LabelElement.call( this, config );
8671 OO.ui.mixin.GroupElement.call( this, config );
8672
8673 if ( config.help ) {
8674 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
8675 classes: [ 'oo-ui-fieldsetLayout-help' ],
8676 framed: false,
8677 icon: 'info'
8678 } );
8679
8680 this.popupButtonWidget.getPopup().$body.append(
8681 $( '<div>' )
8682 .text( config.help )
8683 .addClass( 'oo-ui-fieldsetLayout-help-content' )
8684 );
8685 this.$help = this.popupButtonWidget.$element;
8686 } else {
8687 this.$help = $( [] );
8688 }
8689
8690 // Initialization
8691 this.$element
8692 .addClass( 'oo-ui-fieldsetLayout' )
8693 .prepend( this.$help, this.$icon, this.$label, this.$group );
8694 if ( Array.isArray( config.items ) ) {
8695 this.addItems( config.items );
8696 }
8697 };
8698
8699 /* Setup */
8700
8701 OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
8702 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.IconElement );
8703 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.LabelElement );
8704 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.GroupElement );
8705
8706 /**
8707 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
8708 * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
8709 * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
8710 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
8711 *
8712 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
8713 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
8714 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
8715 * some fancier controls. Some controls have both regular and InputWidget variants, for example
8716 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
8717 * often have simplified APIs to match the capabilities of HTML forms.
8718 * See the [OOjs UI Inputs documentation on MediaWiki] [2] for more information about InputWidgets.
8719 *
8720 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Forms
8721 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
8722 *
8723 * @example
8724 * // Example of a form layout that wraps a fieldset layout
8725 * var input1 = new OO.ui.TextInputWidget( {
8726 * placeholder: 'Username'
8727 * } );
8728 * var input2 = new OO.ui.TextInputWidget( {
8729 * placeholder: 'Password',
8730 * type: 'password'
8731 * } );
8732 * var submit = new OO.ui.ButtonInputWidget( {
8733 * label: 'Submit'
8734 * } );
8735 *
8736 * var fieldset = new OO.ui.FieldsetLayout( {
8737 * label: 'A form layout'
8738 * } );
8739 * fieldset.addItems( [
8740 * new OO.ui.FieldLayout( input1, {
8741 * label: 'Username',
8742 * align: 'top'
8743 * } ),
8744 * new OO.ui.FieldLayout( input2, {
8745 * label: 'Password',
8746 * align: 'top'
8747 * } ),
8748 * new OO.ui.FieldLayout( submit )
8749 * ] );
8750 * var form = new OO.ui.FormLayout( {
8751 * items: [ fieldset ],
8752 * action: '/api/formhandler',
8753 * method: 'get'
8754 * } )
8755 * $( 'body' ).append( form.$element );
8756 *
8757 * @class
8758 * @extends OO.ui.Layout
8759 * @mixins OO.ui.mixin.GroupElement
8760 *
8761 * @constructor
8762 * @param {Object} [config] Configuration options
8763 * @cfg {string} [method] HTML form `method` attribute
8764 * @cfg {string} [action] HTML form `action` attribute
8765 * @cfg {string} [enctype] HTML form `enctype` attribute
8766 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
8767 */
8768 OO.ui.FormLayout = function OoUiFormLayout( config ) {
8769 // Configuration initialization
8770 config = config || {};
8771
8772 // Parent constructor
8773 OO.ui.FormLayout.super.call( this, config );
8774
8775 // Mixin constructors
8776 OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
8777
8778 // Events
8779 this.$element.on( 'submit', this.onFormSubmit.bind( this ) );
8780
8781 // Initialization
8782 this.$element
8783 .addClass( 'oo-ui-formLayout' )
8784 .attr( {
8785 method: config.method,
8786 action: config.action,
8787 enctype: config.enctype
8788 } );
8789 if ( Array.isArray( config.items ) ) {
8790 this.addItems( config.items );
8791 }
8792 };
8793
8794 /* Setup */
8795
8796 OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout );
8797 OO.mixinClass( OO.ui.FormLayout, OO.ui.mixin.GroupElement );
8798
8799 /* Events */
8800
8801 /**
8802 * A 'submit' event is emitted when the form is submitted.
8803 *
8804 * @event submit
8805 */
8806
8807 /* Static Properties */
8808
8809 OO.ui.FormLayout.static.tagName = 'form';
8810
8811 /* Methods */
8812
8813 /**
8814 * Handle form submit events.
8815 *
8816 * @private
8817 * @param {jQuery.Event} e Submit event
8818 * @fires submit
8819 */
8820 OO.ui.FormLayout.prototype.onFormSubmit = function () {
8821 if ( this.emit( 'submit' ) ) {
8822 return false;
8823 }
8824 };
8825
8826 /**
8827 * 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)
8828 * and its size is customized with the #menuSize config. The content area will fill all remaining space.
8829 *
8830 * @example
8831 * var menuLayout = new OO.ui.MenuLayout( {
8832 * position: 'top'
8833 * } ),
8834 * menuPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
8835 * contentPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
8836 * select = new OO.ui.SelectWidget( {
8837 * items: [
8838 * new OO.ui.OptionWidget( {
8839 * data: 'before',
8840 * label: 'Before',
8841 * } ),
8842 * new OO.ui.OptionWidget( {
8843 * data: 'after',
8844 * label: 'After',
8845 * } ),
8846 * new OO.ui.OptionWidget( {
8847 * data: 'top',
8848 * label: 'Top',
8849 * } ),
8850 * new OO.ui.OptionWidget( {
8851 * data: 'bottom',
8852 * label: 'Bottom',
8853 * } )
8854 * ]
8855 * } ).on( 'select', function ( item ) {
8856 * menuLayout.setMenuPosition( item.getData() );
8857 * } );
8858 *
8859 * menuLayout.$menu.append(
8860 * menuPanel.$element.append( '<b>Menu panel</b>', select.$element )
8861 * );
8862 * menuLayout.$content.append(
8863 * contentPanel.$element.append( '<b>Content panel</b>', '<p>Note that the menu is positioned relative to the content panel: top, bottom, after, before.</p>')
8864 * );
8865 * $( 'body' ).append( menuLayout.$element );
8866 *
8867 * If menu size needs to be overridden, it can be accomplished using CSS similar to the snippet
8868 * below. MenuLayout's CSS will override the appropriate values with 'auto' or '0' to display the
8869 * menu correctly. If `menuPosition` is known beforehand, CSS rules corresponding to other positions
8870 * may be omitted.
8871 *
8872 * .oo-ui-menuLayout-menu {
8873 * height: 200px;
8874 * width: 200px;
8875 * }
8876 * .oo-ui-menuLayout-content {
8877 * top: 200px;
8878 * left: 200px;
8879 * right: 200px;
8880 * bottom: 200px;
8881 * }
8882 *
8883 * @class
8884 * @extends OO.ui.Layout
8885 *
8886 * @constructor
8887 * @param {Object} [config] Configuration options
8888 * @cfg {boolean} [showMenu=true] Show menu
8889 * @cfg {string} [menuPosition='before'] Position of menu: `top`, `after`, `bottom` or `before`
8890 */
8891 OO.ui.MenuLayout = function OoUiMenuLayout( config ) {
8892 // Configuration initialization
8893 config = $.extend( {
8894 showMenu: true,
8895 menuPosition: 'before'
8896 }, config );
8897
8898 // Parent constructor
8899 OO.ui.MenuLayout.super.call( this, config );
8900
8901 /**
8902 * Menu DOM node
8903 *
8904 * @property {jQuery}
8905 */
8906 this.$menu = $( '<div>' );
8907 /**
8908 * Content DOM node
8909 *
8910 * @property {jQuery}
8911 */
8912 this.$content = $( '<div>' );
8913
8914 // Initialization
8915 this.$menu
8916 .addClass( 'oo-ui-menuLayout-menu' );
8917 this.$content.addClass( 'oo-ui-menuLayout-content' );
8918 this.$element
8919 .addClass( 'oo-ui-menuLayout' )
8920 .append( this.$content, this.$menu );
8921 this.setMenuPosition( config.menuPosition );
8922 this.toggleMenu( config.showMenu );
8923 };
8924
8925 /* Setup */
8926
8927 OO.inheritClass( OO.ui.MenuLayout, OO.ui.Layout );
8928
8929 /* Methods */
8930
8931 /**
8932 * Toggle menu.
8933 *
8934 * @param {boolean} showMenu Show menu, omit to toggle
8935 * @chainable
8936 */
8937 OO.ui.MenuLayout.prototype.toggleMenu = function ( showMenu ) {
8938 showMenu = showMenu === undefined ? !this.showMenu : !!showMenu;
8939
8940 if ( this.showMenu !== showMenu ) {
8941 this.showMenu = showMenu;
8942 this.$element
8943 .toggleClass( 'oo-ui-menuLayout-showMenu', this.showMenu )
8944 .toggleClass( 'oo-ui-menuLayout-hideMenu', !this.showMenu );
8945 }
8946
8947 return this;
8948 };
8949
8950 /**
8951 * Check if menu is visible
8952 *
8953 * @return {boolean} Menu is visible
8954 */
8955 OO.ui.MenuLayout.prototype.isMenuVisible = function () {
8956 return this.showMenu;
8957 };
8958
8959 /**
8960 * Set menu position.
8961 *
8962 * @param {string} position Position of menu, either `top`, `after`, `bottom` or `before`
8963 * @throws {Error} If position value is not supported
8964 * @chainable
8965 */
8966 OO.ui.MenuLayout.prototype.setMenuPosition = function ( position ) {
8967 this.$element.removeClass( 'oo-ui-menuLayout-' + this.menuPosition );
8968 this.menuPosition = position;
8969 this.$element.addClass( 'oo-ui-menuLayout-' + position );
8970
8971 return this;
8972 };
8973
8974 /**
8975 * Get menu position.
8976 *
8977 * @return {string} Menu position
8978 */
8979 OO.ui.MenuLayout.prototype.getMenuPosition = function () {
8980 return this.menuPosition;
8981 };
8982
8983 /**
8984 * BookletLayouts contain {@link OO.ui.PageLayout page layouts} as well as
8985 * an {@link OO.ui.OutlineSelectWidget outline} that allows users to easily navigate
8986 * through the pages and select which one to display. By default, only one page is
8987 * displayed at a time and the outline is hidden. When a user navigates to a new page,
8988 * the booklet layout automatically focuses on the first focusable element, unless the
8989 * default setting is changed. Optionally, booklets can be configured to show
8990 * {@link OO.ui.OutlineControlsWidget controls} for adding, moving, and removing items.
8991 *
8992 * @example
8993 * // Example of a BookletLayout that contains two PageLayouts.
8994 *
8995 * function PageOneLayout( name, config ) {
8996 * PageOneLayout.super.call( this, name, config );
8997 * this.$element.append( '<p>First page</p><p>(This booklet has an outline, displayed on the left)</p>' );
8998 * }
8999 * OO.inheritClass( PageOneLayout, OO.ui.PageLayout );
9000 * PageOneLayout.prototype.setupOutlineItem = function () {
9001 * this.outlineItem.setLabel( 'Page One' );
9002 * };
9003 *
9004 * function PageTwoLayout( name, config ) {
9005 * PageTwoLayout.super.call( this, name, config );
9006 * this.$element.append( '<p>Second page</p>' );
9007 * }
9008 * OO.inheritClass( PageTwoLayout, OO.ui.PageLayout );
9009 * PageTwoLayout.prototype.setupOutlineItem = function () {
9010 * this.outlineItem.setLabel( 'Page Two' );
9011 * };
9012 *
9013 * var page1 = new PageOneLayout( 'one' ),
9014 * page2 = new PageTwoLayout( 'two' );
9015 *
9016 * var booklet = new OO.ui.BookletLayout( {
9017 * outlined: true
9018 * } );
9019 *
9020 * booklet.addPages ( [ page1, page2 ] );
9021 * $( 'body' ).append( booklet.$element );
9022 *
9023 * @class
9024 * @extends OO.ui.MenuLayout
9025 *
9026 * @constructor
9027 * @param {Object} [config] Configuration options
9028 * @cfg {boolean} [continuous=false] Show all pages, one after another
9029 * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new page is displayed.
9030 * @cfg {boolean} [outlined=false] Show the outline. The outline is used to navigate through the pages of the booklet.
9031 * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
9032 */
9033 OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
9034 // Configuration initialization
9035 config = config || {};
9036
9037 // Parent constructor
9038 OO.ui.BookletLayout.super.call( this, config );
9039
9040 // Properties
9041 this.currentPageName = null;
9042 this.pages = {};
9043 this.ignoreFocus = false;
9044 this.stackLayout = new OO.ui.StackLayout( { continuous: !!config.continuous } );
9045 this.$content.append( this.stackLayout.$element );
9046 this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
9047 this.outlineVisible = false;
9048 this.outlined = !!config.outlined;
9049 if ( this.outlined ) {
9050 this.editable = !!config.editable;
9051 this.outlineControlsWidget = null;
9052 this.outlineSelectWidget = new OO.ui.OutlineSelectWidget();
9053 this.outlinePanel = new OO.ui.PanelLayout( { scrollable: true } );
9054 this.$menu.append( this.outlinePanel.$element );
9055 this.outlineVisible = true;
9056 if ( this.editable ) {
9057 this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
9058 this.outlineSelectWidget
9059 );
9060 }
9061 }
9062 this.toggleMenu( this.outlined );
9063
9064 // Events
9065 this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
9066 if ( this.outlined ) {
9067 this.outlineSelectWidget.connect( this, { select: 'onOutlineSelectWidgetSelect' } );
9068 }
9069 if ( this.autoFocus ) {
9070 // Event 'focus' does not bubble, but 'focusin' does
9071 this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
9072 }
9073
9074 // Initialization
9075 this.$element.addClass( 'oo-ui-bookletLayout' );
9076 this.stackLayout.$element.addClass( 'oo-ui-bookletLayout-stackLayout' );
9077 if ( this.outlined ) {
9078 this.outlinePanel.$element
9079 .addClass( 'oo-ui-bookletLayout-outlinePanel' )
9080 .append( this.outlineSelectWidget.$element );
9081 if ( this.editable ) {
9082 this.outlinePanel.$element
9083 .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
9084 .append( this.outlineControlsWidget.$element );
9085 }
9086 }
9087 };
9088
9089 /* Setup */
9090
9091 OO.inheritClass( OO.ui.BookletLayout, OO.ui.MenuLayout );
9092
9093 /* Events */
9094
9095 /**
9096 * A 'set' event is emitted when a page is {@link #setPage set} to be displayed by the booklet layout.
9097 * @event set
9098 * @param {OO.ui.PageLayout} page Current page
9099 */
9100
9101 /**
9102 * An 'add' event is emitted when pages are {@link #addPages added} to the booklet layout.
9103 *
9104 * @event add
9105 * @param {OO.ui.PageLayout[]} page Added pages
9106 * @param {number} index Index pages were added at
9107 */
9108
9109 /**
9110 * A 'remove' event is emitted when pages are {@link #clearPages cleared} or
9111 * {@link #removePages removed} from the booklet.
9112 *
9113 * @event remove
9114 * @param {OO.ui.PageLayout[]} pages Removed pages
9115 */
9116
9117 /* Methods */
9118
9119 /**
9120 * Handle stack layout focus.
9121 *
9122 * @private
9123 * @param {jQuery.Event} e Focusin event
9124 */
9125 OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) {
9126 var name, $target;
9127
9128 // Find the page that an element was focused within
9129 $target = $( e.target ).closest( '.oo-ui-pageLayout' );
9130 for ( name in this.pages ) {
9131 // Check for page match, exclude current page to find only page changes
9132 if ( this.pages[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentPageName ) {
9133 this.setPage( name );
9134 break;
9135 }
9136 }
9137 };
9138
9139 /**
9140 * Handle stack layout set events.
9141 *
9142 * @private
9143 * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
9144 */
9145 OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
9146 var layout = this;
9147 if ( page ) {
9148 page.scrollElementIntoView( { complete: function () {
9149 if ( layout.autoFocus ) {
9150 layout.focus();
9151 }
9152 } } );
9153 }
9154 };
9155
9156 /**
9157 * Focus the first input in the current page.
9158 *
9159 * If no page is selected, the first selectable page will be selected.
9160 * If the focus is already in an element on the current page, nothing will happen.
9161 * @param {number} [itemIndex] A specific item to focus on
9162 */
9163 OO.ui.BookletLayout.prototype.focus = function ( itemIndex ) {
9164 var $input, page,
9165 items = this.stackLayout.getItems();
9166
9167 if ( itemIndex !== undefined && items[ itemIndex ] ) {
9168 page = items[ itemIndex ];
9169 } else {
9170 page = this.stackLayout.getCurrentItem();
9171 }
9172
9173 if ( !page && this.outlined ) {
9174 this.selectFirstSelectablePage();
9175 page = this.stackLayout.getCurrentItem();
9176 }
9177 if ( !page ) {
9178 return;
9179 }
9180 // Only change the focus if is not already in the current page
9181 if ( !page.$element.find( ':focus' ).length ) {
9182 $input = page.$element.find( ':input:first' );
9183 if ( $input.length ) {
9184 $input[ 0 ].focus();
9185 }
9186 }
9187 };
9188
9189 /**
9190 * Find the first focusable input in the booklet layout and focus
9191 * on it.
9192 */
9193 OO.ui.BookletLayout.prototype.focusFirstFocusable = function () {
9194 var i, len,
9195 found = false,
9196 items = this.stackLayout.getItems(),
9197 checkAndFocus = function () {
9198 if ( OO.ui.isFocusableElement( $( this ) ) ) {
9199 $( this ).focus();
9200 found = true;
9201 return false;
9202 }
9203 };
9204
9205 for ( i = 0, len = items.length; i < len; i++ ) {
9206 if ( found ) {
9207 break;
9208 }
9209 // Find all potentially focusable elements in the item
9210 // and check if they are focusable
9211 items[i].$element
9212 .find( 'input, select, textarea, button, object' )
9213 /* jshint loopfunc:true */
9214 .each( checkAndFocus );
9215 }
9216 };
9217
9218 /**
9219 * Handle outline widget select events.
9220 *
9221 * @private
9222 * @param {OO.ui.OptionWidget|null} item Selected item
9223 */
9224 OO.ui.BookletLayout.prototype.onOutlineSelectWidgetSelect = function ( item ) {
9225 if ( item ) {
9226 this.setPage( item.getData() );
9227 }
9228 };
9229
9230 /**
9231 * Check if booklet has an outline.
9232 *
9233 * @return {boolean} Booklet has an outline
9234 */
9235 OO.ui.BookletLayout.prototype.isOutlined = function () {
9236 return this.outlined;
9237 };
9238
9239 /**
9240 * Check if booklet has editing controls.
9241 *
9242 * @return {boolean} Booklet is editable
9243 */
9244 OO.ui.BookletLayout.prototype.isEditable = function () {
9245 return this.editable;
9246 };
9247
9248 /**
9249 * Check if booklet has a visible outline.
9250 *
9251 * @return {boolean} Outline is visible
9252 */
9253 OO.ui.BookletLayout.prototype.isOutlineVisible = function () {
9254 return this.outlined && this.outlineVisible;
9255 };
9256
9257 /**
9258 * Hide or show the outline.
9259 *
9260 * @param {boolean} [show] Show outline, omit to invert current state
9261 * @chainable
9262 */
9263 OO.ui.BookletLayout.prototype.toggleOutline = function ( show ) {
9264 if ( this.outlined ) {
9265 show = show === undefined ? !this.outlineVisible : !!show;
9266 this.outlineVisible = show;
9267 this.toggleMenu( show );
9268 }
9269
9270 return this;
9271 };
9272
9273 /**
9274 * Get the page closest to the specified page.
9275 *
9276 * @param {OO.ui.PageLayout} page Page to use as a reference point
9277 * @return {OO.ui.PageLayout|null} Page closest to the specified page
9278 */
9279 OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) {
9280 var next, prev, level,
9281 pages = this.stackLayout.getItems(),
9282 index = $.inArray( page, pages );
9283
9284 if ( index !== -1 ) {
9285 next = pages[ index + 1 ];
9286 prev = pages[ index - 1 ];
9287 // Prefer adjacent pages at the same level
9288 if ( this.outlined ) {
9289 level = this.outlineSelectWidget.getItemFromData( page.getName() ).getLevel();
9290 if (
9291 prev &&
9292 level === this.outlineSelectWidget.getItemFromData( prev.getName() ).getLevel()
9293 ) {
9294 return prev;
9295 }
9296 if (
9297 next &&
9298 level === this.outlineSelectWidget.getItemFromData( next.getName() ).getLevel()
9299 ) {
9300 return next;
9301 }
9302 }
9303 }
9304 return prev || next || null;
9305 };
9306
9307 /**
9308 * Get the outline widget.
9309 *
9310 * If the booklet is not outlined, the method will return `null`.
9311 *
9312 * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if the booklet is not outlined
9313 */
9314 OO.ui.BookletLayout.prototype.getOutline = function () {
9315 return this.outlineSelectWidget;
9316 };
9317
9318 /**
9319 * Get the outline controls widget.
9320 *
9321 * If the outline is not editable, the method will return `null`.
9322 *
9323 * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
9324 */
9325 OO.ui.BookletLayout.prototype.getOutlineControls = function () {
9326 return this.outlineControlsWidget;
9327 };
9328
9329 /**
9330 * Get a page by its symbolic name.
9331 *
9332 * @param {string} name Symbolic name of page
9333 * @return {OO.ui.PageLayout|undefined} Page, if found
9334 */
9335 OO.ui.BookletLayout.prototype.getPage = function ( name ) {
9336 return this.pages[ name ];
9337 };
9338
9339 /**
9340 * Get the current page.
9341 *
9342 * @return {OO.ui.PageLayout|undefined} Current page, if found
9343 */
9344 OO.ui.BookletLayout.prototype.getCurrentPage = function () {
9345 var name = this.getCurrentPageName();
9346 return name ? this.getPage( name ) : undefined;
9347 };
9348
9349 /**
9350 * Get the symbolic name of the current page.
9351 *
9352 * @return {string|null} Symbolic name of the current page
9353 */
9354 OO.ui.BookletLayout.prototype.getCurrentPageName = function () {
9355 return this.currentPageName;
9356 };
9357
9358 /**
9359 * Add pages to the booklet layout
9360 *
9361 * When pages are added with the same names as existing pages, the existing pages will be
9362 * automatically removed before the new pages are added.
9363 *
9364 * @param {OO.ui.PageLayout[]} pages Pages to add
9365 * @param {number} index Index of the insertion point
9366 * @fires add
9367 * @chainable
9368 */
9369 OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) {
9370 var i, len, name, page, item, currentIndex,
9371 stackLayoutPages = this.stackLayout.getItems(),
9372 remove = [],
9373 items = [];
9374
9375 // Remove pages with same names
9376 for ( i = 0, len = pages.length; i < len; i++ ) {
9377 page = pages[ i ];
9378 name = page.getName();
9379
9380 if ( Object.prototype.hasOwnProperty.call( this.pages, name ) ) {
9381 // Correct the insertion index
9382 currentIndex = $.inArray( this.pages[ name ], stackLayoutPages );
9383 if ( currentIndex !== -1 && currentIndex + 1 < index ) {
9384 index--;
9385 }
9386 remove.push( this.pages[ name ] );
9387 }
9388 }
9389 if ( remove.length ) {
9390 this.removePages( remove );
9391 }
9392
9393 // Add new pages
9394 for ( i = 0, len = pages.length; i < len; i++ ) {
9395 page = pages[ i ];
9396 name = page.getName();
9397 this.pages[ page.getName() ] = page;
9398 if ( this.outlined ) {
9399 item = new OO.ui.OutlineOptionWidget( { data: name } );
9400 page.setOutlineItem( item );
9401 items.push( item );
9402 }
9403 }
9404
9405 if ( this.outlined && items.length ) {
9406 this.outlineSelectWidget.addItems( items, index );
9407 this.selectFirstSelectablePage();
9408 }
9409 this.stackLayout.addItems( pages, index );
9410 this.emit( 'add', pages, index );
9411
9412 return this;
9413 };
9414
9415 /**
9416 * Remove the specified pages from the booklet layout.
9417 *
9418 * To remove all pages from the booklet, you may wish to use the #clearPages method instead.
9419 *
9420 * @param {OO.ui.PageLayout[]} pages An array of pages to remove
9421 * @fires remove
9422 * @chainable
9423 */
9424 OO.ui.BookletLayout.prototype.removePages = function ( pages ) {
9425 var i, len, name, page,
9426 items = [];
9427
9428 for ( i = 0, len = pages.length; i < len; i++ ) {
9429 page = pages[ i ];
9430 name = page.getName();
9431 delete this.pages[ name ];
9432 if ( this.outlined ) {
9433 items.push( this.outlineSelectWidget.getItemFromData( name ) );
9434 page.setOutlineItem( null );
9435 }
9436 }
9437 if ( this.outlined && items.length ) {
9438 this.outlineSelectWidget.removeItems( items );
9439 this.selectFirstSelectablePage();
9440 }
9441 this.stackLayout.removeItems( pages );
9442 this.emit( 'remove', pages );
9443
9444 return this;
9445 };
9446
9447 /**
9448 * Clear all pages from the booklet layout.
9449 *
9450 * To remove only a subset of pages from the booklet, use the #removePages method.
9451 *
9452 * @fires remove
9453 * @chainable
9454 */
9455 OO.ui.BookletLayout.prototype.clearPages = function () {
9456 var i, len,
9457 pages = this.stackLayout.getItems();
9458
9459 this.pages = {};
9460 this.currentPageName = null;
9461 if ( this.outlined ) {
9462 this.outlineSelectWidget.clearItems();
9463 for ( i = 0, len = pages.length; i < len; i++ ) {
9464 pages[ i ].setOutlineItem( null );
9465 }
9466 }
9467 this.stackLayout.clearItems();
9468
9469 this.emit( 'remove', pages );
9470
9471 return this;
9472 };
9473
9474 /**
9475 * Set the current page by symbolic name.
9476 *
9477 * @fires set
9478 * @param {string} name Symbolic name of page
9479 */
9480 OO.ui.BookletLayout.prototype.setPage = function ( name ) {
9481 var selectedItem,
9482 $focused,
9483 page = this.pages[ name ];
9484
9485 if ( name !== this.currentPageName ) {
9486 if ( this.outlined ) {
9487 selectedItem = this.outlineSelectWidget.getSelectedItem();
9488 if ( selectedItem && selectedItem.getData() !== name ) {
9489 this.outlineSelectWidget.selectItemByData( name );
9490 }
9491 }
9492 if ( page ) {
9493 if ( this.currentPageName && this.pages[ this.currentPageName ] ) {
9494 this.pages[ this.currentPageName ].setActive( false );
9495 // Blur anything focused if the next page doesn't have anything focusable - this
9496 // is not needed if the next page has something focusable because once it is focused
9497 // this blur happens automatically
9498 if ( this.autoFocus && !page.$element.find( ':input' ).length ) {
9499 $focused = this.pages[ this.currentPageName ].$element.find( ':focus' );
9500 if ( $focused.length ) {
9501 $focused[ 0 ].blur();
9502 }
9503 }
9504 }
9505 this.currentPageName = name;
9506 this.stackLayout.setItem( page );
9507 page.setActive( true );
9508 this.emit( 'set', page );
9509 }
9510 }
9511 };
9512
9513 /**
9514 * Select the first selectable page.
9515 *
9516 * @chainable
9517 */
9518 OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () {
9519 if ( !this.outlineSelectWidget.getSelectedItem() ) {
9520 this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getFirstSelectableItem() );
9521 }
9522
9523 return this;
9524 };
9525
9526 /**
9527 * IndexLayouts contain {@link OO.ui.CardLayout card layouts} as well as
9528 * {@link OO.ui.TabSelectWidget tabs} that allow users to easily navigate through the cards and
9529 * select which one to display. By default, only one card is displayed at a time. When a user
9530 * navigates to a new card, the index layout automatically focuses on the first focusable element,
9531 * unless the default setting is changed.
9532 *
9533 * TODO: This class is similar to BookletLayout, we may want to refactor to reduce duplication
9534 *
9535 * @example
9536 * // Example of a IndexLayout that contains two CardLayouts.
9537 *
9538 * function CardOneLayout( name, config ) {
9539 * CardOneLayout.super.call( this, name, config );
9540 * this.$element.append( '<p>First card</p>' );
9541 * }
9542 * OO.inheritClass( CardOneLayout, OO.ui.CardLayout );
9543 * CardOneLayout.prototype.setupTabItem = function () {
9544 * this.tabItem.setLabel( 'Card One' );
9545 * };
9546 *
9547 * function CardTwoLayout( name, config ) {
9548 * CardTwoLayout.super.call( this, name, config );
9549 * this.$element.append( '<p>Second card</p>' );
9550 * }
9551 * OO.inheritClass( CardTwoLayout, OO.ui.CardLayout );
9552 * CardTwoLayout.prototype.setupTabItem = function () {
9553 * this.tabItem.setLabel( 'Card Two' );
9554 * };
9555 *
9556 * var card1 = new CardOneLayout( 'one' ),
9557 * card2 = new CardTwoLayout( 'two' );
9558 *
9559 * var index = new OO.ui.IndexLayout();
9560 *
9561 * index.addCards ( [ card1, card2 ] );
9562 * $( 'body' ).append( index.$element );
9563 *
9564 * @class
9565 * @extends OO.ui.MenuLayout
9566 *
9567 * @constructor
9568 * @param {Object} [config] Configuration options
9569 * @cfg {boolean} [continuous=false] Show all cards, one after another
9570 * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new card is displayed.
9571 */
9572 OO.ui.IndexLayout = function OoUiIndexLayout( config ) {
9573 // Configuration initialization
9574 config = $.extend( {}, config, { menuPosition: 'top' } );
9575
9576 // Parent constructor
9577 OO.ui.IndexLayout.super.call( this, config );
9578
9579 // Properties
9580 this.currentCardName = null;
9581 this.cards = {};
9582 this.ignoreFocus = false;
9583 this.stackLayout = new OO.ui.StackLayout( { continuous: !!config.continuous } );
9584 this.$content.append( this.stackLayout.$element );
9585 this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
9586
9587 this.tabSelectWidget = new OO.ui.TabSelectWidget();
9588 this.tabPanel = new OO.ui.PanelLayout();
9589 this.$menu.append( this.tabPanel.$element );
9590
9591 this.toggleMenu( true );
9592
9593 // Events
9594 this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
9595 this.tabSelectWidget.connect( this, { select: 'onTabSelectWidgetSelect' } );
9596 if ( this.autoFocus ) {
9597 // Event 'focus' does not bubble, but 'focusin' does
9598 this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
9599 }
9600
9601 // Initialization
9602 this.$element.addClass( 'oo-ui-indexLayout' );
9603 this.stackLayout.$element.addClass( 'oo-ui-indexLayout-stackLayout' );
9604 this.tabPanel.$element
9605 .addClass( 'oo-ui-indexLayout-tabPanel' )
9606 .append( this.tabSelectWidget.$element );
9607 };
9608
9609 /* Setup */
9610
9611 OO.inheritClass( OO.ui.IndexLayout, OO.ui.MenuLayout );
9612
9613 /* Events */
9614
9615 /**
9616 * A 'set' event is emitted when a card is {@link #setCard set} to be displayed by the index layout.
9617 * @event set
9618 * @param {OO.ui.CardLayout} card Current card
9619 */
9620
9621 /**
9622 * An 'add' event is emitted when cards are {@link #addCards added} to the index layout.
9623 *
9624 * @event add
9625 * @param {OO.ui.CardLayout[]} card Added cards
9626 * @param {number} index Index cards were added at
9627 */
9628
9629 /**
9630 * A 'remove' event is emitted when cards are {@link #clearCards cleared} or
9631 * {@link #removeCards removed} from the index.
9632 *
9633 * @event remove
9634 * @param {OO.ui.CardLayout[]} cards Removed cards
9635 */
9636
9637 /* Methods */
9638
9639 /**
9640 * Handle stack layout focus.
9641 *
9642 * @private
9643 * @param {jQuery.Event} e Focusin event
9644 */
9645 OO.ui.IndexLayout.prototype.onStackLayoutFocus = function ( e ) {
9646 var name, $target;
9647
9648 // Find the card that an element was focused within
9649 $target = $( e.target ).closest( '.oo-ui-cardLayout' );
9650 for ( name in this.cards ) {
9651 // Check for card match, exclude current card to find only card changes
9652 if ( this.cards[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentCardName ) {
9653 this.setCard( name );
9654 break;
9655 }
9656 }
9657 };
9658
9659 /**
9660 * Handle stack layout set events.
9661 *
9662 * @private
9663 * @param {OO.ui.PanelLayout|null} card The card panel that is now the current panel
9664 */
9665 OO.ui.IndexLayout.prototype.onStackLayoutSet = function ( card ) {
9666 var layout = this;
9667 if ( card ) {
9668 card.scrollElementIntoView( { complete: function () {
9669 if ( layout.autoFocus ) {
9670 layout.focus();
9671 }
9672 } } );
9673 }
9674 };
9675
9676 /**
9677 * Focus the first input in the current card.
9678 *
9679 * If no card is selected, the first selectable card will be selected.
9680 * If the focus is already in an element on the current card, nothing will happen.
9681 * @param {number} [itemIndex] A specific item to focus on
9682 */
9683 OO.ui.IndexLayout.prototype.focus = function ( itemIndex ) {
9684 var $input, card,
9685 items = this.stackLayout.getItems();
9686
9687 if ( itemIndex !== undefined && items[ itemIndex ] ) {
9688 card = items[ itemIndex ];
9689 } else {
9690 card = this.stackLayout.getCurrentItem();
9691 }
9692
9693 if ( !card ) {
9694 this.selectFirstSelectableCard();
9695 card = this.stackLayout.getCurrentItem();
9696 }
9697 if ( !card ) {
9698 return;
9699 }
9700 // Only change the focus if is not already in the current card
9701 if ( !card.$element.find( ':focus' ).length ) {
9702 $input = card.$element.find( ':input:first' );
9703 if ( $input.length ) {
9704 $input[ 0 ].focus();
9705 }
9706 }
9707 };
9708
9709 /**
9710 * Find the first focusable input in the index layout and focus
9711 * on it.
9712 */
9713 OO.ui.IndexLayout.prototype.focusFirstFocusable = function () {
9714 var i, len,
9715 found = false,
9716 items = this.stackLayout.getItems(),
9717 checkAndFocus = function () {
9718 if ( OO.ui.isFocusableElement( $( this ) ) ) {
9719 $( this ).focus();
9720 found = true;
9721 return false;
9722 }
9723 };
9724
9725 for ( i = 0, len = items.length; i < len; i++ ) {
9726 if ( found ) {
9727 break;
9728 }
9729 // Find all potentially focusable elements in the item
9730 // and check if they are focusable
9731 items[i].$element
9732 .find( 'input, select, textarea, button, object' )
9733 .each( checkAndFocus );
9734 }
9735 };
9736
9737 /**
9738 * Handle tab widget select events.
9739 *
9740 * @private
9741 * @param {OO.ui.OptionWidget|null} item Selected item
9742 */
9743 OO.ui.IndexLayout.prototype.onTabSelectWidgetSelect = function ( item ) {
9744 if ( item ) {
9745 this.setCard( item.getData() );
9746 }
9747 };
9748
9749 /**
9750 * Get the card closest to the specified card.
9751 *
9752 * @param {OO.ui.CardLayout} card Card to use as a reference point
9753 * @return {OO.ui.CardLayout|null} Card closest to the specified card
9754 */
9755 OO.ui.IndexLayout.prototype.getClosestCard = function ( card ) {
9756 var next, prev, level,
9757 cards = this.stackLayout.getItems(),
9758 index = $.inArray( card, cards );
9759
9760 if ( index !== -1 ) {
9761 next = cards[ index + 1 ];
9762 prev = cards[ index - 1 ];
9763 // Prefer adjacent cards at the same level
9764 level = this.tabSelectWidget.getItemFromData( card.getName() ).getLevel();
9765 if (
9766 prev &&
9767 level === this.tabSelectWidget.getItemFromData( prev.getName() ).getLevel()
9768 ) {
9769 return prev;
9770 }
9771 if (
9772 next &&
9773 level === this.tabSelectWidget.getItemFromData( next.getName() ).getLevel()
9774 ) {
9775 return next;
9776 }
9777 }
9778 return prev || next || null;
9779 };
9780
9781 /**
9782 * Get the tabs widget.
9783 *
9784 * @return {OO.ui.TabSelectWidget} Tabs widget
9785 */
9786 OO.ui.IndexLayout.prototype.getTabs = function () {
9787 return this.tabSelectWidget;
9788 };
9789
9790 /**
9791 * Get a card by its symbolic name.
9792 *
9793 * @param {string} name Symbolic name of card
9794 * @return {OO.ui.CardLayout|undefined} Card, if found
9795 */
9796 OO.ui.IndexLayout.prototype.getCard = function ( name ) {
9797 return this.cards[ name ];
9798 };
9799
9800 /**
9801 * Get the current card.
9802 *
9803 * @return {OO.ui.CardLayout|undefined} Current card, if found
9804 */
9805 OO.ui.IndexLayout.prototype.getCurrentCard = function () {
9806 var name = this.getCurrentCardName();
9807 return name ? this.getCard( name ) : undefined;
9808 };
9809
9810 /**
9811 * Get the symbolic name of the current card.
9812 *
9813 * @return {string|null} Symbolic name of the current card
9814 */
9815 OO.ui.IndexLayout.prototype.getCurrentCardName = function () {
9816 return this.currentCardName;
9817 };
9818
9819 /**
9820 * Add cards to the index layout
9821 *
9822 * When cards are added with the same names as existing cards, the existing cards will be
9823 * automatically removed before the new cards are added.
9824 *
9825 * @param {OO.ui.CardLayout[]} cards Cards to add
9826 * @param {number} index Index of the insertion point
9827 * @fires add
9828 * @chainable
9829 */
9830 OO.ui.IndexLayout.prototype.addCards = function ( cards, index ) {
9831 var i, len, name, card, item, currentIndex,
9832 stackLayoutCards = this.stackLayout.getItems(),
9833 remove = [],
9834 items = [];
9835
9836 // Remove cards with same names
9837 for ( i = 0, len = cards.length; i < len; i++ ) {
9838 card = cards[ i ];
9839 name = card.getName();
9840
9841 if ( Object.prototype.hasOwnProperty.call( this.cards, name ) ) {
9842 // Correct the insertion index
9843 currentIndex = $.inArray( this.cards[ name ], stackLayoutCards );
9844 if ( currentIndex !== -1 && currentIndex + 1 < index ) {
9845 index--;
9846 }
9847 remove.push( this.cards[ name ] );
9848 }
9849 }
9850 if ( remove.length ) {
9851 this.removeCards( remove );
9852 }
9853
9854 // Add new cards
9855 for ( i = 0, len = cards.length; i < len; i++ ) {
9856 card = cards[ i ];
9857 name = card.getName();
9858 this.cards[ card.getName() ] = card;
9859 item = new OO.ui.TabOptionWidget( { data: name } );
9860 card.setTabItem( item );
9861 items.push( item );
9862 }
9863
9864 if ( items.length ) {
9865 this.tabSelectWidget.addItems( items, index );
9866 this.selectFirstSelectableCard();
9867 }
9868 this.stackLayout.addItems( cards, index );
9869 this.emit( 'add', cards, index );
9870
9871 return this;
9872 };
9873
9874 /**
9875 * Remove the specified cards from the index layout.
9876 *
9877 * To remove all cards from the index, you may wish to use the #clearCards method instead.
9878 *
9879 * @param {OO.ui.CardLayout[]} cards An array of cards to remove
9880 * @fires remove
9881 * @chainable
9882 */
9883 OO.ui.IndexLayout.prototype.removeCards = function ( cards ) {
9884 var i, len, name, card,
9885 items = [];
9886
9887 for ( i = 0, len = cards.length; i < len; i++ ) {
9888 card = cards[ i ];
9889 name = card.getName();
9890 delete this.cards[ name ];
9891 items.push( this.tabSelectWidget.getItemFromData( name ) );
9892 card.setTabItem( null );
9893 }
9894 if ( items.length ) {
9895 this.tabSelectWidget.removeItems( items );
9896 this.selectFirstSelectableCard();
9897 }
9898 this.stackLayout.removeItems( cards );
9899 this.emit( 'remove', cards );
9900
9901 return this;
9902 };
9903
9904 /**
9905 * Clear all cards from the index layout.
9906 *
9907 * To remove only a subset of cards from the index, use the #removeCards method.
9908 *
9909 * @fires remove
9910 * @chainable
9911 */
9912 OO.ui.IndexLayout.prototype.clearCards = function () {
9913 var i, len,
9914 cards = this.stackLayout.getItems();
9915
9916 this.cards = {};
9917 this.currentCardName = null;
9918 this.tabSelectWidget.clearItems();
9919 for ( i = 0, len = cards.length; i < len; i++ ) {
9920 cards[ i ].setTabItem( null );
9921 }
9922 this.stackLayout.clearItems();
9923
9924 this.emit( 'remove', cards );
9925
9926 return this;
9927 };
9928
9929 /**
9930 * Set the current card by symbolic name.
9931 *
9932 * @fires set
9933 * @param {string} name Symbolic name of card
9934 */
9935 OO.ui.IndexLayout.prototype.setCard = function ( name ) {
9936 var selectedItem,
9937 $focused,
9938 card = this.cards[ name ];
9939
9940 if ( name !== this.currentCardName ) {
9941 selectedItem = this.tabSelectWidget.getSelectedItem();
9942 if ( selectedItem && selectedItem.getData() !== name ) {
9943 this.tabSelectWidget.selectItemByData( name );
9944 }
9945 if ( card ) {
9946 if ( this.currentCardName && this.cards[ this.currentCardName ] ) {
9947 this.cards[ this.currentCardName ].setActive( false );
9948 // Blur anything focused if the next card doesn't have anything focusable - this
9949 // is not needed if the next card has something focusable because once it is focused
9950 // this blur happens automatically
9951 if ( this.autoFocus && !card.$element.find( ':input' ).length ) {
9952 $focused = this.cards[ this.currentCardName ].$element.find( ':focus' );
9953 if ( $focused.length ) {
9954 $focused[ 0 ].blur();
9955 }
9956 }
9957 }
9958 this.currentCardName = name;
9959 this.stackLayout.setItem( card );
9960 card.setActive( true );
9961 this.emit( 'set', card );
9962 }
9963 }
9964 };
9965
9966 /**
9967 * Select the first selectable card.
9968 *
9969 * @chainable
9970 */
9971 OO.ui.IndexLayout.prototype.selectFirstSelectableCard = function () {
9972 if ( !this.tabSelectWidget.getSelectedItem() ) {
9973 this.tabSelectWidget.selectItem( this.tabSelectWidget.getFirstSelectableItem() );
9974 }
9975
9976 return this;
9977 };
9978
9979 /**
9980 * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
9981 * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
9982 *
9983 * @example
9984 * // Example of a panel layout
9985 * var panel = new OO.ui.PanelLayout( {
9986 * expanded: false,
9987 * framed: true,
9988 * padded: true,
9989 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
9990 * } );
9991 * $( 'body' ).append( panel.$element );
9992 *
9993 * @class
9994 * @extends OO.ui.Layout
9995 *
9996 * @constructor
9997 * @param {Object} [config] Configuration options
9998 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
9999 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
10000 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
10001 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
10002 */
10003 OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
10004 // Configuration initialization
10005 config = $.extend( {
10006 scrollable: false,
10007 padded: false,
10008 expanded: true,
10009 framed: false
10010 }, config );
10011
10012 // Parent constructor
10013 OO.ui.PanelLayout.super.call( this, config );
10014
10015 // Initialization
10016 this.$element.addClass( 'oo-ui-panelLayout' );
10017 if ( config.scrollable ) {
10018 this.$element.addClass( 'oo-ui-panelLayout-scrollable' );
10019 }
10020 if ( config.padded ) {
10021 this.$element.addClass( 'oo-ui-panelLayout-padded' );
10022 }
10023 if ( config.expanded ) {
10024 this.$element.addClass( 'oo-ui-panelLayout-expanded' );
10025 }
10026 if ( config.framed ) {
10027 this.$element.addClass( 'oo-ui-panelLayout-framed' );
10028 }
10029 };
10030
10031 /* Setup */
10032
10033 OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
10034
10035 /**
10036 * CardLayouts are used within {@link OO.ui.IndexLayout index layouts} to create cards that users can select and display
10037 * from the index's optional {@link OO.ui.TabSelectWidget tab} navigation. Cards are usually not instantiated directly,
10038 * rather extended to include the required content and functionality.
10039 *
10040 * Each card must have a unique symbolic name, which is passed to the constructor. In addition, the card's tab
10041 * item is customized (with a label) using the #setupTabItem method. See
10042 * {@link OO.ui.IndexLayout IndexLayout} for an example.
10043 *
10044 * @class
10045 * @extends OO.ui.PanelLayout
10046 *
10047 * @constructor
10048 * @param {string} name Unique symbolic name of card
10049 * @param {Object} [config] Configuration options
10050 */
10051 OO.ui.CardLayout = function OoUiCardLayout( name, config ) {
10052 // Allow passing positional parameters inside the config object
10053 if ( OO.isPlainObject( name ) && config === undefined ) {
10054 config = name;
10055 name = config.name;
10056 }
10057
10058 // Configuration initialization
10059 config = $.extend( { scrollable: true }, config );
10060
10061 // Parent constructor
10062 OO.ui.CardLayout.super.call( this, config );
10063
10064 // Properties
10065 this.name = name;
10066 this.tabItem = null;
10067 this.active = false;
10068
10069 // Initialization
10070 this.$element.addClass( 'oo-ui-cardLayout' );
10071 };
10072
10073 /* Setup */
10074
10075 OO.inheritClass( OO.ui.CardLayout, OO.ui.PanelLayout );
10076
10077 /* Events */
10078
10079 /**
10080 * An 'active' event is emitted when the card becomes active. Cards become active when they are
10081 * shown in a index layout that is configured to display only one card at a time.
10082 *
10083 * @event active
10084 * @param {boolean} active Card is active
10085 */
10086
10087 /* Methods */
10088
10089 /**
10090 * Get the symbolic name of the card.
10091 *
10092 * @return {string} Symbolic name of card
10093 */
10094 OO.ui.CardLayout.prototype.getName = function () {
10095 return this.name;
10096 };
10097
10098 /**
10099 * Check if card is active.
10100 *
10101 * Cards become active when they are shown in a {@link OO.ui.IndexLayout index layout} that is configured to display
10102 * only one card at a time. Additional CSS is applied to the card's tab item to reflect the active state.
10103 *
10104 * @return {boolean} Card is active
10105 */
10106 OO.ui.CardLayout.prototype.isActive = function () {
10107 return this.active;
10108 };
10109
10110 /**
10111 * Get tab item.
10112 *
10113 * The tab item allows users to access the card from the index's tab
10114 * navigation. The tab item itself can be customized (with a label, level, etc.) using the #setupTabItem method.
10115 *
10116 * @return {OO.ui.TabOptionWidget|null} Tab option widget
10117 */
10118 OO.ui.CardLayout.prototype.getTabItem = function () {
10119 return this.tabItem;
10120 };
10121
10122 /**
10123 * Set or unset the tab item.
10124 *
10125 * Specify a {@link OO.ui.TabOptionWidget tab option} to set it,
10126 * or `null` to clear the tab item. To customize the tab item itself (e.g., to set a label or tab
10127 * level), use #setupTabItem instead of this method.
10128 *
10129 * @param {OO.ui.TabOptionWidget|null} tabItem Tab option widget, null to clear
10130 * @chainable
10131 */
10132 OO.ui.CardLayout.prototype.setTabItem = function ( tabItem ) {
10133 this.tabItem = tabItem || null;
10134 if ( tabItem ) {
10135 this.setupTabItem();
10136 }
10137 return this;
10138 };
10139
10140 /**
10141 * Set up the tab item.
10142 *
10143 * Use this method to customize the tab item (e.g., to add a label or tab level). To set or unset
10144 * the tab item itself (with a {@link OO.ui.TabOptionWidget tab option} or `null`), use
10145 * the #setTabItem method instead.
10146 *
10147 * @param {OO.ui.TabOptionWidget} tabItem Tab option widget to set up
10148 * @chainable
10149 */
10150 OO.ui.CardLayout.prototype.setupTabItem = function () {
10151 return this;
10152 };
10153
10154 /**
10155 * Set the card to its 'active' state.
10156 *
10157 * Cards become active when they are shown in a index layout that is configured to display only one card at a time. Additional
10158 * CSS is applied to the tab item to reflect the card's active state. Outside of the index
10159 * context, setting the active state on a card does nothing.
10160 *
10161 * @param {boolean} value Card is active
10162 * @fires active
10163 */
10164 OO.ui.CardLayout.prototype.setActive = function ( active ) {
10165 active = !!active;
10166
10167 if ( active !== this.active ) {
10168 this.active = active;
10169 this.$element.toggleClass( 'oo-ui-cardLayout-active', this.active );
10170 this.emit( 'active', this.active );
10171 }
10172 };
10173
10174 /**
10175 * PageLayouts are used within {@link OO.ui.BookletLayout booklet layouts} to create pages that users can select and display
10176 * from the booklet's optional {@link OO.ui.OutlineSelectWidget outline} navigation. Pages are usually not instantiated directly,
10177 * rather extended to include the required content and functionality.
10178 *
10179 * Each page must have a unique symbolic name, which is passed to the constructor. In addition, the page's outline
10180 * item is customized (with a label, outline level, etc.) using the #setupOutlineItem method. See
10181 * {@link OO.ui.BookletLayout BookletLayout} for an example.
10182 *
10183 * @class
10184 * @extends OO.ui.PanelLayout
10185 *
10186 * @constructor
10187 * @param {string} name Unique symbolic name of page
10188 * @param {Object} [config] Configuration options
10189 */
10190 OO.ui.PageLayout = function OoUiPageLayout( name, config ) {
10191 // Allow passing positional parameters inside the config object
10192 if ( OO.isPlainObject( name ) && config === undefined ) {
10193 config = name;
10194 name = config.name;
10195 }
10196
10197 // Configuration initialization
10198 config = $.extend( { scrollable: true }, config );
10199
10200 // Parent constructor
10201 OO.ui.PageLayout.super.call( this, config );
10202
10203 // Properties
10204 this.name = name;
10205 this.outlineItem = null;
10206 this.active = false;
10207
10208 // Initialization
10209 this.$element.addClass( 'oo-ui-pageLayout' );
10210 };
10211
10212 /* Setup */
10213
10214 OO.inheritClass( OO.ui.PageLayout, OO.ui.PanelLayout );
10215
10216 /* Events */
10217
10218 /**
10219 * An 'active' event is emitted when the page becomes active. Pages become active when they are
10220 * shown in a booklet layout that is configured to display only one page at a time.
10221 *
10222 * @event active
10223 * @param {boolean} active Page is active
10224 */
10225
10226 /* Methods */
10227
10228 /**
10229 * Get the symbolic name of the page.
10230 *
10231 * @return {string} Symbolic name of page
10232 */
10233 OO.ui.PageLayout.prototype.getName = function () {
10234 return this.name;
10235 };
10236
10237 /**
10238 * Check if page is active.
10239 *
10240 * Pages become active when they are shown in a {@link OO.ui.BookletLayout booklet layout} that is configured to display
10241 * only one page at a time. Additional CSS is applied to the page's outline item to reflect the active state.
10242 *
10243 * @return {boolean} Page is active
10244 */
10245 OO.ui.PageLayout.prototype.isActive = function () {
10246 return this.active;
10247 };
10248
10249 /**
10250 * Get outline item.
10251 *
10252 * The outline item allows users to access the page from the booklet's outline
10253 * navigation. The outline item itself can be customized (with a label, level, etc.) using the #setupOutlineItem method.
10254 *
10255 * @return {OO.ui.OutlineOptionWidget|null} Outline option widget
10256 */
10257 OO.ui.PageLayout.prototype.getOutlineItem = function () {
10258 return this.outlineItem;
10259 };
10260
10261 /**
10262 * Set or unset the outline item.
10263 *
10264 * Specify an {@link OO.ui.OutlineOptionWidget outline option} to set it,
10265 * or `null` to clear the outline item. To customize the outline item itself (e.g., to set a label or outline
10266 * level), use #setupOutlineItem instead of this method.
10267 *
10268 * @param {OO.ui.OutlineOptionWidget|null} outlineItem Outline option widget, null to clear
10269 * @chainable
10270 */
10271 OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) {
10272 this.outlineItem = outlineItem || null;
10273 if ( outlineItem ) {
10274 this.setupOutlineItem();
10275 }
10276 return this;
10277 };
10278
10279 /**
10280 * Set up the outline item.
10281 *
10282 * Use this method to customize the outline item (e.g., to add a label or outline level). To set or unset
10283 * the outline item itself (with an {@link OO.ui.OutlineOptionWidget outline option} or `null`), use
10284 * the #setOutlineItem method instead.
10285 *
10286 * @param {OO.ui.OutlineOptionWidget} outlineItem Outline option widget to set up
10287 * @chainable
10288 */
10289 OO.ui.PageLayout.prototype.setupOutlineItem = function () {
10290 return this;
10291 };
10292
10293 /**
10294 * Set the page to its 'active' state.
10295 *
10296 * Pages become active when they are shown in a booklet layout that is configured to display only one page at a time. Additional
10297 * CSS is applied to the outline item to reflect the page's active state. Outside of the booklet
10298 * context, setting the active state on a page does nothing.
10299 *
10300 * @param {boolean} value Page is active
10301 * @fires active
10302 */
10303 OO.ui.PageLayout.prototype.setActive = function ( active ) {
10304 active = !!active;
10305
10306 if ( active !== this.active ) {
10307 this.active = active;
10308 this.$element.toggleClass( 'oo-ui-pageLayout-active', active );
10309 this.emit( 'active', this.active );
10310 }
10311 };
10312
10313 /**
10314 * StackLayouts contain a series of {@link OO.ui.PanelLayout panel layouts}. By default, only one panel is displayed
10315 * at a time, though the stack layout can also be configured to show all contained panels, one after another,
10316 * by setting the #continuous option to 'true'.
10317 *
10318 * @example
10319 * // A stack layout with two panels, configured to be displayed continously
10320 * var myStack = new OO.ui.StackLayout( {
10321 * items: [
10322 * new OO.ui.PanelLayout( {
10323 * $content: $( '<p>Panel One</p>' ),
10324 * padded: true,
10325 * framed: true
10326 * } ),
10327 * new OO.ui.PanelLayout( {
10328 * $content: $( '<p>Panel Two</p>' ),
10329 * padded: true,
10330 * framed: true
10331 * } )
10332 * ],
10333 * continuous: true
10334 * } );
10335 * $( 'body' ).append( myStack.$element );
10336 *
10337 * @class
10338 * @extends OO.ui.PanelLayout
10339 * @mixins OO.ui.mixin.GroupElement
10340 *
10341 * @constructor
10342 * @param {Object} [config] Configuration options
10343 * @cfg {boolean} [continuous=false] Show all panels, one after another. By default, only one panel is displayed at a time.
10344 * @cfg {OO.ui.Layout[]} [items] Panel layouts to add to the stack layout.
10345 */
10346 OO.ui.StackLayout = function OoUiStackLayout( config ) {
10347 // Configuration initialization
10348 config = $.extend( { scrollable: true }, config );
10349
10350 // Parent constructor
10351 OO.ui.StackLayout.super.call( this, config );
10352
10353 // Mixin constructors
10354 OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
10355
10356 // Properties
10357 this.currentItem = null;
10358 this.continuous = !!config.continuous;
10359
10360 // Initialization
10361 this.$element.addClass( 'oo-ui-stackLayout' );
10362 if ( this.continuous ) {
10363 this.$element.addClass( 'oo-ui-stackLayout-continuous' );
10364 }
10365 if ( Array.isArray( config.items ) ) {
10366 this.addItems( config.items );
10367 }
10368 };
10369
10370 /* Setup */
10371
10372 OO.inheritClass( OO.ui.StackLayout, OO.ui.PanelLayout );
10373 OO.mixinClass( OO.ui.StackLayout, OO.ui.mixin.GroupElement );
10374
10375 /* Events */
10376
10377 /**
10378 * A 'set' event is emitted when panels are {@link #addItems added}, {@link #removeItems removed},
10379 * {@link #clearItems cleared} or {@link #setItem displayed}.
10380 *
10381 * @event set
10382 * @param {OO.ui.Layout|null} item Current panel or `null` if no panel is shown
10383 */
10384
10385 /* Methods */
10386
10387 /**
10388 * Get the current panel.
10389 *
10390 * @return {OO.ui.Layout|null}
10391 */
10392 OO.ui.StackLayout.prototype.getCurrentItem = function () {
10393 return this.currentItem;
10394 };
10395
10396 /**
10397 * Unset the current item.
10398 *
10399 * @private
10400 * @param {OO.ui.StackLayout} layout
10401 * @fires set
10402 */
10403 OO.ui.StackLayout.prototype.unsetCurrentItem = function () {
10404 var prevItem = this.currentItem;
10405 if ( prevItem === null ) {
10406 return;
10407 }
10408
10409 this.currentItem = null;
10410 this.emit( 'set', null );
10411 };
10412
10413 /**
10414 * Add panel layouts to the stack layout.
10415 *
10416 * Panels will be added to the end of the stack layout array unless the optional index parameter specifies a different
10417 * insertion point. Adding a panel that is already in the stack will move it to the end of the array or the point specified
10418 * by the index.
10419 *
10420 * @param {OO.ui.Layout[]} items Panels to add
10421 * @param {number} [index] Index of the insertion point
10422 * @chainable
10423 */
10424 OO.ui.StackLayout.prototype.addItems = function ( items, index ) {
10425 // Update the visibility
10426 this.updateHiddenState( items, this.currentItem );
10427
10428 // Mixin method
10429 OO.ui.mixin.GroupElement.prototype.addItems.call( this, items, index );
10430
10431 if ( !this.currentItem && items.length ) {
10432 this.setItem( items[ 0 ] );
10433 }
10434
10435 return this;
10436 };
10437
10438 /**
10439 * Remove the specified panels from the stack layout.
10440 *
10441 * Removed panels are detached from the DOM, not removed, so that they may be reused. To remove all panels,
10442 * you may wish to use the #clearItems method instead.
10443 *
10444 * @param {OO.ui.Layout[]} items Panels to remove
10445 * @chainable
10446 * @fires set
10447 */
10448 OO.ui.StackLayout.prototype.removeItems = function ( items ) {
10449 // Mixin method
10450 OO.ui.mixin.GroupElement.prototype.removeItems.call( this, items );
10451
10452 if ( $.inArray( this.currentItem, items ) !== -1 ) {
10453 if ( this.items.length ) {
10454 this.setItem( this.items[ 0 ] );
10455 } else {
10456 this.unsetCurrentItem();
10457 }
10458 }
10459
10460 return this;
10461 };
10462
10463 /**
10464 * Clear all panels from the stack layout.
10465 *
10466 * Cleared panels are detached from the DOM, not removed, so that they may be reused. To remove only
10467 * a subset of panels, use the #removeItems method.
10468 *
10469 * @chainable
10470 * @fires set
10471 */
10472 OO.ui.StackLayout.prototype.clearItems = function () {
10473 this.unsetCurrentItem();
10474 OO.ui.mixin.GroupElement.prototype.clearItems.call( this );
10475
10476 return this;
10477 };
10478
10479 /**
10480 * Show the specified panel.
10481 *
10482 * If another panel is currently displayed, it will be hidden.
10483 *
10484 * @param {OO.ui.Layout} item Panel to show
10485 * @chainable
10486 * @fires set
10487 */
10488 OO.ui.StackLayout.prototype.setItem = function ( item ) {
10489 if ( item !== this.currentItem ) {
10490 this.updateHiddenState( this.items, item );
10491
10492 if ( $.inArray( item, this.items ) !== -1 ) {
10493 this.currentItem = item;
10494 this.emit( 'set', item );
10495 } else {
10496 this.unsetCurrentItem();
10497 }
10498 }
10499
10500 return this;
10501 };
10502
10503 /**
10504 * Update the visibility of all items in case of non-continuous view.
10505 *
10506 * Ensure all items are hidden except for the selected one.
10507 * This method does nothing when the stack is continuous.
10508 *
10509 * @private
10510 * @param {OO.ui.Layout[]} items Item list iterate over
10511 * @param {OO.ui.Layout} [selectedItem] Selected item to show
10512 */
10513 OO.ui.StackLayout.prototype.updateHiddenState = function ( items, selectedItem ) {
10514 var i, len;
10515
10516 if ( !this.continuous ) {
10517 for ( i = 0, len = items.length; i < len; i++ ) {
10518 if ( !selectedItem || selectedItem !== items[ i ] ) {
10519 items[ i ].$element.addClass( 'oo-ui-element-hidden' );
10520 }
10521 }
10522 if ( selectedItem ) {
10523 selectedItem.$element.removeClass( 'oo-ui-element-hidden' );
10524 }
10525 }
10526 };
10527
10528 /**
10529 * BarToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
10530 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.MenuToolGroup MenuToolGroup}
10531 * and {@link OO.ui.ListToolGroup ListToolGroup}). The {@link OO.ui.Tool tools} in a BarToolGroup are
10532 * displayed by icon in a single row. The title of the tool is displayed when users move the mouse over
10533 * the tool.
10534 *
10535 * BarToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar is
10536 * set up.
10537 *
10538 * @example
10539 * // Example of a BarToolGroup with two tools
10540 * var toolFactory = new OO.ui.ToolFactory();
10541 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
10542 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
10543 *
10544 * // We will be placing status text in this element when tools are used
10545 * var $area = $( '<p>' ).text( 'Example of a BarToolGroup with two tools.' );
10546 *
10547 * // Define the tools that we're going to place in our toolbar
10548 *
10549 * // Create a class inheriting from OO.ui.Tool
10550 * function PictureTool() {
10551 * PictureTool.super.apply( this, arguments );
10552 * }
10553 * OO.inheritClass( PictureTool, OO.ui.Tool );
10554 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
10555 * // of 'icon' and 'title' (displayed icon and text).
10556 * PictureTool.static.name = 'picture';
10557 * PictureTool.static.icon = 'picture';
10558 * PictureTool.static.title = 'Insert picture';
10559 * // Defines the action that will happen when this tool is selected (clicked).
10560 * PictureTool.prototype.onSelect = function () {
10561 * $area.text( 'Picture tool clicked!' );
10562 * // Never display this tool as "active" (selected).
10563 * this.setActive( false );
10564 * };
10565 * // Make this tool available in our toolFactory and thus our toolbar
10566 * toolFactory.register( PictureTool );
10567 *
10568 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
10569 * // little popup window (a PopupWidget).
10570 * function HelpTool( toolGroup, config ) {
10571 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
10572 * padded: true,
10573 * label: 'Help',
10574 * head: true
10575 * } }, config ) );
10576 * this.popup.$body.append( '<p>I am helpful!</p>' );
10577 * }
10578 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
10579 * HelpTool.static.name = 'help';
10580 * HelpTool.static.icon = 'help';
10581 * HelpTool.static.title = 'Help';
10582 * toolFactory.register( HelpTool );
10583 *
10584 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
10585 * // used once (but not all defined tools must be used).
10586 * toolbar.setup( [
10587 * {
10588 * // 'bar' tool groups display tools by icon only
10589 * type: 'bar',
10590 * include: [ 'picture', 'help' ]
10591 * }
10592 * ] );
10593 *
10594 * // Create some UI around the toolbar and place it in the document
10595 * var frame = new OO.ui.PanelLayout( {
10596 * expanded: false,
10597 * framed: true
10598 * } );
10599 * var contentFrame = new OO.ui.PanelLayout( {
10600 * expanded: false,
10601 * padded: true
10602 * } );
10603 * frame.$element.append(
10604 * toolbar.$element,
10605 * contentFrame.$element.append( $area )
10606 * );
10607 * $( 'body' ).append( frame.$element );
10608 *
10609 * // Here is where the toolbar is actually built. This must be done after inserting it into the
10610 * // document.
10611 * toolbar.initialize();
10612 *
10613 * For more information about how to add tools to a bar tool group, please see {@link OO.ui.ToolGroup toolgroup}.
10614 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
10615 *
10616 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
10617 *
10618 * @class
10619 * @extends OO.ui.ToolGroup
10620 *
10621 * @constructor
10622 * @param {OO.ui.Toolbar} toolbar
10623 * @param {Object} [config] Configuration options
10624 */
10625 OO.ui.BarToolGroup = function OoUiBarToolGroup( toolbar, config ) {
10626 // Allow passing positional parameters inside the config object
10627 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
10628 config = toolbar;
10629 toolbar = config.toolbar;
10630 }
10631
10632 // Parent constructor
10633 OO.ui.BarToolGroup.super.call( this, toolbar, config );
10634
10635 // Initialization
10636 this.$element.addClass( 'oo-ui-barToolGroup' );
10637 };
10638
10639 /* Setup */
10640
10641 OO.inheritClass( OO.ui.BarToolGroup, OO.ui.ToolGroup );
10642
10643 /* Static Properties */
10644
10645 OO.ui.BarToolGroup.static.titleTooltips = true;
10646
10647 OO.ui.BarToolGroup.static.accelTooltips = true;
10648
10649 OO.ui.BarToolGroup.static.name = 'bar';
10650
10651 /**
10652 * PopupToolGroup is an abstract base class used by both {@link OO.ui.MenuToolGroup MenuToolGroup}
10653 * and {@link OO.ui.ListToolGroup ListToolGroup} to provide a popup--an overlaid menu or list of tools with an
10654 * optional icon and label. This class can be used for other base classes that also use this functionality.
10655 *
10656 * @abstract
10657 * @class
10658 * @extends OO.ui.ToolGroup
10659 * @mixins OO.ui.mixin.IconElement
10660 * @mixins OO.ui.mixin.IndicatorElement
10661 * @mixins OO.ui.mixin.LabelElement
10662 * @mixins OO.ui.mixin.TitledElement
10663 * @mixins OO.ui.mixin.ClippableElement
10664 * @mixins OO.ui.mixin.TabIndexedElement
10665 *
10666 * @constructor
10667 * @param {OO.ui.Toolbar} toolbar
10668 * @param {Object} [config] Configuration options
10669 * @cfg {string} [header] Text to display at the top of the popup
10670 */
10671 OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
10672 // Allow passing positional parameters inside the config object
10673 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
10674 config = toolbar;
10675 toolbar = config.toolbar;
10676 }
10677
10678 // Configuration initialization
10679 config = config || {};
10680
10681 // Parent constructor
10682 OO.ui.PopupToolGroup.super.call( this, toolbar, config );
10683
10684 // Properties
10685 this.active = false;
10686 this.dragging = false;
10687 this.onBlurHandler = this.onBlur.bind( this );
10688 this.$handle = $( '<span>' );
10689
10690 // Mixin constructors
10691 OO.ui.mixin.IconElement.call( this, config );
10692 OO.ui.mixin.IndicatorElement.call( this, config );
10693 OO.ui.mixin.LabelElement.call( this, config );
10694 OO.ui.mixin.TitledElement.call( this, config );
10695 OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
10696 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
10697
10698 // Events
10699 this.$handle.on( {
10700 keydown: this.onHandleMouseKeyDown.bind( this ),
10701 keyup: this.onHandleMouseKeyUp.bind( this ),
10702 mousedown: this.onHandleMouseKeyDown.bind( this ),
10703 mouseup: this.onHandleMouseKeyUp.bind( this )
10704 } );
10705
10706 // Initialization
10707 this.$handle
10708 .addClass( 'oo-ui-popupToolGroup-handle' )
10709 .append( this.$icon, this.$label, this.$indicator );
10710 // If the pop-up should have a header, add it to the top of the toolGroup.
10711 // Note: If this feature is useful for other widgets, we could abstract it into an
10712 // OO.ui.HeaderedElement mixin constructor.
10713 if ( config.header !== undefined ) {
10714 this.$group
10715 .prepend( $( '<span>' )
10716 .addClass( 'oo-ui-popupToolGroup-header' )
10717 .text( config.header )
10718 );
10719 }
10720 this.$element
10721 .addClass( 'oo-ui-popupToolGroup' )
10722 .prepend( this.$handle );
10723 };
10724
10725 /* Setup */
10726
10727 OO.inheritClass( OO.ui.PopupToolGroup, OO.ui.ToolGroup );
10728 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.IconElement );
10729 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.IndicatorElement );
10730 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.LabelElement );
10731 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.TitledElement );
10732 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.ClippableElement );
10733 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.TabIndexedElement );
10734
10735 /* Methods */
10736
10737 /**
10738 * @inheritdoc
10739 */
10740 OO.ui.PopupToolGroup.prototype.setDisabled = function () {
10741 // Parent method
10742 OO.ui.PopupToolGroup.super.prototype.setDisabled.apply( this, arguments );
10743
10744 if ( this.isDisabled() && this.isElementAttached() ) {
10745 this.setActive( false );
10746 }
10747 };
10748
10749 /**
10750 * Handle focus being lost.
10751 *
10752 * The event is actually generated from a mouseup/keyup, so it is not a normal blur event object.
10753 *
10754 * @protected
10755 * @param {jQuery.Event} e Mouse up or key up event
10756 */
10757 OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) {
10758 // Only deactivate when clicking outside the dropdown element
10759 if ( $( e.target ).closest( '.oo-ui-popupToolGroup' )[ 0 ] !== this.$element[ 0 ] ) {
10760 this.setActive( false );
10761 }
10762 };
10763
10764 /**
10765 * @inheritdoc
10766 */
10767 OO.ui.PopupToolGroup.prototype.onMouseKeyUp = function ( e ) {
10768 // Only close toolgroup when a tool was actually selected
10769 if (
10770 !this.isDisabled() && this.pressed && this.pressed === this.getTargetTool( e ) &&
10771 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
10772 ) {
10773 this.setActive( false );
10774 }
10775 return OO.ui.PopupToolGroup.super.prototype.onMouseKeyUp.call( this, e );
10776 };
10777
10778 /**
10779 * Handle mouse up and key up events.
10780 *
10781 * @protected
10782 * @param {jQuery.Event} e Mouse up or key up event
10783 */
10784 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyUp = function ( e ) {
10785 if (
10786 !this.isDisabled() &&
10787 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
10788 ) {
10789 return false;
10790 }
10791 };
10792
10793 /**
10794 * Handle mouse down and key down events.
10795 *
10796 * @protected
10797 * @param {jQuery.Event} e Mouse down or key down event
10798 */
10799 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyDown = function ( e ) {
10800 if (
10801 !this.isDisabled() &&
10802 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
10803 ) {
10804 this.setActive( !this.active );
10805 return false;
10806 }
10807 };
10808
10809 /**
10810 * Switch into 'active' mode.
10811 *
10812 * When active, the popup is visible. A mouseup event anywhere in the document will trigger
10813 * deactivation.
10814 */
10815 OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
10816 value = !!value;
10817 if ( this.active !== value ) {
10818 this.active = value;
10819 if ( value ) {
10820 this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler, true );
10821 this.getElementDocument().addEventListener( 'keyup', this.onBlurHandler, true );
10822
10823 // Try anchoring the popup to the left first
10824 this.$element.addClass( 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left' );
10825 this.toggleClipping( true );
10826 if ( this.isClippedHorizontally() ) {
10827 // Anchoring to the left caused the popup to clip, so anchor it to the right instead
10828 this.toggleClipping( false );
10829 this.$element
10830 .removeClass( 'oo-ui-popupToolGroup-left' )
10831 .addClass( 'oo-ui-popupToolGroup-right' );
10832 this.toggleClipping( true );
10833 }
10834 } else {
10835 this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler, true );
10836 this.getElementDocument().removeEventListener( 'keyup', this.onBlurHandler, true );
10837 this.$element.removeClass(
10838 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left oo-ui-popupToolGroup-right'
10839 );
10840 this.toggleClipping( false );
10841 }
10842 }
10843 };
10844
10845 /**
10846 * ListToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
10847 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.MenuToolGroup MenuToolGroup}
10848 * and {@link OO.ui.BarToolGroup BarToolGroup}). The {@link OO.ui.Tool tools} in a ListToolGroup are displayed
10849 * by label in a dropdown menu. The title of the tool is used as the label text. The menu itself can be configured
10850 * with a label, icon, indicator, header, and title.
10851 *
10852 * ListToolGroups can be configured to be expanded and collapsed. Collapsed lists will have a ‘More’ option that
10853 * users can select to see the full list of tools. If a collapsed toolgroup is expanded, a ‘Fewer’ option permits
10854 * users to collapse the list again.
10855 *
10856 * ListToolGroups are created by a {@link OO.ui.ToolGroupFactory toolgroup factory} when the toolbar is set up. The factory
10857 * requires the ListToolGroup's symbolic name, 'list', which is specified along with the other configurations. For more
10858 * information about how to add tools to a ListToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
10859 *
10860 * @example
10861 * // Example of a ListToolGroup
10862 * var toolFactory = new OO.ui.ToolFactory();
10863 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
10864 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
10865 *
10866 * // Configure and register two tools
10867 * function SettingsTool() {
10868 * SettingsTool.super.apply( this, arguments );
10869 * }
10870 * OO.inheritClass( SettingsTool, OO.ui.Tool );
10871 * SettingsTool.static.name = 'settings';
10872 * SettingsTool.static.icon = 'settings';
10873 * SettingsTool.static.title = 'Change settings';
10874 * SettingsTool.prototype.onSelect = function () {
10875 * this.setActive( false );
10876 * };
10877 * toolFactory.register( SettingsTool );
10878 * // Register two more tools, nothing interesting here
10879 * function StuffTool() {
10880 * StuffTool.super.apply( this, arguments );
10881 * }
10882 * OO.inheritClass( StuffTool, OO.ui.Tool );
10883 * StuffTool.static.name = 'stuff';
10884 * StuffTool.static.icon = 'ellipsis';
10885 * StuffTool.static.title = 'Change the world';
10886 * StuffTool.prototype.onSelect = function () {
10887 * this.setActive( false );
10888 * };
10889 * toolFactory.register( StuffTool );
10890 * toolbar.setup( [
10891 * {
10892 * // Configurations for list toolgroup.
10893 * type: 'list',
10894 * label: 'ListToolGroup',
10895 * indicator: 'down',
10896 * icon: 'picture',
10897 * title: 'This is the title, displayed when user moves the mouse over the list toolgroup',
10898 * header: 'This is the header',
10899 * include: [ 'settings', 'stuff' ],
10900 * allowCollapse: ['stuff']
10901 * }
10902 * ] );
10903 *
10904 * // Create some UI around the toolbar and place it in the document
10905 * var frame = new OO.ui.PanelLayout( {
10906 * expanded: false,
10907 * framed: true
10908 * } );
10909 * frame.$element.append(
10910 * toolbar.$element
10911 * );
10912 * $( 'body' ).append( frame.$element );
10913 * // Build the toolbar. This must be done after the toolbar has been appended to the document.
10914 * toolbar.initialize();
10915 *
10916 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
10917 *
10918 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
10919 *
10920 * @class
10921 * @extends OO.ui.PopupToolGroup
10922 *
10923 * @constructor
10924 * @param {OO.ui.Toolbar} toolbar
10925 * @param {Object} [config] Configuration options
10926 * @cfg {Array} [allowCollapse] Allow the specified tools to be collapsed. By default, collapsible tools
10927 * will only be displayed if users click the ‘More’ option displayed at the bottom of the list. If
10928 * the list is expanded, a ‘Fewer’ option permits users to collapse the list again. Any tools that
10929 * are included in the toolgroup, but are not designated as collapsible, will always be displayed.
10930 * To open a collapsible list in its expanded state, set #expanded to 'true'.
10931 * @cfg {Array} [forceExpand] Expand the specified tools. All other tools will be designated as collapsible.
10932 * Unless #expanded is set to true, the collapsible tools will be collapsed when the list is first opened.
10933 * @cfg {boolean} [expanded=false] Expand collapsible tools. This config is only relevant if tools have
10934 * been designated as collapsible. When expanded is set to true, all tools in the group will be displayed
10935 * when the list is first opened. Users can collapse the list with a ‘Fewer’ option at the bottom.
10936 */
10937 OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
10938 // Allow passing positional parameters inside the config object
10939 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
10940 config = toolbar;
10941 toolbar = config.toolbar;
10942 }
10943
10944 // Configuration initialization
10945 config = config || {};
10946
10947 // Properties (must be set before parent constructor, which calls #populate)
10948 this.allowCollapse = config.allowCollapse;
10949 this.forceExpand = config.forceExpand;
10950 this.expanded = config.expanded !== undefined ? config.expanded : false;
10951 this.collapsibleTools = [];
10952
10953 // Parent constructor
10954 OO.ui.ListToolGroup.super.call( this, toolbar, config );
10955
10956 // Initialization
10957 this.$element.addClass( 'oo-ui-listToolGroup' );
10958 };
10959
10960 /* Setup */
10961
10962 OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup );
10963
10964 /* Static Properties */
10965
10966 OO.ui.ListToolGroup.static.name = 'list';
10967
10968 /* Methods */
10969
10970 /**
10971 * @inheritdoc
10972 */
10973 OO.ui.ListToolGroup.prototype.populate = function () {
10974 var i, len, allowCollapse = [];
10975
10976 OO.ui.ListToolGroup.super.prototype.populate.call( this );
10977
10978 // Update the list of collapsible tools
10979 if ( this.allowCollapse !== undefined ) {
10980 allowCollapse = this.allowCollapse;
10981 } else if ( this.forceExpand !== undefined ) {
10982 allowCollapse = OO.simpleArrayDifference( Object.keys( this.tools ), this.forceExpand );
10983 }
10984
10985 this.collapsibleTools = [];
10986 for ( i = 0, len = allowCollapse.length; i < len; i++ ) {
10987 if ( this.tools[ allowCollapse[ i ] ] !== undefined ) {
10988 this.collapsibleTools.push( this.tools[ allowCollapse[ i ] ] );
10989 }
10990 }
10991
10992 // Keep at the end, even when tools are added
10993 this.$group.append( this.getExpandCollapseTool().$element );
10994
10995 this.getExpandCollapseTool().toggle( this.collapsibleTools.length !== 0 );
10996 this.updateCollapsibleState();
10997 };
10998
10999 OO.ui.ListToolGroup.prototype.getExpandCollapseTool = function () {
11000 if ( this.expandCollapseTool === undefined ) {
11001 var ExpandCollapseTool = function () {
11002 ExpandCollapseTool.super.apply( this, arguments );
11003 };
11004
11005 OO.inheritClass( ExpandCollapseTool, OO.ui.Tool );
11006
11007 ExpandCollapseTool.prototype.onSelect = function () {
11008 this.toolGroup.expanded = !this.toolGroup.expanded;
11009 this.toolGroup.updateCollapsibleState();
11010 this.setActive( false );
11011 };
11012 ExpandCollapseTool.prototype.onUpdateState = function () {
11013 // Do nothing. Tool interface requires an implementation of this function.
11014 };
11015
11016 ExpandCollapseTool.static.name = 'more-fewer';
11017
11018 this.expandCollapseTool = new ExpandCollapseTool( this );
11019 }
11020 return this.expandCollapseTool;
11021 };
11022
11023 /**
11024 * @inheritdoc
11025 */
11026 OO.ui.ListToolGroup.prototype.onMouseKeyUp = function ( e ) {
11027 // Do not close the popup when the user wants to show more/fewer tools
11028 if (
11029 $( e.target ).closest( '.oo-ui-tool-name-more-fewer' ).length &&
11030 ( e.which === 1 || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
11031 ) {
11032 // HACK: Prevent the popup list from being hidden. Skip the PopupToolGroup implementation (which
11033 // hides the popup list when a tool is selected) and call ToolGroup's implementation directly.
11034 return OO.ui.ListToolGroup.super.super.prototype.onMouseKeyUp.call( this, e );
11035 } else {
11036 return OO.ui.ListToolGroup.super.prototype.onMouseKeyUp.call( this, e );
11037 }
11038 };
11039
11040 OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () {
11041 var i, len;
11042
11043 this.getExpandCollapseTool()
11044 .setIcon( this.expanded ? 'collapse' : 'expand' )
11045 .setTitle( OO.ui.msg( this.expanded ? 'ooui-toolgroup-collapse' : 'ooui-toolgroup-expand' ) );
11046
11047 for ( i = 0, len = this.collapsibleTools.length; i < len; i++ ) {
11048 this.collapsibleTools[ i ].toggle( this.expanded );
11049 }
11050 };
11051
11052 /**
11053 * MenuToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
11054 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.BarToolGroup BarToolGroup}
11055 * and {@link OO.ui.ListToolGroup ListToolGroup}). MenuToolGroups contain selectable {@link OO.ui.Tool tools},
11056 * which are displayed by label in a dropdown menu. The tool's title is used as the label text, and the
11057 * menu label is updated to reflect which tool or tools are currently selected. If no tools are selected,
11058 * the menu label is empty. The menu can be configured with an indicator, icon, title, and/or header.
11059 *
11060 * MenuToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar
11061 * is set up. Note that all tools must define an {@link OO.ui.Tool#onUpdateState onUpdateState} method if
11062 * a MenuToolGroup is used.
11063 *
11064 * @example
11065 * // Example of a MenuToolGroup
11066 * var toolFactory = new OO.ui.ToolFactory();
11067 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
11068 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
11069 *
11070 * // We will be placing status text in this element when tools are used
11071 * var $area = $( '<p>' ).text( 'An example of a MenuToolGroup. Select a tool from the dropdown menu.' );
11072 *
11073 * // Define the tools that we're going to place in our toolbar
11074 *
11075 * function SettingsTool() {
11076 * SettingsTool.super.apply( this, arguments );
11077 * this.reallyActive = false;
11078 * }
11079 * OO.inheritClass( SettingsTool, OO.ui.Tool );
11080 * SettingsTool.static.name = 'settings';
11081 * SettingsTool.static.icon = 'settings';
11082 * SettingsTool.static.title = 'Change settings';
11083 * SettingsTool.prototype.onSelect = function () {
11084 * $area.text( 'Settings tool clicked!' );
11085 * // Toggle the active state on each click
11086 * this.reallyActive = !this.reallyActive;
11087 * this.setActive( this.reallyActive );
11088 * // To update the menu label
11089 * this.toolbar.emit( 'updateState' );
11090 * };
11091 * SettingsTool.prototype.onUpdateState = function () {
11092 * };
11093 * toolFactory.register( SettingsTool );
11094 *
11095 * function StuffTool() {
11096 * StuffTool.super.apply( this, arguments );
11097 * this.reallyActive = false;
11098 * }
11099 * OO.inheritClass( StuffTool, OO.ui.Tool );
11100 * StuffTool.static.name = 'stuff';
11101 * StuffTool.static.icon = 'ellipsis';
11102 * StuffTool.static.title = 'More stuff';
11103 * StuffTool.prototype.onSelect = function () {
11104 * $area.text( 'More stuff tool clicked!' );
11105 * // Toggle the active state on each click
11106 * this.reallyActive = !this.reallyActive;
11107 * this.setActive( this.reallyActive );
11108 * // To update the menu label
11109 * this.toolbar.emit( 'updateState' );
11110 * };
11111 * StuffTool.prototype.onUpdateState = function () {
11112 * };
11113 * toolFactory.register( StuffTool );
11114 *
11115 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
11116 * // used once (but not all defined tools must be used).
11117 * toolbar.setup( [
11118 * {
11119 * type: 'menu',
11120 * header: 'This is the (optional) header',
11121 * title: 'This is the (optional) title',
11122 * indicator: 'down',
11123 * include: [ 'settings', 'stuff' ]
11124 * }
11125 * ] );
11126 *
11127 * // Create some UI around the toolbar and place it in the document
11128 * var frame = new OO.ui.PanelLayout( {
11129 * expanded: false,
11130 * framed: true
11131 * } );
11132 * var contentFrame = new OO.ui.PanelLayout( {
11133 * expanded: false,
11134 * padded: true
11135 * } );
11136 * frame.$element.append(
11137 * toolbar.$element,
11138 * contentFrame.$element.append( $area )
11139 * );
11140 * $( 'body' ).append( frame.$element );
11141 *
11142 * // Here is where the toolbar is actually built. This must be done after inserting it into the
11143 * // document.
11144 * toolbar.initialize();
11145 * toolbar.emit( 'updateState' );
11146 *
11147 * For more information about how to add tools to a MenuToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
11148 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki] [1].
11149 *
11150 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
11151 *
11152 * @class
11153 * @extends OO.ui.PopupToolGroup
11154 *
11155 * @constructor
11156 * @param {OO.ui.Toolbar} toolbar
11157 * @param {Object} [config] Configuration options
11158 */
11159 OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
11160 // Allow passing positional parameters inside the config object
11161 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
11162 config = toolbar;
11163 toolbar = config.toolbar;
11164 }
11165
11166 // Configuration initialization
11167 config = config || {};
11168
11169 // Parent constructor
11170 OO.ui.MenuToolGroup.super.call( this, toolbar, config );
11171
11172 // Events
11173 this.toolbar.connect( this, { updateState: 'onUpdateState' } );
11174
11175 // Initialization
11176 this.$element.addClass( 'oo-ui-menuToolGroup' );
11177 };
11178
11179 /* Setup */
11180
11181 OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup );
11182
11183 /* Static Properties */
11184
11185 OO.ui.MenuToolGroup.static.name = 'menu';
11186
11187 /* Methods */
11188
11189 /**
11190 * Handle the toolbar state being updated.
11191 *
11192 * When the state changes, the title of each active item in the menu will be joined together and
11193 * used as a label for the group. The label will be empty if none of the items are active.
11194 *
11195 * @private
11196 */
11197 OO.ui.MenuToolGroup.prototype.onUpdateState = function () {
11198 var name,
11199 labelTexts = [];
11200
11201 for ( name in this.tools ) {
11202 if ( this.tools[ name ].isActive() ) {
11203 labelTexts.push( this.tools[ name ].getTitle() );
11204 }
11205 }
11206
11207 this.setLabel( labelTexts.join( ', ' ) || ' ' );
11208 };
11209
11210 /**
11211 * Popup tools open a popup window when they are selected from the {@link OO.ui.Toolbar toolbar}. Each popup tool is configured
11212 * 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
11213 * an #onSelect or #onUpdateState method, as these methods have been implemented already.
11214 *
11215 * // Example of a popup tool. When selected, a popup tool displays
11216 * // a popup window.
11217 * function HelpTool( toolGroup, config ) {
11218 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
11219 * padded: true,
11220 * label: 'Help',
11221 * head: true
11222 * } }, config ) );
11223 * this.popup.$body.append( '<p>I am helpful!</p>' );
11224 * };
11225 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
11226 * HelpTool.static.name = 'help';
11227 * HelpTool.static.icon = 'help';
11228 * HelpTool.static.title = 'Help';
11229 * toolFactory.register( HelpTool );
11230 *
11231 * For an example of a toolbar that contains a popup tool, see {@link OO.ui.Toolbar toolbars}. For more information about
11232 * toolbars in genreral, please see the [OOjs UI documentation on MediaWiki][1].
11233 *
11234 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
11235 *
11236 * @abstract
11237 * @class
11238 * @extends OO.ui.Tool
11239 * @mixins OO.ui.mixin.PopupElement
11240 *
11241 * @constructor
11242 * @param {OO.ui.ToolGroup} toolGroup
11243 * @param {Object} [config] Configuration options
11244 */
11245 OO.ui.PopupTool = function OoUiPopupTool( toolGroup, config ) {
11246 // Allow passing positional parameters inside the config object
11247 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
11248 config = toolGroup;
11249 toolGroup = config.toolGroup;
11250 }
11251
11252 // Parent constructor
11253 OO.ui.PopupTool.super.call( this, toolGroup, config );
11254
11255 // Mixin constructors
11256 OO.ui.mixin.PopupElement.call( this, config );
11257
11258 // Initialization
11259 this.$element
11260 .addClass( 'oo-ui-popupTool' )
11261 .append( this.popup.$element );
11262 };
11263
11264 /* Setup */
11265
11266 OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool );
11267 OO.mixinClass( OO.ui.PopupTool, OO.ui.mixin.PopupElement );
11268
11269 /* Methods */
11270
11271 /**
11272 * Handle the tool being selected.
11273 *
11274 * @inheritdoc
11275 */
11276 OO.ui.PopupTool.prototype.onSelect = function () {
11277 if ( !this.isDisabled() ) {
11278 this.popup.toggle();
11279 }
11280 this.setActive( false );
11281 return false;
11282 };
11283
11284 /**
11285 * Handle the toolbar state being updated.
11286 *
11287 * @inheritdoc
11288 */
11289 OO.ui.PopupTool.prototype.onUpdateState = function () {
11290 this.setActive( false );
11291 };
11292
11293 /**
11294 * A ToolGroupTool is a special sort of tool that can contain other {@link OO.ui.Tool tools}
11295 * and {@link OO.ui.ToolGroup toolgroups}. The ToolGroupTool was specifically designed to be used
11296 * inside a {@link OO.ui.BarToolGroup bar} toolgroup to provide access to additional tools from
11297 * the bar item. Included tools will be displayed in a dropdown {@link OO.ui.ListToolGroup list}
11298 * when the ToolGroupTool is selected.
11299 *
11300 * // Example: ToolGroupTool with two nested tools, 'setting1' and 'setting2', defined elsewhere.
11301 *
11302 * function SettingsTool() {
11303 * SettingsTool.super.apply( this, arguments );
11304 * };
11305 * OO.inheritClass( SettingsTool, OO.ui.ToolGroupTool );
11306 * SettingsTool.static.name = 'settings';
11307 * SettingsTool.static.title = 'Change settings';
11308 * SettingsTool.static.groupConfig = {
11309 * icon: 'settings',
11310 * label: 'ToolGroupTool',
11311 * include: [ 'setting1', 'setting2' ]
11312 * };
11313 * toolFactory.register( SettingsTool );
11314 *
11315 * For more information, please see the [OOjs UI documentation on MediaWiki][1].
11316 *
11317 * Please note that this implementation is subject to change per [T74159] [2].
11318 *
11319 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars#ToolGroupTool
11320 * [2]: https://phabricator.wikimedia.org/T74159
11321 *
11322 * @abstract
11323 * @class
11324 * @extends OO.ui.Tool
11325 *
11326 * @constructor
11327 * @param {OO.ui.ToolGroup} toolGroup
11328 * @param {Object} [config] Configuration options
11329 */
11330 OO.ui.ToolGroupTool = function OoUiToolGroupTool( toolGroup, config ) {
11331 // Allow passing positional parameters inside the config object
11332 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
11333 config = toolGroup;
11334 toolGroup = config.toolGroup;
11335 }
11336
11337 // Parent constructor
11338 OO.ui.ToolGroupTool.super.call( this, toolGroup, config );
11339
11340 // Properties
11341 this.innerToolGroup = this.createGroup( this.constructor.static.groupConfig );
11342
11343 // Events
11344 this.innerToolGroup.connect( this, { disable: 'onToolGroupDisable' } );
11345
11346 // Initialization
11347 this.$link.remove();
11348 this.$element
11349 .addClass( 'oo-ui-toolGroupTool' )
11350 .append( this.innerToolGroup.$element );
11351 };
11352
11353 /* Setup */
11354
11355 OO.inheritClass( OO.ui.ToolGroupTool, OO.ui.Tool );
11356
11357 /* Static Properties */
11358
11359 /**
11360 * Toolgroup configuration.
11361 *
11362 * The toolgroup configuration consists of the tools to include, as well as an icon and label
11363 * to use for the bar item. Tools can be included by symbolic name, group, or with the
11364 * wildcard selector. Please see {@link OO.ui.ToolGroup toolgroup} for more information.
11365 *
11366 * @property {Object.<string,Array>}
11367 */
11368 OO.ui.ToolGroupTool.static.groupConfig = {};
11369
11370 /* Methods */
11371
11372 /**
11373 * Handle the tool being selected.
11374 *
11375 * @inheritdoc
11376 */
11377 OO.ui.ToolGroupTool.prototype.onSelect = function () {
11378 this.innerToolGroup.setActive( !this.innerToolGroup.active );
11379 return false;
11380 };
11381
11382 /**
11383 * Synchronize disabledness state of the tool with the inner toolgroup.
11384 *
11385 * @private
11386 * @param {boolean} disabled Element is disabled
11387 */
11388 OO.ui.ToolGroupTool.prototype.onToolGroupDisable = function ( disabled ) {
11389 this.setDisabled( disabled );
11390 };
11391
11392 /**
11393 * Handle the toolbar state being updated.
11394 *
11395 * @inheritdoc
11396 */
11397 OO.ui.ToolGroupTool.prototype.onUpdateState = function () {
11398 this.setActive( false );
11399 };
11400
11401 /**
11402 * Build a {@link OO.ui.ToolGroup toolgroup} from the specified configuration.
11403 *
11404 * @param {Object.<string,Array>} group Toolgroup configuration. Please see {@link OO.ui.ToolGroup toolgroup} for
11405 * more information.
11406 * @return {OO.ui.ListToolGroup}
11407 */
11408 OO.ui.ToolGroupTool.prototype.createGroup = function ( group ) {
11409 if ( group.include === '*' ) {
11410 // Apply defaults to catch-all groups
11411 if ( group.label === undefined ) {
11412 group.label = OO.ui.msg( 'ooui-toolbar-more' );
11413 }
11414 }
11415
11416 return this.toolbar.getToolGroupFactory().create( 'list', this.toolbar, group );
11417 };
11418
11419 /**
11420 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
11421 *
11422 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
11423 *
11424 * @private
11425 * @abstract
11426 * @class
11427 * @extends OO.ui.mixin.GroupElement
11428 *
11429 * @constructor
11430 * @param {Object} [config] Configuration options
11431 */
11432 OO.ui.mixin.GroupWidget = function OoUiMixinGroupWidget( config ) {
11433 // Parent constructor
11434 OO.ui.mixin.GroupWidget.super.call( this, config );
11435 };
11436
11437 /* Setup */
11438
11439 OO.inheritClass( OO.ui.mixin.GroupWidget, OO.ui.mixin.GroupElement );
11440
11441 /* Methods */
11442
11443 /**
11444 * Set the disabled state of the widget.
11445 *
11446 * This will also update the disabled state of child widgets.
11447 *
11448 * @param {boolean} disabled Disable widget
11449 * @chainable
11450 */
11451 OO.ui.mixin.GroupWidget.prototype.setDisabled = function ( disabled ) {
11452 var i, len;
11453
11454 // Parent method
11455 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
11456 OO.ui.Widget.prototype.setDisabled.call( this, disabled );
11457
11458 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
11459 if ( this.items ) {
11460 for ( i = 0, len = this.items.length; i < len; i++ ) {
11461 this.items[ i ].updateDisabled();
11462 }
11463 }
11464
11465 return this;
11466 };
11467
11468 /**
11469 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
11470 *
11471 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
11472 * allows bidirectional communication.
11473 *
11474 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
11475 *
11476 * @private
11477 * @abstract
11478 * @class
11479 *
11480 * @constructor
11481 */
11482 OO.ui.mixin.ItemWidget = function OoUiMixinItemWidget() {
11483 //
11484 };
11485
11486 /* Methods */
11487
11488 /**
11489 * Check if widget is disabled.
11490 *
11491 * Checks parent if present, making disabled state inheritable.
11492 *
11493 * @return {boolean} Widget is disabled
11494 */
11495 OO.ui.mixin.ItemWidget.prototype.isDisabled = function () {
11496 return this.disabled ||
11497 ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() );
11498 };
11499
11500 /**
11501 * Set group element is in.
11502 *
11503 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
11504 * @chainable
11505 */
11506 OO.ui.mixin.ItemWidget.prototype.setElementGroup = function ( group ) {
11507 // Parent method
11508 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
11509 OO.ui.Element.prototype.setElementGroup.call( this, group );
11510
11511 // Initialize item disabled states
11512 this.updateDisabled();
11513
11514 return this;
11515 };
11516
11517 /**
11518 * OutlineControlsWidget is a set of controls for an {@link OO.ui.OutlineSelectWidget outline select widget}.
11519 * Controls include moving items up and down, removing items, and adding different kinds of items.
11520 * ####Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.####
11521 *
11522 * @class
11523 * @extends OO.ui.Widget
11524 * @mixins OO.ui.mixin.GroupElement
11525 * @mixins OO.ui.mixin.IconElement
11526 *
11527 * @constructor
11528 * @param {OO.ui.OutlineSelectWidget} outline Outline to control
11529 * @param {Object} [config] Configuration options
11530 * @cfg {Object} [abilities] List of abilties
11531 * @cfg {boolean} [abilities.move=true] Allow moving movable items
11532 * @cfg {boolean} [abilities.remove=true] Allow removing removable items
11533 */
11534 OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
11535 // Allow passing positional parameters inside the config object
11536 if ( OO.isPlainObject( outline ) && config === undefined ) {
11537 config = outline;
11538 outline = config.outline;
11539 }
11540
11541 // Configuration initialization
11542 config = $.extend( { icon: 'add' }, config );
11543
11544 // Parent constructor
11545 OO.ui.OutlineControlsWidget.super.call( this, config );
11546
11547 // Mixin constructors
11548 OO.ui.mixin.GroupElement.call( this, config );
11549 OO.ui.mixin.IconElement.call( this, config );
11550
11551 // Properties
11552 this.outline = outline;
11553 this.$movers = $( '<div>' );
11554 this.upButton = new OO.ui.ButtonWidget( {
11555 framed: false,
11556 icon: 'collapse',
11557 title: OO.ui.msg( 'ooui-outline-control-move-up' )
11558 } );
11559 this.downButton = new OO.ui.ButtonWidget( {
11560 framed: false,
11561 icon: 'expand',
11562 title: OO.ui.msg( 'ooui-outline-control-move-down' )
11563 } );
11564 this.removeButton = new OO.ui.ButtonWidget( {
11565 framed: false,
11566 icon: 'remove',
11567 title: OO.ui.msg( 'ooui-outline-control-remove' )
11568 } );
11569 this.abilities = { move: true, remove: true };
11570
11571 // Events
11572 outline.connect( this, {
11573 select: 'onOutlineChange',
11574 add: 'onOutlineChange',
11575 remove: 'onOutlineChange'
11576 } );
11577 this.upButton.connect( this, { click: [ 'emit', 'move', -1 ] } );
11578 this.downButton.connect( this, { click: [ 'emit', 'move', 1 ] } );
11579 this.removeButton.connect( this, { click: [ 'emit', 'remove' ] } );
11580
11581 // Initialization
11582 this.$element.addClass( 'oo-ui-outlineControlsWidget' );
11583 this.$group.addClass( 'oo-ui-outlineControlsWidget-items' );
11584 this.$movers
11585 .addClass( 'oo-ui-outlineControlsWidget-movers' )
11586 .append( this.removeButton.$element, this.upButton.$element, this.downButton.$element );
11587 this.$element.append( this.$icon, this.$group, this.$movers );
11588 this.setAbilities( config.abilities || {} );
11589 };
11590
11591 /* Setup */
11592
11593 OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget );
11594 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.mixin.GroupElement );
11595 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.mixin.IconElement );
11596
11597 /* Events */
11598
11599 /**
11600 * @event move
11601 * @param {number} places Number of places to move
11602 */
11603
11604 /**
11605 * @event remove
11606 */
11607
11608 /* Methods */
11609
11610 /**
11611 * Set abilities.
11612 *
11613 * @param {Object} abilities List of abilties
11614 * @param {boolean} [abilities.move] Allow moving movable items
11615 * @param {boolean} [abilities.remove] Allow removing removable items
11616 */
11617 OO.ui.OutlineControlsWidget.prototype.setAbilities = function ( abilities ) {
11618 var ability;
11619
11620 for ( ability in this.abilities ) {
11621 if ( abilities[ability] !== undefined ) {
11622 this.abilities[ability] = !!abilities[ability];
11623 }
11624 }
11625
11626 this.onOutlineChange();
11627 };
11628
11629 /**
11630 *
11631 * @private
11632 * Handle outline change events.
11633 */
11634 OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () {
11635 var i, len, firstMovable, lastMovable,
11636 items = this.outline.getItems(),
11637 selectedItem = this.outline.getSelectedItem(),
11638 movable = this.abilities.move && selectedItem && selectedItem.isMovable(),
11639 removable = this.abilities.remove && selectedItem && selectedItem.isRemovable();
11640
11641 if ( movable ) {
11642 i = -1;
11643 len = items.length;
11644 while ( ++i < len ) {
11645 if ( items[ i ].isMovable() ) {
11646 firstMovable = items[ i ];
11647 break;
11648 }
11649 }
11650 i = len;
11651 while ( i-- ) {
11652 if ( items[ i ].isMovable() ) {
11653 lastMovable = items[ i ];
11654 break;
11655 }
11656 }
11657 }
11658 this.upButton.setDisabled( !movable || selectedItem === firstMovable );
11659 this.downButton.setDisabled( !movable || selectedItem === lastMovable );
11660 this.removeButton.setDisabled( !removable );
11661 };
11662
11663 /**
11664 * ToggleWidget implements basic behavior of widgets with an on/off state.
11665 * Please see OO.ui.ToggleButtonWidget and OO.ui.ToggleSwitchWidget for examples.
11666 *
11667 * @abstract
11668 * @class
11669 * @extends OO.ui.Widget
11670 *
11671 * @constructor
11672 * @param {Object} [config] Configuration options
11673 * @cfg {boolean} [value=false] The toggle’s initial on/off state.
11674 * By default, the toggle is in the 'off' state.
11675 */
11676 OO.ui.ToggleWidget = function OoUiToggleWidget( config ) {
11677 // Configuration initialization
11678 config = config || {};
11679
11680 // Parent constructor
11681 OO.ui.ToggleWidget.super.call( this, config );
11682
11683 // Properties
11684 this.value = null;
11685
11686 // Initialization
11687 this.$element.addClass( 'oo-ui-toggleWidget' );
11688 this.setValue( !!config.value );
11689 };
11690
11691 /* Setup */
11692
11693 OO.inheritClass( OO.ui.ToggleWidget, OO.ui.Widget );
11694
11695 /* Events */
11696
11697 /**
11698 * @event change
11699 *
11700 * A change event is emitted when the on/off state of the toggle changes.
11701 *
11702 * @param {boolean} value Value representing the new state of the toggle
11703 */
11704
11705 /* Methods */
11706
11707 /**
11708 * Get the value representing the toggle’s state.
11709 *
11710 * @return {boolean} The on/off state of the toggle
11711 */
11712 OO.ui.ToggleWidget.prototype.getValue = function () {
11713 return this.value;
11714 };
11715
11716 /**
11717 * Set the state of the toggle: `true` for 'on', `false' for 'off'.
11718 *
11719 * @param {boolean} value The state of the toggle
11720 * @fires change
11721 * @chainable
11722 */
11723 OO.ui.ToggleWidget.prototype.setValue = function ( value ) {
11724 value = !!value;
11725 if ( this.value !== value ) {
11726 this.value = value;
11727 this.emit( 'change', value );
11728 this.$element.toggleClass( 'oo-ui-toggleWidget-on', value );
11729 this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value );
11730 this.$element.attr( 'aria-checked', value.toString() );
11731 }
11732 return this;
11733 };
11734
11735 /**
11736 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
11737 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
11738 * removed, and cleared from the group.
11739 *
11740 * @example
11741 * // Example: A ButtonGroupWidget with two buttons
11742 * var button1 = new OO.ui.PopupButtonWidget( {
11743 * label: 'Select a category',
11744 * icon: 'menu',
11745 * popup: {
11746 * $content: $( '<p>List of categories...</p>' ),
11747 * padded: true,
11748 * align: 'left'
11749 * }
11750 * } );
11751 * var button2 = new OO.ui.ButtonWidget( {
11752 * label: 'Add item'
11753 * });
11754 * var buttonGroup = new OO.ui.ButtonGroupWidget( {
11755 * items: [button1, button2]
11756 * } );
11757 * $( 'body' ).append( buttonGroup.$element );
11758 *
11759 * @class
11760 * @extends OO.ui.Widget
11761 * @mixins OO.ui.mixin.GroupElement
11762 *
11763 * @constructor
11764 * @param {Object} [config] Configuration options
11765 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
11766 */
11767 OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
11768 // Configuration initialization
11769 config = config || {};
11770
11771 // Parent constructor
11772 OO.ui.ButtonGroupWidget.super.call( this, config );
11773
11774 // Mixin constructors
11775 OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
11776
11777 // Initialization
11778 this.$element.addClass( 'oo-ui-buttonGroupWidget' );
11779 if ( Array.isArray( config.items ) ) {
11780 this.addItems( config.items );
11781 }
11782 };
11783
11784 /* Setup */
11785
11786 OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget );
11787 OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.mixin.GroupElement );
11788
11789 /**
11790 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
11791 * feels, and functionality can be customized via the class’s configuration options
11792 * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
11793 * and examples.
11794 *
11795 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
11796 *
11797 * @example
11798 * // A button widget
11799 * var button = new OO.ui.ButtonWidget( {
11800 * label: 'Button with Icon',
11801 * icon: 'remove',
11802 * iconTitle: 'Remove'
11803 * } );
11804 * $( 'body' ).append( button.$element );
11805 *
11806 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
11807 *
11808 * @class
11809 * @extends OO.ui.Widget
11810 * @mixins OO.ui.mixin.ButtonElement
11811 * @mixins OO.ui.mixin.IconElement
11812 * @mixins OO.ui.mixin.IndicatorElement
11813 * @mixins OO.ui.mixin.LabelElement
11814 * @mixins OO.ui.mixin.TitledElement
11815 * @mixins OO.ui.mixin.FlaggedElement
11816 * @mixins OO.ui.mixin.TabIndexedElement
11817 *
11818 * @constructor
11819 * @param {Object} [config] Configuration options
11820 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
11821 * @cfg {string} [target] The frame or window in which to open the hyperlink.
11822 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
11823 */
11824 OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
11825 // Configuration initialization
11826 config = config || {};
11827
11828 // Parent constructor
11829 OO.ui.ButtonWidget.super.call( this, config );
11830
11831 // Mixin constructors
11832 OO.ui.mixin.ButtonElement.call( this, config );
11833 OO.ui.mixin.IconElement.call( this, config );
11834 OO.ui.mixin.IndicatorElement.call( this, config );
11835 OO.ui.mixin.LabelElement.call( this, config );
11836 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
11837 OO.ui.mixin.FlaggedElement.call( this, config );
11838 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
11839
11840 // Properties
11841 this.href = null;
11842 this.target = null;
11843 this.noFollow = false;
11844
11845 // Events
11846 this.connect( this, { disable: 'onDisable' } );
11847
11848 // Initialization
11849 this.$button.append( this.$icon, this.$label, this.$indicator );
11850 this.$element
11851 .addClass( 'oo-ui-buttonWidget' )
11852 .append( this.$button );
11853 this.setHref( config.href );
11854 this.setTarget( config.target );
11855 this.setNoFollow( config.noFollow );
11856 };
11857
11858 /* Setup */
11859
11860 OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget );
11861 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.ButtonElement );
11862 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.IconElement );
11863 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.IndicatorElement );
11864 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.LabelElement );
11865 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.TitledElement );
11866 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.FlaggedElement );
11867 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.TabIndexedElement );
11868
11869 /* Methods */
11870
11871 /**
11872 * @inheritdoc
11873 */
11874 OO.ui.ButtonWidget.prototype.onMouseDown = function ( e ) {
11875 if ( !this.isDisabled() ) {
11876 // Remove the tab-index while the button is down to prevent the button from stealing focus
11877 this.$button.removeAttr( 'tabindex' );
11878 }
11879
11880 return OO.ui.mixin.ButtonElement.prototype.onMouseDown.call( this, e );
11881 };
11882
11883 /**
11884 * @inheritdoc
11885 */
11886 OO.ui.ButtonWidget.prototype.onMouseUp = function ( e ) {
11887 if ( !this.isDisabled() ) {
11888 // Restore the tab-index after the button is up to restore the button's accessibility
11889 this.$button.attr( 'tabindex', this.tabIndex );
11890 }
11891
11892 return OO.ui.mixin.ButtonElement.prototype.onMouseUp.call( this, e );
11893 };
11894
11895 /**
11896 * Get hyperlink location.
11897 *
11898 * @return {string} Hyperlink location
11899 */
11900 OO.ui.ButtonWidget.prototype.getHref = function () {
11901 return this.href;
11902 };
11903
11904 /**
11905 * Get hyperlink target.
11906 *
11907 * @return {string} Hyperlink target
11908 */
11909 OO.ui.ButtonWidget.prototype.getTarget = function () {
11910 return this.target;
11911 };
11912
11913 /**
11914 * Get search engine traversal hint.
11915 *
11916 * @return {boolean} Whether search engines should avoid traversing this hyperlink
11917 */
11918 OO.ui.ButtonWidget.prototype.getNoFollow = function () {
11919 return this.noFollow;
11920 };
11921
11922 /**
11923 * Set hyperlink location.
11924 *
11925 * @param {string|null} href Hyperlink location, null to remove
11926 */
11927 OO.ui.ButtonWidget.prototype.setHref = function ( href ) {
11928 href = typeof href === 'string' ? href : null;
11929
11930 if ( href !== this.href ) {
11931 this.href = href;
11932 this.updateHref();
11933 }
11934
11935 return this;
11936 };
11937
11938 /**
11939 * Update the `href` attribute, in case of changes to href or
11940 * disabled state.
11941 *
11942 * @private
11943 * @chainable
11944 */
11945 OO.ui.ButtonWidget.prototype.updateHref = function () {
11946 if ( this.href !== null && !this.isDisabled() ) {
11947 this.$button.attr( 'href', this.href );
11948 } else {
11949 this.$button.removeAttr( 'href' );
11950 }
11951
11952 return this;
11953 };
11954
11955 /**
11956 * Handle disable events.
11957 *
11958 * @private
11959 * @param {boolean} disabled Element is disabled
11960 */
11961 OO.ui.ButtonWidget.prototype.onDisable = function () {
11962 this.updateHref();
11963 };
11964
11965 /**
11966 * Set hyperlink target.
11967 *
11968 * @param {string|null} target Hyperlink target, null to remove
11969 */
11970 OO.ui.ButtonWidget.prototype.setTarget = function ( target ) {
11971 target = typeof target === 'string' ? target : null;
11972
11973 if ( target !== this.target ) {
11974 this.target = target;
11975 if ( target !== null ) {
11976 this.$button.attr( 'target', target );
11977 } else {
11978 this.$button.removeAttr( 'target' );
11979 }
11980 }
11981
11982 return this;
11983 };
11984
11985 /**
11986 * Set search engine traversal hint.
11987 *
11988 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
11989 */
11990 OO.ui.ButtonWidget.prototype.setNoFollow = function ( noFollow ) {
11991 noFollow = typeof noFollow === 'boolean' ? noFollow : true;
11992
11993 if ( noFollow !== this.noFollow ) {
11994 this.noFollow = noFollow;
11995 if ( noFollow ) {
11996 this.$button.attr( 'rel', 'nofollow' );
11997 } else {
11998 this.$button.removeAttr( 'rel' );
11999 }
12000 }
12001
12002 return this;
12003 };
12004
12005 /**
12006 * An ActionWidget is a {@link OO.ui.ButtonWidget button widget} that executes an action.
12007 * Action widgets are used with OO.ui.ActionSet, which manages the behavior and availability
12008 * of the actions.
12009 *
12010 * Both actions and action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
12011 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information
12012 * and examples.
12013 *
12014 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
12015 *
12016 * @class
12017 * @extends OO.ui.ButtonWidget
12018 * @mixins OO.ui.mixin.PendingElement
12019 *
12020 * @constructor
12021 * @param {Object} [config] Configuration options
12022 * @cfg {string} [action] Symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
12023 * @cfg {string[]} [modes] Symbolic names of the modes (e.g., ‘edit’ or ‘read’) in which the action
12024 * should be made available. See the action set's {@link OO.ui.ActionSet#setMode setMode} method
12025 * for more information about setting modes.
12026 * @cfg {boolean} [framed=false] Render the action button with a frame
12027 */
12028 OO.ui.ActionWidget = function OoUiActionWidget( config ) {
12029 // Configuration initialization
12030 config = $.extend( { framed: false }, config );
12031
12032 // Parent constructor
12033 OO.ui.ActionWidget.super.call( this, config );
12034
12035 // Mixin constructors
12036 OO.ui.mixin.PendingElement.call( this, config );
12037
12038 // Properties
12039 this.action = config.action || '';
12040 this.modes = config.modes || [];
12041 this.width = 0;
12042 this.height = 0;
12043
12044 // Initialization
12045 this.$element.addClass( 'oo-ui-actionWidget' );
12046 };
12047
12048 /* Setup */
12049
12050 OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget );
12051 OO.mixinClass( OO.ui.ActionWidget, OO.ui.mixin.PendingElement );
12052
12053 /* Events */
12054
12055 /**
12056 * A resize event is emitted when the size of the widget changes.
12057 *
12058 * @event resize
12059 */
12060
12061 /* Methods */
12062
12063 /**
12064 * Check if the action is configured to be available in the specified `mode`.
12065 *
12066 * @param {string} mode Name of mode
12067 * @return {boolean} The action is configured with the mode
12068 */
12069 OO.ui.ActionWidget.prototype.hasMode = function ( mode ) {
12070 return this.modes.indexOf( mode ) !== -1;
12071 };
12072
12073 /**
12074 * Get the symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
12075 *
12076 * @return {string}
12077 */
12078 OO.ui.ActionWidget.prototype.getAction = function () {
12079 return this.action;
12080 };
12081
12082 /**
12083 * Get the symbolic name of the mode or modes for which the action is configured to be available.
12084 *
12085 * The current mode is set with the action set's {@link OO.ui.ActionSet#setMode setMode} method.
12086 * Only actions that are configured to be avaiable in the current mode will be visible. All other actions
12087 * are hidden.
12088 *
12089 * @return {string[]}
12090 */
12091 OO.ui.ActionWidget.prototype.getModes = function () {
12092 return this.modes.slice();
12093 };
12094
12095 /**
12096 * Emit a resize event if the size has changed.
12097 *
12098 * @private
12099 * @chainable
12100 */
12101 OO.ui.ActionWidget.prototype.propagateResize = function () {
12102 var width, height;
12103
12104 if ( this.isElementAttached() ) {
12105 width = this.$element.width();
12106 height = this.$element.height();
12107
12108 if ( width !== this.width || height !== this.height ) {
12109 this.width = width;
12110 this.height = height;
12111 this.emit( 'resize' );
12112 }
12113 }
12114
12115 return this;
12116 };
12117
12118 /**
12119 * @inheritdoc
12120 */
12121 OO.ui.ActionWidget.prototype.setIcon = function () {
12122 // Mixin method
12123 OO.ui.mixin.IconElement.prototype.setIcon.apply( this, arguments );
12124 this.propagateResize();
12125
12126 return this;
12127 };
12128
12129 /**
12130 * @inheritdoc
12131 */
12132 OO.ui.ActionWidget.prototype.setLabel = function () {
12133 // Mixin method
12134 OO.ui.mixin.LabelElement.prototype.setLabel.apply( this, arguments );
12135 this.propagateResize();
12136
12137 return this;
12138 };
12139
12140 /**
12141 * @inheritdoc
12142 */
12143 OO.ui.ActionWidget.prototype.setFlags = function () {
12144 // Mixin method
12145 OO.ui.mixin.FlaggedElement.prototype.setFlags.apply( this, arguments );
12146 this.propagateResize();
12147
12148 return this;
12149 };
12150
12151 /**
12152 * @inheritdoc
12153 */
12154 OO.ui.ActionWidget.prototype.clearFlags = function () {
12155 // Mixin method
12156 OO.ui.mixin.FlaggedElement.prototype.clearFlags.apply( this, arguments );
12157 this.propagateResize();
12158
12159 return this;
12160 };
12161
12162 /**
12163 * Toggle the visibility of the action button.
12164 *
12165 * @param {boolean} [show] Show button, omit to toggle visibility
12166 * @chainable
12167 */
12168 OO.ui.ActionWidget.prototype.toggle = function () {
12169 // Parent method
12170 OO.ui.ActionWidget.super.prototype.toggle.apply( this, arguments );
12171 this.propagateResize();
12172
12173 return this;
12174 };
12175
12176 /**
12177 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
12178 * which is used to display additional information or options.
12179 *
12180 * @example
12181 * // Example of a popup button.
12182 * var popupButton = new OO.ui.PopupButtonWidget( {
12183 * label: 'Popup button with options',
12184 * icon: 'menu',
12185 * popup: {
12186 * $content: $( '<p>Additional options here.</p>' ),
12187 * padded: true,
12188 * align: 'force-left'
12189 * }
12190 * } );
12191 * // Append the button to the DOM.
12192 * $( 'body' ).append( popupButton.$element );
12193 *
12194 * @class
12195 * @extends OO.ui.ButtonWidget
12196 * @mixins OO.ui.mixin.PopupElement
12197 *
12198 * @constructor
12199 * @param {Object} [config] Configuration options
12200 */
12201 OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
12202 // Parent constructor
12203 OO.ui.PopupButtonWidget.super.call( this, config );
12204
12205 // Mixin constructors
12206 OO.ui.mixin.PopupElement.call( this, config );
12207
12208 // Events
12209 this.connect( this, { click: 'onAction' } );
12210
12211 // Initialization
12212 this.$element
12213 .addClass( 'oo-ui-popupButtonWidget' )
12214 .attr( 'aria-haspopup', 'true' )
12215 .append( this.popup.$element );
12216 };
12217
12218 /* Setup */
12219
12220 OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
12221 OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.mixin.PopupElement );
12222
12223 /* Methods */
12224
12225 /**
12226 * Handle the button action being triggered.
12227 *
12228 * @private
12229 */
12230 OO.ui.PopupButtonWidget.prototype.onAction = function () {
12231 this.popup.toggle();
12232 };
12233
12234 /**
12235 * ToggleButtons are buttons that have a state (‘on’ or ‘off’) that is represented by a
12236 * Boolean value. Like other {@link OO.ui.ButtonWidget buttons}, toggle buttons can be
12237 * configured with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators},
12238 * {@link OO.ui.mixin.TitledElement titles}, {@link OO.ui.mixin.FlaggedElement styling flags},
12239 * and {@link OO.ui.mixin.LabelElement labels}. Please see
12240 * the [OOjs UI documentation][1] on MediaWiki for more information.
12241 *
12242 * @example
12243 * // Toggle buttons in the 'off' and 'on' state.
12244 * var toggleButton1 = new OO.ui.ToggleButtonWidget( {
12245 * label: 'Toggle Button off'
12246 * } );
12247 * var toggleButton2 = new OO.ui.ToggleButtonWidget( {
12248 * label: 'Toggle Button on',
12249 * value: true
12250 * } );
12251 * // Append the buttons to the DOM.
12252 * $( 'body' ).append( toggleButton1.$element, toggleButton2.$element );
12253 *
12254 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Toggle_buttons
12255 *
12256 * @class
12257 * @extends OO.ui.ToggleWidget
12258 * @mixins OO.ui.mixin.ButtonElement
12259 * @mixins OO.ui.mixin.IconElement
12260 * @mixins OO.ui.mixin.IndicatorElement
12261 * @mixins OO.ui.mixin.LabelElement
12262 * @mixins OO.ui.mixin.TitledElement
12263 * @mixins OO.ui.mixin.FlaggedElement
12264 * @mixins OO.ui.mixin.TabIndexedElement
12265 *
12266 * @constructor
12267 * @param {Object} [config] Configuration options
12268 * @cfg {boolean} [value=false] The toggle button’s initial on/off
12269 * state. By default, the button is in the 'off' state.
12270 */
12271 OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) {
12272 // Configuration initialization
12273 config = config || {};
12274
12275 // Parent constructor
12276 OO.ui.ToggleButtonWidget.super.call( this, config );
12277
12278 // Mixin constructors
12279 OO.ui.mixin.ButtonElement.call( this, config );
12280 OO.ui.mixin.IconElement.call( this, config );
12281 OO.ui.mixin.IndicatorElement.call( this, config );
12282 OO.ui.mixin.LabelElement.call( this, config );
12283 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
12284 OO.ui.mixin.FlaggedElement.call( this, config );
12285 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
12286
12287 // Events
12288 this.connect( this, { click: 'onAction' } );
12289
12290 // Initialization
12291 this.$button.append( this.$icon, this.$label, this.$indicator );
12292 this.$element
12293 .addClass( 'oo-ui-toggleButtonWidget' )
12294 .append( this.$button );
12295 };
12296
12297 /* Setup */
12298
12299 OO.inheritClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget );
12300 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.ButtonElement );
12301 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.IconElement );
12302 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.IndicatorElement );
12303 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.LabelElement );
12304 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.TitledElement );
12305 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.FlaggedElement );
12306 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.TabIndexedElement );
12307
12308 /* Methods */
12309
12310 /**
12311 * Handle the button action being triggered.
12312 *
12313 * @private
12314 */
12315 OO.ui.ToggleButtonWidget.prototype.onAction = function () {
12316 this.setValue( !this.value );
12317 };
12318
12319 /**
12320 * @inheritdoc
12321 */
12322 OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
12323 value = !!value;
12324 if ( value !== this.value ) {
12325 // Might be called from parent constructor before ButtonElement constructor
12326 if ( this.$button ) {
12327 this.$button.attr( 'aria-pressed', value.toString() );
12328 }
12329 this.setActive( value );
12330 }
12331
12332 // Parent method
12333 OO.ui.ToggleButtonWidget.super.prototype.setValue.call( this, value );
12334
12335 return this;
12336 };
12337
12338 /**
12339 * @inheritdoc
12340 */
12341 OO.ui.ToggleButtonWidget.prototype.setButtonElement = function ( $button ) {
12342 if ( this.$button ) {
12343 this.$button.removeAttr( 'aria-pressed' );
12344 }
12345 OO.ui.mixin.ButtonElement.prototype.setButtonElement.call( this, $button );
12346 this.$button.attr( 'aria-pressed', this.value.toString() );
12347 };
12348
12349 /**
12350 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
12351 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
12352 * users can interact with it.
12353 *
12354 * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
12355 * OO.ui.DropdownInputWidget instead.
12356 *
12357 * @example
12358 * // Example: A DropdownWidget with a menu that contains three options
12359 * var dropDown = new OO.ui.DropdownWidget( {
12360 * label: 'Dropdown menu: Select a menu option',
12361 * menu: {
12362 * items: [
12363 * new OO.ui.MenuOptionWidget( {
12364 * data: 'a',
12365 * label: 'First'
12366 * } ),
12367 * new OO.ui.MenuOptionWidget( {
12368 * data: 'b',
12369 * label: 'Second'
12370 * } ),
12371 * new OO.ui.MenuOptionWidget( {
12372 * data: 'c',
12373 * label: 'Third'
12374 * } )
12375 * ]
12376 * }
12377 * } );
12378 *
12379 * $( 'body' ).append( dropDown.$element );
12380 *
12381 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
12382 *
12383 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
12384 *
12385 * @class
12386 * @extends OO.ui.Widget
12387 * @mixins OO.ui.mixin.IconElement
12388 * @mixins OO.ui.mixin.IndicatorElement
12389 * @mixins OO.ui.mixin.LabelElement
12390 * @mixins OO.ui.mixin.TitledElement
12391 * @mixins OO.ui.mixin.TabIndexedElement
12392 *
12393 * @constructor
12394 * @param {Object} [config] Configuration options
12395 * @cfg {Object} [menu] Configuration options to pass to menu widget
12396 */
12397 OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
12398 // Configuration initialization
12399 config = $.extend( { indicator: 'down' }, config );
12400
12401 // Parent constructor
12402 OO.ui.DropdownWidget.super.call( this, config );
12403
12404 // Properties (must be set before TabIndexedElement constructor call)
12405 this.$handle = this.$( '<span>' );
12406
12407 // Mixin constructors
12408 OO.ui.mixin.IconElement.call( this, config );
12409 OO.ui.mixin.IndicatorElement.call( this, config );
12410 OO.ui.mixin.LabelElement.call( this, config );
12411 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
12412 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
12413
12414 // Properties
12415 this.menu = new OO.ui.MenuSelectWidget( $.extend( { widget: this }, config.menu ) );
12416
12417 // Events
12418 this.$handle.on( {
12419 click: this.onClick.bind( this ),
12420 keypress: this.onKeyPress.bind( this )
12421 } );
12422 this.menu.connect( this, { select: 'onMenuSelect' } );
12423
12424 // Initialization
12425 this.$handle
12426 .addClass( 'oo-ui-dropdownWidget-handle' )
12427 .append( this.$icon, this.$label, this.$indicator );
12428 this.$element
12429 .addClass( 'oo-ui-dropdownWidget' )
12430 .append( this.$handle, this.menu.$element );
12431 };
12432
12433 /* Setup */
12434
12435 OO.inheritClass( OO.ui.DropdownWidget, OO.ui.Widget );
12436 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.IconElement );
12437 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.IndicatorElement );
12438 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.LabelElement );
12439 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.TitledElement );
12440 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.TabIndexedElement );
12441
12442 /* Methods */
12443
12444 /**
12445 * Get the menu.
12446 *
12447 * @return {OO.ui.MenuSelectWidget} Menu of widget
12448 */
12449 OO.ui.DropdownWidget.prototype.getMenu = function () {
12450 return this.menu;
12451 };
12452
12453 /**
12454 * Handles menu select events.
12455 *
12456 * @private
12457 * @param {OO.ui.MenuOptionWidget} item Selected menu item
12458 */
12459 OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
12460 var selectedLabel;
12461
12462 if ( !item ) {
12463 this.setLabel( null );
12464 return;
12465 }
12466
12467 selectedLabel = item.getLabel();
12468
12469 // If the label is a DOM element, clone it, because setLabel will append() it
12470 if ( selectedLabel instanceof jQuery ) {
12471 selectedLabel = selectedLabel.clone();
12472 }
12473
12474 this.setLabel( selectedLabel );
12475 };
12476
12477 /**
12478 * Handle mouse click events.
12479 *
12480 * @private
12481 * @param {jQuery.Event} e Mouse click event
12482 */
12483 OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
12484 if ( !this.isDisabled() && e.which === 1 ) {
12485 this.menu.toggle();
12486 }
12487 return false;
12488 };
12489
12490 /**
12491 * Handle key press events.
12492 *
12493 * @private
12494 * @param {jQuery.Event} e Key press event
12495 */
12496 OO.ui.DropdownWidget.prototype.onKeyPress = function ( e ) {
12497 if ( !this.isDisabled() &&
12498 ( ( e.which === OO.ui.Keys.SPACE && !this.menu.isVisible() ) || e.which === OO.ui.Keys.ENTER )
12499 ) {
12500 this.menu.toggle();
12501 return false;
12502 }
12503 };
12504
12505 /**
12506 * SelectFileWidgets allow for selecting files, using the HTML5 File API. These
12507 * widgets can be configured with {@link OO.ui.mixin.IconElement icons} and {@link
12508 * OO.ui.mixin.IndicatorElement indicators}.
12509 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
12510 *
12511 * @example
12512 * // Example of a file select widget
12513 * var selectFile = new OO.ui.SelectFileWidget();
12514 * $( 'body' ).append( selectFile.$element );
12515 *
12516 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets
12517 *
12518 * @class
12519 * @extends OO.ui.Widget
12520 * @mixins OO.ui.mixin.IconElement
12521 * @mixins OO.ui.mixin.IndicatorElement
12522 * @mixins OO.ui.mixin.PendingElement
12523 * @mixins OO.ui.mixin.LabelElement
12524 * @mixins OO.ui.mixin.TabIndexedElement
12525 *
12526 * @constructor
12527 * @param {Object} [config] Configuration options
12528 * @cfg {string[]|null} [accept=null] MIME types to accept. null accepts all types.
12529 * @cfg {string} [placeholder] Text to display when no file is selected.
12530 * @cfg {string} [notsupported] Text to display when file support is missing in the browser.
12531 * @cfg {boolean} [droppable=true] Whether to accept files by drag and drop.
12532 */
12533 OO.ui.SelectFileWidget = function OoUiSelectFileWidget( config ) {
12534 var dragHandler;
12535
12536 // Configuration initialization
12537 config = $.extend( {
12538 accept: null,
12539 placeholder: OO.ui.msg( 'ooui-selectfile-placeholder' ),
12540 notsupported: OO.ui.msg( 'ooui-selectfile-not-supported' ),
12541 droppable: true
12542 }, config );
12543
12544 // Parent constructor
12545 OO.ui.SelectFileWidget.super.call( this, config );
12546
12547 // Properties (must be set before TabIndexedElement constructor call)
12548 this.$handle = $( '<span>' );
12549
12550 // Mixin constructors
12551 OO.ui.mixin.IconElement.call( this, config );
12552 OO.ui.mixin.IndicatorElement.call( this, config );
12553 OO.ui.mixin.PendingElement.call( this, config );
12554 OO.ui.mixin.LabelElement.call( this, $.extend( config, { autoFitLabel: true } ) );
12555 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
12556
12557 // Properties
12558 this.isSupported = this.constructor.static.isSupported();
12559 this.currentFile = null;
12560 if ( Array.isArray( config.accept ) ) {
12561 this.accept = config.accept;
12562 } else {
12563 this.accept = null;
12564 }
12565 this.placeholder = config.placeholder;
12566 this.notsupported = config.notsupported;
12567 this.onFileSelectedHandler = this.onFileSelected.bind( this );
12568
12569 this.clearButton = new OO.ui.ButtonWidget( {
12570 classes: [ 'oo-ui-selectFileWidget-clearButton' ],
12571 framed: false,
12572 icon: 'remove',
12573 disabled: this.disabled
12574 } );
12575
12576 // Events
12577 this.$handle.on( {
12578 keypress: this.onKeyPress.bind( this )
12579 } );
12580 this.clearButton.connect( this, {
12581 click: 'onClearClick'
12582 } );
12583 if ( config.droppable ) {
12584 dragHandler = this.onDragEnterOrOver.bind( this );
12585 this.$handle.on( {
12586 dragenter: dragHandler,
12587 dragover: dragHandler,
12588 dragleave: this.onDragLeave.bind( this ),
12589 drop: this.onDrop.bind( this )
12590 } );
12591 }
12592
12593 // Initialization
12594 this.addInput();
12595 this.updateUI();
12596 this.$label.addClass( 'oo-ui-selectFileWidget-label' );
12597 this.$handle
12598 .addClass( 'oo-ui-selectFileWidget-handle' )
12599 .append( this.$icon, this.$label, this.clearButton.$element, this.$indicator );
12600 this.$element
12601 .addClass( 'oo-ui-selectFileWidget' )
12602 .append( this.$handle );
12603 if ( config.droppable ) {
12604 this.$element.addClass( 'oo-ui-selectFileWidget-droppable' );
12605 }
12606 };
12607
12608 /* Setup */
12609
12610 OO.inheritClass( OO.ui.SelectFileWidget, OO.ui.Widget );
12611 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.IconElement );
12612 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.IndicatorElement );
12613 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.PendingElement );
12614 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.LabelElement );
12615 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.TabIndexedElement );
12616
12617 /* Static properties */
12618
12619 /**
12620 * Check if this widget is supported
12621 *
12622 * @static
12623 * @return {boolean}
12624 */
12625 OO.ui.SelectFileWidget.static.isSupported = function () {
12626 var $input;
12627 if ( OO.ui.SelectFileWidget.static.isSupportedCache === null ) {
12628 $input = $( '<input type="file">' );
12629 OO.ui.SelectFileWidget.static.isSupportedCache = $input[0].files !== undefined;
12630 }
12631 return OO.ui.SelectFileWidget.static.isSupportedCache;
12632 };
12633
12634 OO.ui.SelectFileWidget.static.isSupportedCache = null;
12635
12636 /* Events */
12637
12638 /**
12639 * @event change
12640 *
12641 * A change event is emitted when the on/off state of the toggle changes.
12642 *
12643 * @param {File|null} value New value
12644 */
12645
12646 /* Methods */
12647
12648 /**
12649 * Get the current value of the field
12650 *
12651 * @return {File|null}
12652 */
12653 OO.ui.SelectFileWidget.prototype.getValue = function () {
12654 return this.currentFile;
12655 };
12656
12657 /**
12658 * Set the current value of the field
12659 *
12660 * @param {File|null} file File to select
12661 */
12662 OO.ui.SelectFileWidget.prototype.setValue = function ( file ) {
12663 if ( this.currentFile !== file ) {
12664 this.currentFile = file;
12665 this.updateUI();
12666 this.emit( 'change', this.currentFile );
12667 }
12668 };
12669
12670 /**
12671 * Update the user interface when a file is selected or unselected
12672 *
12673 * @protected
12674 */
12675 OO.ui.SelectFileWidget.prototype.updateUI = function () {
12676 if ( !this.isSupported ) {
12677 this.$element.addClass( 'oo-ui-selectFileWidget-notsupported' );
12678 this.$element.removeClass( 'oo-ui-selectFileWidget-empty' );
12679 this.setLabel( this.notsupported );
12680 } else if ( this.currentFile ) {
12681 this.$element.removeClass( 'oo-ui-selectFileWidget-empty' );
12682 this.setLabel( this.currentFile.name +
12683 ( this.currentFile.type !== '' ? OO.ui.msg( 'ooui-semicolon-separator' ) + this.currentFile.type : '' )
12684 );
12685 } else {
12686 this.$element.addClass( 'oo-ui-selectFileWidget-empty' );
12687 this.setLabel( this.placeholder );
12688 }
12689
12690 if ( this.$input ) {
12691 this.$input.attr( 'title', this.getLabel() );
12692 }
12693 };
12694
12695 /**
12696 * Add the input to the handle
12697 *
12698 * @private
12699 */
12700 OO.ui.SelectFileWidget.prototype.addInput = function () {
12701 if ( this.$input ) {
12702 this.$input.remove();
12703 }
12704
12705 if ( !this.isSupported ) {
12706 this.$input = null;
12707 return;
12708 }
12709
12710 this.$input = $( '<input type="file">' );
12711 this.$input.on( 'change', this.onFileSelectedHandler );
12712 this.$input.attr( {
12713 tabindex: -1,
12714 title: this.getLabel()
12715 } );
12716 if ( this.accept ) {
12717 this.$input.attr( 'accept', this.accept.join( ', ' ) );
12718 }
12719 this.$handle.append( this.$input );
12720 };
12721
12722 /**
12723 * Determine if we should accept this file
12724 *
12725 * @private
12726 * @param {File} file
12727 * @return {boolean}
12728 */
12729 OO.ui.SelectFileWidget.prototype.isFileAcceptable = function ( file ) {
12730 var i, mime, mimeTest;
12731
12732 if ( !this.accept || file.type === '' ) {
12733 return true;
12734 }
12735
12736 mime = file.type;
12737 for ( i = 0; i < this.accept.length; i++ ) {
12738 mimeTest = this.accept[i];
12739 if ( mimeTest === mime ) {
12740 return true;
12741 } else if ( mimeTest.substr( -2 ) === '/*' ) {
12742 mimeTest = mimeTest.substr( 0, mimeTest.length - 1 );
12743 if ( mime.substr( 0, mimeTest.length ) === mimeTest ) {
12744 return true;
12745 }
12746 }
12747 }
12748
12749 return false;
12750 };
12751
12752 /**
12753 * Handle file selection from the input
12754 *
12755 * @private
12756 * @param {jQuery.Event} e
12757 */
12758 OO.ui.SelectFileWidget.prototype.onFileSelected = function ( e ) {
12759 var file = null;
12760
12761 if ( e.target.files && e.target.files[0] ) {
12762 file = e.target.files[0];
12763 if ( !this.isFileAcceptable( file ) ) {
12764 file = null;
12765 }
12766 }
12767
12768 this.setValue( file );
12769 this.addInput();
12770 };
12771
12772 /**
12773 * Handle clear button click events.
12774 *
12775 * @private
12776 */
12777 OO.ui.SelectFileWidget.prototype.onClearClick = function () {
12778 this.setValue( null );
12779 return false;
12780 };
12781
12782 /**
12783 * Handle key press events.
12784 *
12785 * @private
12786 * @param {jQuery.Event} e Key press event
12787 */
12788 OO.ui.SelectFileWidget.prototype.onKeyPress = function ( e ) {
12789 if ( this.isSupported && !this.isDisabled() && this.$input &&
12790 ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
12791 ) {
12792 this.$input.click();
12793 return false;
12794 }
12795 };
12796
12797 /**
12798 * Handle drag enter and over events
12799 *
12800 * @private
12801 * @param {jQuery.Event} e Drag event
12802 */
12803 OO.ui.SelectFileWidget.prototype.onDragEnterOrOver = function ( e ) {
12804 var file = null,
12805 dt = e.originalEvent.dataTransfer;
12806
12807 e.preventDefault();
12808 e.stopPropagation();
12809
12810 if ( this.isDisabled() || !this.isSupported ) {
12811 this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
12812 dt.dropEffect = 'none';
12813 return false;
12814 }
12815
12816 if ( dt && dt.files && dt.files[0] ) {
12817 file = dt.files[0];
12818 if ( !this.isFileAcceptable( file ) ) {
12819 file = null;
12820 }
12821 } else if ( dt && dt.types && $.inArray( 'Files', dt.types ) ) {
12822 // We know we have files so set 'file' to something truthy, we just
12823 // can't know any details about them.
12824 // * https://bugzilla.mozilla.org/show_bug.cgi?id=640534
12825 file = 'Files exist, but details are unknown';
12826 }
12827 if ( file ) {
12828 this.$element.addClass( 'oo-ui-selectFileWidget-canDrop' );
12829 } else {
12830 this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
12831 dt.dropEffect = 'none';
12832 }
12833
12834 return false;
12835 };
12836
12837 /**
12838 * Handle drag leave events
12839 *
12840 * @private
12841 * @param {jQuery.Event} e Drag event
12842 */
12843 OO.ui.SelectFileWidget.prototype.onDragLeave = function () {
12844 this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
12845 };
12846
12847 /**
12848 * Handle drop events
12849 *
12850 * @private
12851 * @param {jQuery.Event} e Drop event
12852 */
12853 OO.ui.SelectFileWidget.prototype.onDrop = function ( e ) {
12854 var file = null,
12855 dt = e.originalEvent.dataTransfer;
12856
12857 e.preventDefault();
12858 e.stopPropagation();
12859 this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
12860
12861 if ( this.isDisabled() || !this.isSupported ) {
12862 return false;
12863 }
12864
12865 if ( dt && dt.files && dt.files[0] ) {
12866 file = dt.files[0];
12867 if ( !this.isFileAcceptable( file ) ) {
12868 file = null;
12869 }
12870 }
12871 if ( file ) {
12872 this.setValue( file );
12873 }
12874
12875 return false;
12876 };
12877
12878 /**
12879 * @inheritdoc
12880 */
12881 OO.ui.SelectFileWidget.prototype.setDisabled = function ( state ) {
12882 OO.ui.SelectFileWidget.super.prototype.setDisabled.call( this, state );
12883 if ( this.clearButton ) {
12884 this.clearButton.setDisabled( state );
12885 }
12886 return this;
12887 };
12888
12889 /**
12890 * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
12891 * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
12892 * for a list of icons included in the library.
12893 *
12894 * @example
12895 * // An icon widget with a label
12896 * var myIcon = new OO.ui.IconWidget( {
12897 * icon: 'help',
12898 * iconTitle: 'Help'
12899 * } );
12900 * // Create a label.
12901 * var iconLabel = new OO.ui.LabelWidget( {
12902 * label: 'Help'
12903 * } );
12904 * $( 'body' ).append( myIcon.$element, iconLabel.$element );
12905 *
12906 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
12907 *
12908 * @class
12909 * @extends OO.ui.Widget
12910 * @mixins OO.ui.mixin.IconElement
12911 * @mixins OO.ui.mixin.TitledElement
12912 * @mixins OO.ui.mixin.FlaggedElement
12913 *
12914 * @constructor
12915 * @param {Object} [config] Configuration options
12916 */
12917 OO.ui.IconWidget = function OoUiIconWidget( config ) {
12918 // Configuration initialization
12919 config = config || {};
12920
12921 // Parent constructor
12922 OO.ui.IconWidget.super.call( this, config );
12923
12924 // Mixin constructors
12925 OO.ui.mixin.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) );
12926 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
12927 OO.ui.mixin.FlaggedElement.call( this, $.extend( {}, config, { $flagged: this.$element } ) );
12928
12929 // Initialization
12930 this.$element.addClass( 'oo-ui-iconWidget' );
12931 };
12932
12933 /* Setup */
12934
12935 OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
12936 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.IconElement );
12937 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.TitledElement );
12938 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.FlaggedElement );
12939
12940 /* Static Properties */
12941
12942 OO.ui.IconWidget.static.tagName = 'span';
12943
12944 /**
12945 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
12946 * attention to the status of an item or to clarify the function of a control. For a list of
12947 * indicators included in the library, please see the [OOjs UI documentation on MediaWiki][1].
12948 *
12949 * @example
12950 * // Example of an indicator widget
12951 * var indicator1 = new OO.ui.IndicatorWidget( {
12952 * indicator: 'alert'
12953 * } );
12954 *
12955 * // Create a fieldset layout to add a label
12956 * var fieldset = new OO.ui.FieldsetLayout();
12957 * fieldset.addItems( [
12958 * new OO.ui.FieldLayout( indicator1, { label: 'An alert indicator:' } )
12959 * ] );
12960 * $( 'body' ).append( fieldset.$element );
12961 *
12962 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
12963 *
12964 * @class
12965 * @extends OO.ui.Widget
12966 * @mixins OO.ui.mixin.IndicatorElement
12967 * @mixins OO.ui.mixin.TitledElement
12968 *
12969 * @constructor
12970 * @param {Object} [config] Configuration options
12971 */
12972 OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
12973 // Configuration initialization
12974 config = config || {};
12975
12976 // Parent constructor
12977 OO.ui.IndicatorWidget.super.call( this, config );
12978
12979 // Mixin constructors
12980 OO.ui.mixin.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) );
12981 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
12982
12983 // Initialization
12984 this.$element.addClass( 'oo-ui-indicatorWidget' );
12985 };
12986
12987 /* Setup */
12988
12989 OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
12990 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.mixin.IndicatorElement );
12991 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.mixin.TitledElement );
12992
12993 /* Static Properties */
12994
12995 OO.ui.IndicatorWidget.static.tagName = 'span';
12996
12997 /**
12998 * InputWidget is the base class for all input widgets, which
12999 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
13000 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
13001 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
13002 *
13003 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
13004 *
13005 * @abstract
13006 * @class
13007 * @extends OO.ui.Widget
13008 * @mixins OO.ui.mixin.FlaggedElement
13009 * @mixins OO.ui.mixin.TabIndexedElement
13010 *
13011 * @constructor
13012 * @param {Object} [config] Configuration options
13013 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
13014 * @cfg {string} [value=''] The value of the input.
13015 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
13016 * before it is accepted.
13017 */
13018 OO.ui.InputWidget = function OoUiInputWidget( config ) {
13019 // Configuration initialization
13020 config = config || {};
13021
13022 // Parent constructor
13023 OO.ui.InputWidget.super.call( this, config );
13024
13025 // Properties
13026 this.$input = this.getInputElement( config );
13027 this.value = '';
13028 this.inputFilter = config.inputFilter;
13029
13030 // Mixin constructors
13031 OO.ui.mixin.FlaggedElement.call( this, config );
13032 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$input } ) );
13033
13034 // Events
13035 this.$input.on( 'keydown mouseup cut paste change input select', this.onEdit.bind( this ) );
13036
13037 // Initialization
13038 this.$input
13039 .attr( 'name', config.name )
13040 .prop( 'disabled', this.isDisabled() );
13041 this.$element.addClass( 'oo-ui-inputWidget' ).append( this.$input, $( '<span>' ) );
13042 this.setValue( config.value );
13043 };
13044
13045 /* Setup */
13046
13047 OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
13048 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.FlaggedElement );
13049 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.TabIndexedElement );
13050
13051 /* Static Properties */
13052
13053 OO.ui.InputWidget.static.supportsSimpleLabel = true;
13054
13055 /* Events */
13056
13057 /**
13058 * @event change
13059 *
13060 * A change event is emitted when the value of the input changes.
13061 *
13062 * @param {string} value
13063 */
13064
13065 /* Methods */
13066
13067 /**
13068 * Get input element.
13069 *
13070 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
13071 * different circumstances. The element must have a `value` property (like form elements).
13072 *
13073 * @protected
13074 * @param {Object} config Configuration options
13075 * @return {jQuery} Input element
13076 */
13077 OO.ui.InputWidget.prototype.getInputElement = function () {
13078 return $( '<input>' );
13079 };
13080
13081 /**
13082 * Handle potentially value-changing events.
13083 *
13084 * @private
13085 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
13086 */
13087 OO.ui.InputWidget.prototype.onEdit = function () {
13088 var widget = this;
13089 if ( !this.isDisabled() ) {
13090 // Allow the stack to clear so the value will be updated
13091 setTimeout( function () {
13092 widget.setValue( widget.$input.val() );
13093 } );
13094 }
13095 };
13096
13097 /**
13098 * Get the value of the input.
13099 *
13100 * @return {string} Input value
13101 */
13102 OO.ui.InputWidget.prototype.getValue = function () {
13103 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
13104 // it, and we won't know unless they're kind enough to trigger a 'change' event.
13105 var value = this.$input.val();
13106 if ( this.value !== value ) {
13107 this.setValue( value );
13108 }
13109 return this.value;
13110 };
13111
13112 /**
13113 * Set the direction of the input, either RTL (right-to-left) or LTR (left-to-right).
13114 *
13115 * @param {boolean} isRTL
13116 * Direction is right-to-left
13117 */
13118 OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
13119 this.$input.prop( 'dir', isRTL ? 'rtl' : 'ltr' );
13120 };
13121
13122 /**
13123 * Set the value of the input.
13124 *
13125 * @param {string} value New value
13126 * @fires change
13127 * @chainable
13128 */
13129 OO.ui.InputWidget.prototype.setValue = function ( value ) {
13130 value = this.cleanUpValue( value );
13131 // Update the DOM if it has changed. Note that with cleanUpValue, it
13132 // is possible for the DOM value to change without this.value changing.
13133 if ( this.$input.val() !== value ) {
13134 this.$input.val( value );
13135 }
13136 if ( this.value !== value ) {
13137 this.value = value;
13138 this.emit( 'change', this.value );
13139 }
13140 return this;
13141 };
13142
13143 /**
13144 * Clean up incoming value.
13145 *
13146 * Ensures value is a string, and converts undefined and null to empty string.
13147 *
13148 * @private
13149 * @param {string} value Original value
13150 * @return {string} Cleaned up value
13151 */
13152 OO.ui.InputWidget.prototype.cleanUpValue = function ( value ) {
13153 if ( value === undefined || value === null ) {
13154 return '';
13155 } else if ( this.inputFilter ) {
13156 return this.inputFilter( String( value ) );
13157 } else {
13158 return String( value );
13159 }
13160 };
13161
13162 /**
13163 * Simulate the behavior of clicking on a label bound to this input. This method is only called by
13164 * {@link OO.ui.LabelWidget LabelWidget} and {@link OO.ui.FieldLayout FieldLayout}. It should not be
13165 * called directly.
13166 */
13167 OO.ui.InputWidget.prototype.simulateLabelClick = function () {
13168 if ( !this.isDisabled() ) {
13169 if ( this.$input.is( ':checkbox, :radio' ) ) {
13170 this.$input.click();
13171 }
13172 if ( this.$input.is( ':input' ) ) {
13173 this.$input[ 0 ].focus();
13174 }
13175 }
13176 };
13177
13178 /**
13179 * @inheritdoc
13180 */
13181 OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
13182 OO.ui.InputWidget.super.prototype.setDisabled.call( this, state );
13183 if ( this.$input ) {
13184 this.$input.prop( 'disabled', this.isDisabled() );
13185 }
13186 return this;
13187 };
13188
13189 /**
13190 * Focus the input.
13191 *
13192 * @chainable
13193 */
13194 OO.ui.InputWidget.prototype.focus = function () {
13195 this.$input[ 0 ].focus();
13196 return this;
13197 };
13198
13199 /**
13200 * Blur the input.
13201 *
13202 * @chainable
13203 */
13204 OO.ui.InputWidget.prototype.blur = function () {
13205 this.$input[ 0 ].blur();
13206 return this;
13207 };
13208
13209 /**
13210 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
13211 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
13212 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
13213 * HTML `<button/>` (the default) or an HTML `<input/>` tags. See the
13214 * [OOjs UI documentation on MediaWiki] [1] for more information.
13215 *
13216 * @example
13217 * // A ButtonInputWidget rendered as an HTML button, the default.
13218 * var button = new OO.ui.ButtonInputWidget( {
13219 * label: 'Input button',
13220 * icon: 'check',
13221 * value: 'check'
13222 * } );
13223 * $( 'body' ).append( button.$element );
13224 *
13225 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
13226 *
13227 * @class
13228 * @extends OO.ui.InputWidget
13229 * @mixins OO.ui.mixin.ButtonElement
13230 * @mixins OO.ui.mixin.IconElement
13231 * @mixins OO.ui.mixin.IndicatorElement
13232 * @mixins OO.ui.mixin.LabelElement
13233 * @mixins OO.ui.mixin.TitledElement
13234 *
13235 * @constructor
13236 * @param {Object} [config] Configuration options
13237 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
13238 * @cfg {boolean} [useInputTag=false] Use an `<input/>` tag instead of a `<button/>` tag, the default.
13239 * Widgets configured to be an `<input/>` do not support {@link #icon icons} and {@link #indicator indicators},
13240 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
13241 * be set to `true` when there’s need to support IE6 in a form with multiple buttons.
13242 */
13243 OO.ui.ButtonInputWidget = function OoUiButtonInputWidget( config ) {
13244 // Configuration initialization
13245 config = $.extend( { type: 'button', useInputTag: false }, config );
13246
13247 // Properties (must be set before parent constructor, which calls #setValue)
13248 this.useInputTag = config.useInputTag;
13249
13250 // Parent constructor
13251 OO.ui.ButtonInputWidget.super.call( this, config );
13252
13253 // Mixin constructors
13254 OO.ui.mixin.ButtonElement.call( this, $.extend( {}, config, { $button: this.$input } ) );
13255 OO.ui.mixin.IconElement.call( this, config );
13256 OO.ui.mixin.IndicatorElement.call( this, config );
13257 OO.ui.mixin.LabelElement.call( this, config );
13258 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
13259
13260 // Initialization
13261 if ( !config.useInputTag ) {
13262 this.$input.append( this.$icon, this.$label, this.$indicator );
13263 }
13264 this.$element.addClass( 'oo-ui-buttonInputWidget' );
13265 };
13266
13267 /* Setup */
13268
13269 OO.inheritClass( OO.ui.ButtonInputWidget, OO.ui.InputWidget );
13270 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.ButtonElement );
13271 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IconElement );
13272 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IndicatorElement );
13273 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.LabelElement );
13274 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.TitledElement );
13275
13276 /* Methods */
13277
13278 /**
13279 * @inheritdoc
13280 * @protected
13281 */
13282 OO.ui.ButtonInputWidget.prototype.getInputElement = function ( config ) {
13283 var type = [ 'button', 'submit', 'reset' ].indexOf( config.type ) !== -1 ?
13284 config.type :
13285 'button';
13286 return $( '<' + ( config.useInputTag ? 'input' : 'button' ) + ' type="' + type + '">' );
13287 };
13288
13289 /**
13290 * Set label value.
13291 *
13292 * If #useInputTag is `true`, the label is set as the `value` of the `<input/>` tag.
13293 *
13294 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
13295 * text, or `null` for no label
13296 * @chainable
13297 */
13298 OO.ui.ButtonInputWidget.prototype.setLabel = function ( label ) {
13299 OO.ui.mixin.LabelElement.prototype.setLabel.call( this, label );
13300
13301 if ( this.useInputTag ) {
13302 if ( typeof label === 'function' ) {
13303 label = OO.ui.resolveMsg( label );
13304 }
13305 if ( label instanceof jQuery ) {
13306 label = label.text();
13307 }
13308 if ( !label ) {
13309 label = '';
13310 }
13311 this.$input.val( label );
13312 }
13313
13314 return this;
13315 };
13316
13317 /**
13318 * Set the value of the input.
13319 *
13320 * This method is disabled for button inputs configured as {@link #useInputTag <input/> tags}, as
13321 * they do not support {@link #value values}.
13322 *
13323 * @param {string} value New value
13324 * @chainable
13325 */
13326 OO.ui.ButtonInputWidget.prototype.setValue = function ( value ) {
13327 if ( !this.useInputTag ) {
13328 OO.ui.ButtonInputWidget.super.prototype.setValue.call( this, value );
13329 }
13330 return this;
13331 };
13332
13333 /**
13334 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
13335 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
13336 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
13337 * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
13338 *
13339 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
13340 *
13341 * @example
13342 * // An example of selected, unselected, and disabled checkbox inputs
13343 * var checkbox1=new OO.ui.CheckboxInputWidget( {
13344 * value: 'a',
13345 * selected: true
13346 * } );
13347 * var checkbox2=new OO.ui.CheckboxInputWidget( {
13348 * value: 'b'
13349 * } );
13350 * var checkbox3=new OO.ui.CheckboxInputWidget( {
13351 * value:'c',
13352 * disabled: true
13353 * } );
13354 * // Create a fieldset layout with fields for each checkbox.
13355 * var fieldset = new OO.ui.FieldsetLayout( {
13356 * label: 'Checkboxes'
13357 * } );
13358 * fieldset.addItems( [
13359 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
13360 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
13361 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
13362 * ] );
13363 * $( 'body' ).append( fieldset.$element );
13364 *
13365 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
13366 *
13367 * @class
13368 * @extends OO.ui.InputWidget
13369 *
13370 * @constructor
13371 * @param {Object} [config] Configuration options
13372 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
13373 */
13374 OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
13375 // Configuration initialization
13376 config = config || {};
13377
13378 // Parent constructor
13379 OO.ui.CheckboxInputWidget.super.call( this, config );
13380
13381 // Initialization
13382 this.$element.addClass( 'oo-ui-checkboxInputWidget' );
13383 this.setSelected( config.selected !== undefined ? config.selected : false );
13384 };
13385
13386 /* Setup */
13387
13388 OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
13389
13390 /* Methods */
13391
13392 /**
13393 * @inheritdoc
13394 * @protected
13395 */
13396 OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
13397 return $( '<input type="checkbox" />' );
13398 };
13399
13400 /**
13401 * @inheritdoc
13402 */
13403 OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
13404 var widget = this;
13405 if ( !this.isDisabled() ) {
13406 // Allow the stack to clear so the value will be updated
13407 setTimeout( function () {
13408 widget.setSelected( widget.$input.prop( 'checked' ) );
13409 } );
13410 }
13411 };
13412
13413 /**
13414 * Set selection state of this checkbox.
13415 *
13416 * @param {boolean} state `true` for selected
13417 * @chainable
13418 */
13419 OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state ) {
13420 state = !!state;
13421 if ( this.selected !== state ) {
13422 this.selected = state;
13423 this.$input.prop( 'checked', this.selected );
13424 this.emit( 'change', this.selected );
13425 }
13426 return this;
13427 };
13428
13429 /**
13430 * Check if this checkbox is selected.
13431 *
13432 * @return {boolean} Checkbox is selected
13433 */
13434 OO.ui.CheckboxInputWidget.prototype.isSelected = function () {
13435 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
13436 // it, and we won't know unless they're kind enough to trigger a 'change' event.
13437 var selected = this.$input.prop( 'checked' );
13438 if ( this.selected !== selected ) {
13439 this.setSelected( selected );
13440 }
13441 return this.selected;
13442 };
13443
13444 /**
13445 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
13446 * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
13447 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
13448 * more information about input widgets.
13449 *
13450 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
13451 * are no options. If no `value` configuration option is provided, the first option is selected.
13452 * If you need a state representing no value (no option being selected), use a DropdownWidget.
13453 *
13454 * This and OO.ui.RadioSelectInputWidget support the same configuration options.
13455 *
13456 * @example
13457 * // Example: A DropdownInputWidget with three options
13458 * var dropdownInput = new OO.ui.DropdownInputWidget( {
13459 * options: [
13460 * { data: 'a', label: 'First' },
13461 * { data: 'b', label: 'Second'},
13462 * { data: 'c', label: 'Third' }
13463 * ]
13464 * } );
13465 * $( 'body' ).append( dropdownInput.$element );
13466 *
13467 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
13468 *
13469 * @class
13470 * @extends OO.ui.InputWidget
13471 *
13472 * @constructor
13473 * @param {Object} [config] Configuration options
13474 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
13475 */
13476 OO.ui.DropdownInputWidget = function OoUiDropdownInputWidget( config ) {
13477 // Configuration initialization
13478 config = config || {};
13479
13480 // Properties (must be done before parent constructor which calls #setDisabled)
13481 this.dropdownWidget = new OO.ui.DropdownWidget();
13482
13483 // Parent constructor
13484 OO.ui.DropdownInputWidget.super.call( this, config );
13485
13486 // Events
13487 this.dropdownWidget.getMenu().connect( this, { select: 'onMenuSelect' } );
13488
13489 // Initialization
13490 this.setOptions( config.options || [] );
13491 this.$element
13492 .addClass( 'oo-ui-dropdownInputWidget' )
13493 .append( this.dropdownWidget.$element );
13494 };
13495
13496 /* Setup */
13497
13498 OO.inheritClass( OO.ui.DropdownInputWidget, OO.ui.InputWidget );
13499
13500 /* Methods */
13501
13502 /**
13503 * @inheritdoc
13504 * @protected
13505 */
13506 OO.ui.DropdownInputWidget.prototype.getInputElement = function () {
13507 return $( '<input type="hidden">' );
13508 };
13509
13510 /**
13511 * Handles menu select events.
13512 *
13513 * @private
13514 * @param {OO.ui.MenuOptionWidget} item Selected menu item
13515 */
13516 OO.ui.DropdownInputWidget.prototype.onMenuSelect = function ( item ) {
13517 this.setValue( item.getData() );
13518 };
13519
13520 /**
13521 * @inheritdoc
13522 */
13523 OO.ui.DropdownInputWidget.prototype.setValue = function ( value ) {
13524 this.dropdownWidget.getMenu().selectItemByData( value );
13525 OO.ui.DropdownInputWidget.super.prototype.setValue.call( this, value );
13526 return this;
13527 };
13528
13529 /**
13530 * @inheritdoc
13531 */
13532 OO.ui.DropdownInputWidget.prototype.setDisabled = function ( state ) {
13533 this.dropdownWidget.setDisabled( state );
13534 OO.ui.DropdownInputWidget.super.prototype.setDisabled.call( this, state );
13535 return this;
13536 };
13537
13538 /**
13539 * Set the options available for this input.
13540 *
13541 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
13542 * @chainable
13543 */
13544 OO.ui.DropdownInputWidget.prototype.setOptions = function ( options ) {
13545 var value = this.getValue();
13546
13547 // Rebuild the dropdown menu
13548 this.dropdownWidget.getMenu()
13549 .clearItems()
13550 .addItems( options.map( function ( opt ) {
13551 return new OO.ui.MenuOptionWidget( {
13552 data: opt.data,
13553 label: opt.label !== undefined ? opt.label : opt.data
13554 } );
13555 } ) );
13556
13557 // Restore the previous value, or reset to something sensible
13558 if ( this.dropdownWidget.getMenu().getItemFromData( value ) ) {
13559 // Previous value is still available, ensure consistency with the dropdown
13560 this.setValue( value );
13561 } else {
13562 // No longer valid, reset
13563 if ( options.length ) {
13564 this.setValue( options[ 0 ].data );
13565 }
13566 }
13567
13568 return this;
13569 };
13570
13571 /**
13572 * @inheritdoc
13573 */
13574 OO.ui.DropdownInputWidget.prototype.focus = function () {
13575 this.dropdownWidget.getMenu().toggle( true );
13576 return this;
13577 };
13578
13579 /**
13580 * @inheritdoc
13581 */
13582 OO.ui.DropdownInputWidget.prototype.blur = function () {
13583 this.dropdownWidget.getMenu().toggle( false );
13584 return this;
13585 };
13586
13587 /**
13588 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
13589 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
13590 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
13591 * please see the [OOjs UI documentation on MediaWiki][1].
13592 *
13593 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
13594 *
13595 * @example
13596 * // An example of selected, unselected, and disabled radio inputs
13597 * var radio1 = new OO.ui.RadioInputWidget( {
13598 * value: 'a',
13599 * selected: true
13600 * } );
13601 * var radio2 = new OO.ui.RadioInputWidget( {
13602 * value: 'b'
13603 * } );
13604 * var radio3 = new OO.ui.RadioInputWidget( {
13605 * value: 'c',
13606 * disabled: true
13607 * } );
13608 * // Create a fieldset layout with fields for each radio button.
13609 * var fieldset = new OO.ui.FieldsetLayout( {
13610 * label: 'Radio inputs'
13611 * } );
13612 * fieldset.addItems( [
13613 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
13614 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
13615 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
13616 * ] );
13617 * $( 'body' ).append( fieldset.$element );
13618 *
13619 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
13620 *
13621 * @class
13622 * @extends OO.ui.InputWidget
13623 *
13624 * @constructor
13625 * @param {Object} [config] Configuration options
13626 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
13627 */
13628 OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) {
13629 // Configuration initialization
13630 config = config || {};
13631
13632 // Parent constructor
13633 OO.ui.RadioInputWidget.super.call( this, config );
13634
13635 // Initialization
13636 this.$element.addClass( 'oo-ui-radioInputWidget' );
13637 this.setSelected( config.selected !== undefined ? config.selected : false );
13638 };
13639
13640 /* Setup */
13641
13642 OO.inheritClass( OO.ui.RadioInputWidget, OO.ui.InputWidget );
13643
13644 /* Methods */
13645
13646 /**
13647 * @inheritdoc
13648 * @protected
13649 */
13650 OO.ui.RadioInputWidget.prototype.getInputElement = function () {
13651 return $( '<input type="radio" />' );
13652 };
13653
13654 /**
13655 * @inheritdoc
13656 */
13657 OO.ui.RadioInputWidget.prototype.onEdit = function () {
13658 // RadioInputWidget doesn't track its state.
13659 };
13660
13661 /**
13662 * Set selection state of this radio button.
13663 *
13664 * @param {boolean} state `true` for selected
13665 * @chainable
13666 */
13667 OO.ui.RadioInputWidget.prototype.setSelected = function ( state ) {
13668 // RadioInputWidget doesn't track its state.
13669 this.$input.prop( 'checked', state );
13670 return this;
13671 };
13672
13673 /**
13674 * Check if this radio button is selected.
13675 *
13676 * @return {boolean} Radio is selected
13677 */
13678 OO.ui.RadioInputWidget.prototype.isSelected = function () {
13679 return this.$input.prop( 'checked' );
13680 };
13681
13682 /**
13683 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
13684 * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
13685 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
13686 * more information about input widgets.
13687 *
13688 * This and OO.ui.DropdownInputWidget support the same configuration options.
13689 *
13690 * @example
13691 * // Example: A RadioSelectInputWidget with three options
13692 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
13693 * options: [
13694 * { data: 'a', label: 'First' },
13695 * { data: 'b', label: 'Second'},
13696 * { data: 'c', label: 'Third' }
13697 * ]
13698 * } );
13699 * $( 'body' ).append( radioSelectInput.$element );
13700 *
13701 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
13702 *
13703 * @class
13704 * @extends OO.ui.InputWidget
13705 *
13706 * @constructor
13707 * @param {Object} [config] Configuration options
13708 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
13709 */
13710 OO.ui.RadioSelectInputWidget = function OoUiRadioSelectInputWidget( config ) {
13711 // Configuration initialization
13712 config = config || {};
13713
13714 // Properties (must be done before parent constructor which calls #setDisabled)
13715 this.radioSelectWidget = new OO.ui.RadioSelectWidget();
13716
13717 // Parent constructor
13718 OO.ui.RadioSelectInputWidget.super.call( this, config );
13719
13720 // Events
13721 this.radioSelectWidget.connect( this, { select: 'onMenuSelect' } );
13722
13723 // Initialization
13724 this.setOptions( config.options || [] );
13725 this.$element
13726 .addClass( 'oo-ui-radioSelectInputWidget' )
13727 .empty()
13728 .append( this.radioSelectWidget.$element );
13729 };
13730
13731 /* Setup */
13732
13733 OO.inheritClass( OO.ui.RadioSelectInputWidget, OO.ui.InputWidget );
13734
13735 /* Static Properties */
13736
13737 OO.ui.RadioSelectInputWidget.static.supportsSimpleLabel = false;
13738
13739 /* Methods */
13740
13741 /**
13742 * @inheritdoc
13743 * @protected
13744 */
13745 OO.ui.RadioSelectInputWidget.prototype.getInputElement = function () {
13746 return $( '<input type="hidden">' );
13747 };
13748
13749 /**
13750 * Handles menu select events.
13751 *
13752 * @private
13753 * @param {OO.ui.RadioOptionWidget} item Selected menu item
13754 */
13755 OO.ui.RadioSelectInputWidget.prototype.onMenuSelect = function ( item ) {
13756 this.setValue( item.getData() );
13757 };
13758
13759 /**
13760 * @inheritdoc
13761 */
13762 OO.ui.RadioSelectInputWidget.prototype.setValue = function ( value ) {
13763 this.radioSelectWidget.selectItemByData( value );
13764 OO.ui.RadioSelectInputWidget.super.prototype.setValue.call( this, value );
13765 return this;
13766 };
13767
13768 /**
13769 * @inheritdoc
13770 */
13771 OO.ui.RadioSelectInputWidget.prototype.setDisabled = function ( state ) {
13772 this.radioSelectWidget.setDisabled( state );
13773 OO.ui.RadioSelectInputWidget.super.prototype.setDisabled.call( this, state );
13774 return this;
13775 };
13776
13777 /**
13778 * Set the options available for this input.
13779 *
13780 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
13781 * @chainable
13782 */
13783 OO.ui.RadioSelectInputWidget.prototype.setOptions = function ( options ) {
13784 var value = this.getValue();
13785
13786 // Rebuild the radioSelect menu
13787 this.radioSelectWidget
13788 .clearItems()
13789 .addItems( options.map( function ( opt ) {
13790 return new OO.ui.RadioOptionWidget( {
13791 data: opt.data,
13792 label: opt.label !== undefined ? opt.label : opt.data
13793 } );
13794 } ) );
13795
13796 // Restore the previous value, or reset to something sensible
13797 if ( this.radioSelectWidget.getItemFromData( value ) ) {
13798 // Previous value is still available, ensure consistency with the radioSelect
13799 this.setValue( value );
13800 } else {
13801 // No longer valid, reset
13802 if ( options.length ) {
13803 this.setValue( options[ 0 ].data );
13804 }
13805 }
13806
13807 return this;
13808 };
13809
13810 /**
13811 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
13812 * size of the field as well as its presentation. In addition, these widgets can be configured
13813 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
13814 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
13815 * which modifies incoming values rather than validating them.
13816 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
13817 *
13818 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
13819 *
13820 * @example
13821 * // Example of a text input widget
13822 * var textInput = new OO.ui.TextInputWidget( {
13823 * value: 'Text input'
13824 * } )
13825 * $( 'body' ).append( textInput.$element );
13826 *
13827 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
13828 *
13829 * @class
13830 * @extends OO.ui.InputWidget
13831 * @mixins OO.ui.mixin.IconElement
13832 * @mixins OO.ui.mixin.IndicatorElement
13833 * @mixins OO.ui.mixin.PendingElement
13834 * @mixins OO.ui.mixin.LabelElement
13835 *
13836 * @constructor
13837 * @param {Object} [config] Configuration options
13838 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password', 'search',
13839 * 'email' or 'url'. Ignored if `multiline` is true.
13840 * @cfg {string} [placeholder] Placeholder text
13841 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
13842 * instruct the browser to focus this widget.
13843 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
13844 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
13845 * @cfg {boolean} [multiline=false] Allow multiple lines of text
13846 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
13847 * Use the #maxRows config to specify a maximum number of displayed rows.
13848 * @cfg {boolean} [maxRows=10] Maximum number of rows to display when #autosize is set to true.
13849 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
13850 * the value or placeholder text: `'before'` or `'after'`
13851 * @cfg {boolean} [required=false] Mark the field as required
13852 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
13853 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
13854 * (the value must contain only numbers); when RegExp, a regular expression that must match the
13855 * value for it to be considered valid; when Function, a function receiving the value as parameter
13856 * that must return true, or promise resolving to true, for it to be considered valid.
13857 */
13858 OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
13859 // Configuration initialization
13860 config = $.extend( {
13861 type: 'text',
13862 labelPosition: 'after',
13863 maxRows: 10
13864 }, config );
13865
13866 // Parent constructor
13867 OO.ui.TextInputWidget.super.call( this, config );
13868
13869 // Mixin constructors
13870 OO.ui.mixin.IconElement.call( this, config );
13871 OO.ui.mixin.IndicatorElement.call( this, config );
13872 OO.ui.mixin.PendingElement.call( this, config );
13873 OO.ui.mixin.LabelElement.call( this, config );
13874
13875 // Properties
13876 this.readOnly = false;
13877 this.multiline = !!config.multiline;
13878 this.autosize = !!config.autosize;
13879 this.maxRows = config.maxRows;
13880 this.validate = null;
13881
13882 // Clone for resizing
13883 if ( this.autosize ) {
13884 this.$clone = this.$input
13885 .clone()
13886 .insertAfter( this.$input )
13887 .attr( 'aria-hidden', 'true' )
13888 .addClass( 'oo-ui-element-hidden' );
13889 }
13890
13891 this.setValidation( config.validate );
13892 this.setLabelPosition( config.labelPosition );
13893
13894 // Events
13895 this.$input.on( {
13896 keypress: this.onKeyPress.bind( this ),
13897 blur: this.onBlur.bind( this )
13898 } );
13899 this.$input.one( {
13900 focus: this.onElementAttach.bind( this )
13901 } );
13902 this.$icon.on( 'mousedown', this.onIconMouseDown.bind( this ) );
13903 this.$indicator.on( 'mousedown', this.onIndicatorMouseDown.bind( this ) );
13904 this.on( 'labelChange', this.updatePosition.bind( this ) );
13905 this.connect( this, { change: 'onChange' } );
13906
13907 // Initialization
13908 this.$element
13909 .addClass( 'oo-ui-textInputWidget' )
13910 .append( this.$icon, this.$indicator );
13911 this.setReadOnly( !!config.readOnly );
13912 if ( config.placeholder ) {
13913 this.$input.attr( 'placeholder', config.placeholder );
13914 }
13915 if ( config.maxLength !== undefined ) {
13916 this.$input.attr( 'maxlength', config.maxLength );
13917 }
13918 if ( config.autofocus ) {
13919 this.$input.attr( 'autofocus', 'autofocus' );
13920 }
13921 if ( config.required ) {
13922 this.$input.attr( 'required', 'required' );
13923 this.$input.attr( 'aria-required', 'true' );
13924 }
13925 if ( this.label || config.autosize ) {
13926 this.installParentChangeDetector();
13927 }
13928 };
13929
13930 /* Setup */
13931
13932 OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
13933 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IconElement );
13934 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IndicatorElement );
13935 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.PendingElement );
13936 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.LabelElement );
13937
13938 /* Static properties */
13939
13940 OO.ui.TextInputWidget.static.validationPatterns = {
13941 'non-empty': /.+/,
13942 integer: /^\d+$/
13943 };
13944
13945 /* Events */
13946
13947 /**
13948 * An `enter` event is emitted when the user presses 'enter' inside the text box.
13949 *
13950 * Not emitted if the input is multiline.
13951 *
13952 * @event enter
13953 */
13954
13955 /* Methods */
13956
13957 /**
13958 * Handle icon mouse down events.
13959 *
13960 * @private
13961 * @param {jQuery.Event} e Mouse down event
13962 * @fires icon
13963 */
13964 OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
13965 if ( e.which === 1 ) {
13966 this.$input[ 0 ].focus();
13967 return false;
13968 }
13969 };
13970
13971 /**
13972 * Handle indicator mouse down events.
13973 *
13974 * @private
13975 * @param {jQuery.Event} e Mouse down event
13976 * @fires indicator
13977 */
13978 OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
13979 if ( e.which === 1 ) {
13980 this.$input[ 0 ].focus();
13981 return false;
13982 }
13983 };
13984
13985 /**
13986 * Handle key press events.
13987 *
13988 * @private
13989 * @param {jQuery.Event} e Key press event
13990 * @fires enter If enter key is pressed and input is not multiline
13991 */
13992 OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
13993 if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) {
13994 this.emit( 'enter', e );
13995 }
13996 };
13997
13998 /**
13999 * Handle blur events.
14000 *
14001 * @private
14002 * @param {jQuery.Event} e Blur event
14003 */
14004 OO.ui.TextInputWidget.prototype.onBlur = function () {
14005 this.setValidityFlag();
14006 };
14007
14008 /**
14009 * Handle element attach events.
14010 *
14011 * @private
14012 * @param {jQuery.Event} e Element attach event
14013 */
14014 OO.ui.TextInputWidget.prototype.onElementAttach = function () {
14015 // Any previously calculated size is now probably invalid if we reattached elsewhere
14016 this.valCache = null;
14017 this.adjustSize();
14018 this.positionLabel();
14019 };
14020
14021 /**
14022 * Handle change events.
14023 *
14024 * @param {string} value
14025 * @private
14026 */
14027 OO.ui.TextInputWidget.prototype.onChange = function () {
14028 this.setValidityFlag();
14029 this.adjustSize();
14030 };
14031
14032 /**
14033 * Check if the input is {@link #readOnly read-only}.
14034 *
14035 * @return {boolean}
14036 */
14037 OO.ui.TextInputWidget.prototype.isReadOnly = function () {
14038 return this.readOnly;
14039 };
14040
14041 /**
14042 * Set the {@link #readOnly read-only} state of the input.
14043 *
14044 * @param {boolean} state Make input read-only
14045 * @chainable
14046 */
14047 OO.ui.TextInputWidget.prototype.setReadOnly = function ( state ) {
14048 this.readOnly = !!state;
14049 this.$input.prop( 'readOnly', this.readOnly );
14050 return this;
14051 };
14052
14053 /**
14054 * Support function for making #onElementAttach work across browsers.
14055 *
14056 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
14057 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
14058 *
14059 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
14060 * first time that the element gets attached to the documented.
14061 */
14062 OO.ui.TextInputWidget.prototype.installParentChangeDetector = function () {
14063 var mutationObserver, onRemove, topmostNode, fakeParentNode,
14064 MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
14065 widget = this;
14066
14067 if ( MutationObserver ) {
14068 // The new way. If only it wasn't so ugly.
14069
14070 if ( this.$element.closest( 'html' ).length ) {
14071 // Widget is attached already, do nothing. This breaks the functionality of this function when
14072 // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
14073 // would require observation of the whole document, which would hurt performance of other,
14074 // more important code.
14075 return;
14076 }
14077
14078 // Find topmost node in the tree
14079 topmostNode = this.$element[0];
14080 while ( topmostNode.parentNode ) {
14081 topmostNode = topmostNode.parentNode;
14082 }
14083
14084 // We have no way to detect the $element being attached somewhere without observing the entire
14085 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
14086 // parent node of $element, and instead detect when $element is removed from it (and thus
14087 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
14088 // doesn't get attached, we end up back here and create the parent.
14089
14090 mutationObserver = new MutationObserver( function ( mutations ) {
14091 var i, j, removedNodes;
14092 for ( i = 0; i < mutations.length; i++ ) {
14093 removedNodes = mutations[ i ].removedNodes;
14094 for ( j = 0; j < removedNodes.length; j++ ) {
14095 if ( removedNodes[ j ] === topmostNode ) {
14096 setTimeout( onRemove, 0 );
14097 return;
14098 }
14099 }
14100 }
14101 } );
14102
14103 onRemove = function () {
14104 // If the node was attached somewhere else, report it
14105 if ( widget.$element.closest( 'html' ).length ) {
14106 widget.onElementAttach();
14107 }
14108 mutationObserver.disconnect();
14109 widget.installParentChangeDetector();
14110 };
14111
14112 // Create a fake parent and observe it
14113 fakeParentNode = $( '<div>' ).append( this.$element )[0];
14114 mutationObserver.observe( fakeParentNode, { childList: true } );
14115 } else {
14116 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
14117 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
14118 this.$element.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach.bind( this ) );
14119 }
14120 };
14121
14122 /**
14123 * Automatically adjust the size of the text input.
14124 *
14125 * This only affects #multiline inputs that are {@link #autosize autosized}.
14126 *
14127 * @chainable
14128 */
14129 OO.ui.TextInputWidget.prototype.adjustSize = function () {
14130 var scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError, idealHeight;
14131
14132 if ( this.multiline && this.autosize && this.$input.val() !== this.valCache ) {
14133 this.$clone
14134 .val( this.$input.val() )
14135 .attr( 'rows', '' )
14136 // Set inline height property to 0 to measure scroll height
14137 .css( 'height', 0 );
14138
14139 this.$clone.removeClass( 'oo-ui-element-hidden' );
14140
14141 this.valCache = this.$input.val();
14142
14143 scrollHeight = this.$clone[ 0 ].scrollHeight;
14144
14145 // Remove inline height property to measure natural heights
14146 this.$clone.css( 'height', '' );
14147 innerHeight = this.$clone.innerHeight();
14148 outerHeight = this.$clone.outerHeight();
14149
14150 // Measure max rows height
14151 this.$clone
14152 .attr( 'rows', this.maxRows )
14153 .css( 'height', 'auto' )
14154 .val( '' );
14155 maxInnerHeight = this.$clone.innerHeight();
14156
14157 // Difference between reported innerHeight and scrollHeight with no scrollbars present
14158 // Equals 1 on Blink-based browsers and 0 everywhere else
14159 measurementError = maxInnerHeight - this.$clone[ 0 ].scrollHeight;
14160 idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
14161
14162 this.$clone.addClass( 'oo-ui-element-hidden' );
14163
14164 // Only apply inline height when expansion beyond natural height is needed
14165 if ( idealHeight > innerHeight ) {
14166 // Use the difference between the inner and outer height as a buffer
14167 this.$input.css( 'height', idealHeight + ( outerHeight - innerHeight ) );
14168 } else {
14169 this.$input.css( 'height', '' );
14170 }
14171 }
14172 return this;
14173 };
14174
14175 /**
14176 * @inheritdoc
14177 * @protected
14178 */
14179 OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
14180 var type = [ 'text', 'password', 'search', 'email', 'url' ].indexOf( config.type ) !== -1 ?
14181 config.type :
14182 'text';
14183 return config.multiline ? $( '<textarea>' ) : $( '<input type="' + type + '" />' );
14184 };
14185
14186 /**
14187 * Check if the input supports multiple lines.
14188 *
14189 * @return {boolean}
14190 */
14191 OO.ui.TextInputWidget.prototype.isMultiline = function () {
14192 return !!this.multiline;
14193 };
14194
14195 /**
14196 * Check if the input automatically adjusts its size.
14197 *
14198 * @return {boolean}
14199 */
14200 OO.ui.TextInputWidget.prototype.isAutosizing = function () {
14201 return !!this.autosize;
14202 };
14203
14204 /**
14205 * Select the entire text of the input.
14206 *
14207 * @chainable
14208 */
14209 OO.ui.TextInputWidget.prototype.select = function () {
14210 this.$input.select();
14211 return this;
14212 };
14213
14214 /**
14215 * Set the validation pattern.
14216 *
14217 * The validation pattern is either a regular expression, a function, or the symbolic name of a
14218 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
14219 * value must contain only numbers).
14220 *
14221 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
14222 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
14223 */
14224 OO.ui.TextInputWidget.prototype.setValidation = function ( validate ) {
14225 if ( validate instanceof RegExp || validate instanceof Function ) {
14226 this.validate = validate;
14227 } else {
14228 this.validate = this.constructor.static.validationPatterns[ validate ] || /.*/;
14229 }
14230 };
14231
14232 /**
14233 * Sets the 'invalid' flag appropriately.
14234 *
14235 * @param {boolean} [isValid] Optionally override validation result
14236 */
14237 OO.ui.TextInputWidget.prototype.setValidityFlag = function ( isValid ) {
14238 var widget = this,
14239 setFlag = function ( valid ) {
14240 if ( !valid ) {
14241 widget.$input.attr( 'aria-invalid', 'true' );
14242 } else {
14243 widget.$input.removeAttr( 'aria-invalid' );
14244 }
14245 widget.setFlags( { invalid: !valid } );
14246 };
14247
14248 if ( isValid !== undefined ) {
14249 setFlag( isValid );
14250 } else {
14251 this.isValid().done( setFlag );
14252 }
14253 };
14254
14255 /**
14256 * Check if a value is valid.
14257 *
14258 * This method returns a promise that resolves with a boolean `true` if the current value is
14259 * considered valid according to the supplied {@link #validate validation pattern}.
14260 *
14261 * @return {jQuery.Promise} A promise that resolves to a boolean `true` if the value is valid.
14262 */
14263 OO.ui.TextInputWidget.prototype.isValid = function () {
14264 if ( this.validate instanceof Function ) {
14265 var result = this.validate( this.getValue() );
14266 if ( $.isFunction( result.promise ) ) {
14267 return result.promise();
14268 } else {
14269 return $.Deferred().resolve( !!result ).promise();
14270 }
14271 } else {
14272 return $.Deferred().resolve( !!this.getValue().match( this.validate ) ).promise();
14273 }
14274 };
14275
14276 /**
14277 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
14278 *
14279 * @param {string} labelPosition Label position, 'before' or 'after'
14280 * @chainable
14281 */
14282 OO.ui.TextInputWidget.prototype.setLabelPosition = function ( labelPosition ) {
14283 this.labelPosition = labelPosition;
14284 this.updatePosition();
14285 return this;
14286 };
14287
14288 /**
14289 * Deprecated alias of #setLabelPosition
14290 *
14291 * @deprecated Use setLabelPosition instead.
14292 */
14293 OO.ui.TextInputWidget.prototype.setPosition =
14294 OO.ui.TextInputWidget.prototype.setLabelPosition;
14295
14296 /**
14297 * Update the position of the inline label.
14298 *
14299 * This method is called by #setLabelPosition, and can also be called on its own if
14300 * something causes the label to be mispositioned.
14301 *
14302 *
14303 * @chainable
14304 */
14305 OO.ui.TextInputWidget.prototype.updatePosition = function () {
14306 var after = this.labelPosition === 'after';
14307
14308 this.$element
14309 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label && after )
14310 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label && !after );
14311
14312 if ( this.label ) {
14313 this.positionLabel();
14314 }
14315
14316 return this;
14317 };
14318
14319 /**
14320 * Position the label by setting the correct padding on the input.
14321 *
14322 * @private
14323 * @chainable
14324 */
14325 OO.ui.TextInputWidget.prototype.positionLabel = function () {
14326 // Clear old values
14327 this.$input
14328 // Clear old values if present
14329 .css( {
14330 'padding-right': '',
14331 'padding-left': ''
14332 } );
14333
14334 if ( this.label ) {
14335 this.$element.append( this.$label );
14336 } else {
14337 this.$label.detach();
14338 return;
14339 }
14340
14341 var after = this.labelPosition === 'after',
14342 rtl = this.$element.css( 'direction' ) === 'rtl',
14343 property = after === rtl ? 'padding-left' : 'padding-right';
14344
14345 this.$input.css( property, this.$label.outerWidth( true ) );
14346
14347 return this;
14348 };
14349
14350 /**
14351 * ComboBoxWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
14352 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
14353 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
14354 *
14355 * - by typing a value in the text input field. If the value exactly matches the value of a menu
14356 * option, that option will appear to be selected.
14357 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
14358 * input field.
14359 *
14360 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
14361 *
14362 * @example
14363 * // Example: A ComboBoxWidget.
14364 * var comboBox = new OO.ui.ComboBoxWidget( {
14365 * label: 'ComboBoxWidget',
14366 * input: { value: 'Option One' },
14367 * menu: {
14368 * items: [
14369 * new OO.ui.MenuOptionWidget( {
14370 * data: 'Option 1',
14371 * label: 'Option One'
14372 * } ),
14373 * new OO.ui.MenuOptionWidget( {
14374 * data: 'Option 2',
14375 * label: 'Option Two'
14376 * } ),
14377 * new OO.ui.MenuOptionWidget( {
14378 * data: 'Option 3',
14379 * label: 'Option Three'
14380 * } ),
14381 * new OO.ui.MenuOptionWidget( {
14382 * data: 'Option 4',
14383 * label: 'Option Four'
14384 * } ),
14385 * new OO.ui.MenuOptionWidget( {
14386 * data: 'Option 5',
14387 * label: 'Option Five'
14388 * } )
14389 * ]
14390 * }
14391 * } );
14392 * $( 'body' ).append( comboBox.$element );
14393 *
14394 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
14395 *
14396 * @class
14397 * @extends OO.ui.Widget
14398 * @mixins OO.ui.mixin.TabIndexedElement
14399 *
14400 * @constructor
14401 * @param {Object} [config] Configuration options
14402 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}.
14403 * @cfg {Object} [input] Configuration options to pass to the {@link OO.ui.TextInputWidget text input widget}.
14404 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
14405 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
14406 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
14407 */
14408 OO.ui.ComboBoxWidget = function OoUiComboBoxWidget( config ) {
14409 // Configuration initialization
14410 config = config || {};
14411
14412 // Parent constructor
14413 OO.ui.ComboBoxWidget.super.call( this, config );
14414
14415 // Properties (must be set before TabIndexedElement constructor call)
14416 this.$indicator = this.$( '<span>' );
14417
14418 // Mixin constructors
14419 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$indicator } ) );
14420
14421 // Properties
14422 this.$overlay = config.$overlay || this.$element;
14423 this.input = new OO.ui.TextInputWidget( $.extend(
14424 {
14425 indicator: 'down',
14426 $indicator: this.$indicator,
14427 disabled: this.isDisabled()
14428 },
14429 config.input
14430 ) );
14431 this.input.$input.eq( 0 ).attr( {
14432 role: 'combobox',
14433 'aria-autocomplete': 'list'
14434 } );
14435 this.menu = new OO.ui.TextInputMenuSelectWidget( this.input, $.extend(
14436 {
14437 widget: this,
14438 input: this.input,
14439 disabled: this.isDisabled()
14440 },
14441 config.menu
14442 ) );
14443
14444 // Events
14445 this.$indicator.on( {
14446 click: this.onClick.bind( this ),
14447 keypress: this.onKeyPress.bind( this )
14448 } );
14449 this.input.connect( this, {
14450 change: 'onInputChange',
14451 enter: 'onInputEnter'
14452 } );
14453 this.menu.connect( this, {
14454 choose: 'onMenuChoose',
14455 add: 'onMenuItemsChange',
14456 remove: 'onMenuItemsChange'
14457 } );
14458
14459 // Initialization
14460 this.$element.addClass( 'oo-ui-comboBoxWidget' ).append( this.input.$element );
14461 this.$overlay.append( this.menu.$element );
14462 this.onMenuItemsChange();
14463 };
14464
14465 /* Setup */
14466
14467 OO.inheritClass( OO.ui.ComboBoxWidget, OO.ui.Widget );
14468 OO.mixinClass( OO.ui.ComboBoxWidget, OO.ui.mixin.TabIndexedElement );
14469
14470 /* Methods */
14471
14472 /**
14473 * Get the combobox's menu.
14474 * @return {OO.ui.TextInputMenuSelectWidget} Menu widget
14475 */
14476 OO.ui.ComboBoxWidget.prototype.getMenu = function () {
14477 return this.menu;
14478 };
14479
14480 /**
14481 * Handle input change events.
14482 *
14483 * @private
14484 * @param {string} value New value
14485 */
14486 OO.ui.ComboBoxWidget.prototype.onInputChange = function ( value ) {
14487 var match = this.menu.getItemFromData( value );
14488
14489 this.menu.selectItem( match );
14490 if ( this.menu.getHighlightedItem() ) {
14491 this.menu.highlightItem( match );
14492 }
14493
14494 if ( !this.isDisabled() ) {
14495 this.menu.toggle( true );
14496 }
14497 };
14498
14499 /**
14500 * Handle mouse click events.
14501 *
14502 *
14503 * @private
14504 * @param {jQuery.Event} e Mouse click event
14505 */
14506 OO.ui.ComboBoxWidget.prototype.onClick = function ( e ) {
14507 if ( !this.isDisabled() && e.which === 1 ) {
14508 this.menu.toggle();
14509 this.input.$input[ 0 ].focus();
14510 }
14511 return false;
14512 };
14513
14514 /**
14515 * Handle key press events.
14516 *
14517 *
14518 * @private
14519 * @param {jQuery.Event} e Key press event
14520 */
14521 OO.ui.ComboBoxWidget.prototype.onKeyPress = function ( e ) {
14522 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
14523 this.menu.toggle();
14524 this.input.$input[ 0 ].focus();
14525 return false;
14526 }
14527 };
14528
14529 /**
14530 * Handle input enter events.
14531 *
14532 * @private
14533 */
14534 OO.ui.ComboBoxWidget.prototype.onInputEnter = function () {
14535 if ( !this.isDisabled() ) {
14536 this.menu.toggle( false );
14537 }
14538 };
14539
14540 /**
14541 * Handle menu choose events.
14542 *
14543 * @private
14544 * @param {OO.ui.OptionWidget} item Chosen item
14545 */
14546 OO.ui.ComboBoxWidget.prototype.onMenuChoose = function ( item ) {
14547 this.input.setValue( item.getData() );
14548 };
14549
14550 /**
14551 * Handle menu item change events.
14552 *
14553 * @private
14554 */
14555 OO.ui.ComboBoxWidget.prototype.onMenuItemsChange = function () {
14556 var match = this.menu.getItemFromData( this.input.getValue() );
14557 this.menu.selectItem( match );
14558 if ( this.menu.getHighlightedItem() ) {
14559 this.menu.highlightItem( match );
14560 }
14561 this.$element.toggleClass( 'oo-ui-comboBoxWidget-empty', this.menu.isEmpty() );
14562 };
14563
14564 /**
14565 * @inheritdoc
14566 */
14567 OO.ui.ComboBoxWidget.prototype.setDisabled = function ( disabled ) {
14568 // Parent method
14569 OO.ui.ComboBoxWidget.super.prototype.setDisabled.call( this, disabled );
14570
14571 if ( this.input ) {
14572 this.input.setDisabled( this.isDisabled() );
14573 }
14574 if ( this.menu ) {
14575 this.menu.setDisabled( this.isDisabled() );
14576 }
14577
14578 return this;
14579 };
14580
14581 /**
14582 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
14583 * be configured with a `label` option that is set to a string, a label node, or a function:
14584 *
14585 * - String: a plaintext string
14586 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
14587 * label that includes a link or special styling, such as a gray color or additional graphical elements.
14588 * - Function: a function that will produce a string in the future. Functions are used
14589 * in cases where the value of the label is not currently defined.
14590 *
14591 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
14592 * will come into focus when the label is clicked.
14593 *
14594 * @example
14595 * // Examples of LabelWidgets
14596 * var label1 = new OO.ui.LabelWidget( {
14597 * label: 'plaintext label'
14598 * } );
14599 * var label2 = new OO.ui.LabelWidget( {
14600 * label: $( '<a href="default.html">jQuery label</a>' )
14601 * } );
14602 * // Create a fieldset layout with fields for each example
14603 * var fieldset = new OO.ui.FieldsetLayout();
14604 * fieldset.addItems( [
14605 * new OO.ui.FieldLayout( label1 ),
14606 * new OO.ui.FieldLayout( label2 )
14607 * ] );
14608 * $( 'body' ).append( fieldset.$element );
14609 *
14610 *
14611 * @class
14612 * @extends OO.ui.Widget
14613 * @mixins OO.ui.mixin.LabelElement
14614 *
14615 * @constructor
14616 * @param {Object} [config] Configuration options
14617 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
14618 * Clicking the label will focus the specified input field.
14619 */
14620 OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
14621 // Configuration initialization
14622 config = config || {};
14623
14624 // Parent constructor
14625 OO.ui.LabelWidget.super.call( this, config );
14626
14627 // Mixin constructors
14628 OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, { $label: this.$element } ) );
14629 OO.ui.mixin.TitledElement.call( this, config );
14630
14631 // Properties
14632 this.input = config.input;
14633
14634 // Events
14635 if ( this.input instanceof OO.ui.InputWidget ) {
14636 this.$element.on( 'click', this.onClick.bind( this ) );
14637 }
14638
14639 // Initialization
14640 this.$element.addClass( 'oo-ui-labelWidget' );
14641 };
14642
14643 /* Setup */
14644
14645 OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
14646 OO.mixinClass( OO.ui.LabelWidget, OO.ui.mixin.LabelElement );
14647 OO.mixinClass( OO.ui.LabelWidget, OO.ui.mixin.TitledElement );
14648
14649 /* Static Properties */
14650
14651 OO.ui.LabelWidget.static.tagName = 'span';
14652
14653 /* Methods */
14654
14655 /**
14656 * Handles label mouse click events.
14657 *
14658 * @private
14659 * @param {jQuery.Event} e Mouse click event
14660 */
14661 OO.ui.LabelWidget.prototype.onClick = function () {
14662 this.input.simulateLabelClick();
14663 return false;
14664 };
14665
14666 /**
14667 * OptionWidgets are special elements that can be selected and configured with data. The
14668 * data is often unique for each option, but it does not have to be. OptionWidgets are used
14669 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
14670 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
14671 *
14672 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
14673 *
14674 * @class
14675 * @extends OO.ui.Widget
14676 * @mixins OO.ui.mixin.LabelElement
14677 * @mixins OO.ui.mixin.FlaggedElement
14678 *
14679 * @constructor
14680 * @param {Object} [config] Configuration options
14681 */
14682 OO.ui.OptionWidget = function OoUiOptionWidget( config ) {
14683 // Configuration initialization
14684 config = config || {};
14685
14686 // Parent constructor
14687 OO.ui.OptionWidget.super.call( this, config );
14688
14689 // Mixin constructors
14690 OO.ui.mixin.ItemWidget.call( this );
14691 OO.ui.mixin.LabelElement.call( this, config );
14692 OO.ui.mixin.FlaggedElement.call( this, config );
14693
14694 // Properties
14695 this.selected = false;
14696 this.highlighted = false;
14697 this.pressed = false;
14698
14699 // Initialization
14700 this.$element
14701 .data( 'oo-ui-optionWidget', this )
14702 .attr( 'role', 'option' )
14703 .addClass( 'oo-ui-optionWidget' )
14704 .append( this.$label );
14705 };
14706
14707 /* Setup */
14708
14709 OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
14710 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.ItemWidget );
14711 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.LabelElement );
14712 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.FlaggedElement );
14713
14714 /* Static Properties */
14715
14716 OO.ui.OptionWidget.static.selectable = true;
14717
14718 OO.ui.OptionWidget.static.highlightable = true;
14719
14720 OO.ui.OptionWidget.static.pressable = true;
14721
14722 OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
14723
14724 /* Methods */
14725
14726 /**
14727 * Check if the option can be selected.
14728 *
14729 * @return {boolean} Item is selectable
14730 */
14731 OO.ui.OptionWidget.prototype.isSelectable = function () {
14732 return this.constructor.static.selectable && !this.isDisabled();
14733 };
14734
14735 /**
14736 * Check if the option can be highlighted. A highlight indicates that the option
14737 * may be selected when a user presses enter or clicks. Disabled items cannot
14738 * be highlighted.
14739 *
14740 * @return {boolean} Item is highlightable
14741 */
14742 OO.ui.OptionWidget.prototype.isHighlightable = function () {
14743 return this.constructor.static.highlightable && !this.isDisabled();
14744 };
14745
14746 /**
14747 * Check if the option can be pressed. The pressed state occurs when a user mouses
14748 * down on an item, but has not yet let go of the mouse.
14749 *
14750 * @return {boolean} Item is pressable
14751 */
14752 OO.ui.OptionWidget.prototype.isPressable = function () {
14753 return this.constructor.static.pressable && !this.isDisabled();
14754 };
14755
14756 /**
14757 * Check if the option is selected.
14758 *
14759 * @return {boolean} Item is selected
14760 */
14761 OO.ui.OptionWidget.prototype.isSelected = function () {
14762 return this.selected;
14763 };
14764
14765 /**
14766 * Check if the option is highlighted. A highlight indicates that the
14767 * item may be selected when a user presses enter or clicks.
14768 *
14769 * @return {boolean} Item is highlighted
14770 */
14771 OO.ui.OptionWidget.prototype.isHighlighted = function () {
14772 return this.highlighted;
14773 };
14774
14775 /**
14776 * Check if the option is pressed. The pressed state occurs when a user mouses
14777 * down on an item, but has not yet let go of the mouse. The item may appear
14778 * selected, but it will not be selected until the user releases the mouse.
14779 *
14780 * @return {boolean} Item is pressed
14781 */
14782 OO.ui.OptionWidget.prototype.isPressed = function () {
14783 return this.pressed;
14784 };
14785
14786 /**
14787 * Set the option’s selected state. In general, all modifications to the selection
14788 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
14789 * method instead of this method.
14790 *
14791 * @param {boolean} [state=false] Select option
14792 * @chainable
14793 */
14794 OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
14795 if ( this.constructor.static.selectable ) {
14796 this.selected = !!state;
14797 this.$element
14798 .toggleClass( 'oo-ui-optionWidget-selected', state )
14799 .attr( 'aria-selected', state.toString() );
14800 if ( state && this.constructor.static.scrollIntoViewOnSelect ) {
14801 this.scrollElementIntoView();
14802 }
14803 this.updateThemeClasses();
14804 }
14805 return this;
14806 };
14807
14808 /**
14809 * Set the option’s highlighted state. In general, all programmatic
14810 * modifications to the highlight should be handled by the
14811 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
14812 * method instead of this method.
14813 *
14814 * @param {boolean} [state=false] Highlight option
14815 * @chainable
14816 */
14817 OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
14818 if ( this.constructor.static.highlightable ) {
14819 this.highlighted = !!state;
14820 this.$element.toggleClass( 'oo-ui-optionWidget-highlighted', state );
14821 this.updateThemeClasses();
14822 }
14823 return this;
14824 };
14825
14826 /**
14827 * Set the option’s pressed state. In general, all
14828 * programmatic modifications to the pressed state should be handled by the
14829 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
14830 * method instead of this method.
14831 *
14832 * @param {boolean} [state=false] Press option
14833 * @chainable
14834 */
14835 OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
14836 if ( this.constructor.static.pressable ) {
14837 this.pressed = !!state;
14838 this.$element.toggleClass( 'oo-ui-optionWidget-pressed', state );
14839 this.updateThemeClasses();
14840 }
14841 return this;
14842 };
14843
14844 /**
14845 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
14846 * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
14847 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
14848 * options. For more information about options and selects, please see the
14849 * [OOjs UI documentation on MediaWiki][1].
14850 *
14851 * @example
14852 * // Decorated options in a select widget
14853 * var select = new OO.ui.SelectWidget( {
14854 * items: [
14855 * new OO.ui.DecoratedOptionWidget( {
14856 * data: 'a',
14857 * label: 'Option with icon',
14858 * icon: 'help'
14859 * } ),
14860 * new OO.ui.DecoratedOptionWidget( {
14861 * data: 'b',
14862 * label: 'Option with indicator',
14863 * indicator: 'next'
14864 * } )
14865 * ]
14866 * } );
14867 * $( 'body' ).append( select.$element );
14868 *
14869 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
14870 *
14871 * @class
14872 * @extends OO.ui.OptionWidget
14873 * @mixins OO.ui.mixin.IconElement
14874 * @mixins OO.ui.mixin.IndicatorElement
14875 *
14876 * @constructor
14877 * @param {Object} [config] Configuration options
14878 */
14879 OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( config ) {
14880 // Parent constructor
14881 OO.ui.DecoratedOptionWidget.super.call( this, config );
14882
14883 // Mixin constructors
14884 OO.ui.mixin.IconElement.call( this, config );
14885 OO.ui.mixin.IndicatorElement.call( this, config );
14886
14887 // Initialization
14888 this.$element
14889 .addClass( 'oo-ui-decoratedOptionWidget' )
14890 .prepend( this.$icon )
14891 .append( this.$indicator );
14892 };
14893
14894 /* Setup */
14895
14896 OO.inheritClass( OO.ui.DecoratedOptionWidget, OO.ui.OptionWidget );
14897 OO.mixinClass( OO.ui.DecoratedOptionWidget, OO.ui.mixin.IconElement );
14898 OO.mixinClass( OO.ui.DecoratedOptionWidget, OO.ui.mixin.IndicatorElement );
14899
14900 /**
14901 * ButtonOptionWidget is a special type of {@link OO.ui.mixin.ButtonElement button element} that
14902 * can be selected and configured with data. The class is
14903 * used with OO.ui.ButtonSelectWidget to create a selection of button options. Please see the
14904 * [OOjs UI documentation on MediaWiki] [1] for more information.
14905 *
14906 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_options
14907 *
14908 * @class
14909 * @extends OO.ui.DecoratedOptionWidget
14910 * @mixins OO.ui.mixin.ButtonElement
14911 * @mixins OO.ui.mixin.TabIndexedElement
14912 *
14913 * @constructor
14914 * @param {Object} [config] Configuration options
14915 */
14916 OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( config ) {
14917 // Configuration initialization
14918 config = $.extend( { tabIndex: -1 }, config );
14919
14920 // Parent constructor
14921 OO.ui.ButtonOptionWidget.super.call( this, config );
14922
14923 // Mixin constructors
14924 OO.ui.mixin.ButtonElement.call( this, config );
14925 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
14926
14927 // Initialization
14928 this.$element.addClass( 'oo-ui-buttonOptionWidget' );
14929 this.$button.append( this.$element.contents() );
14930 this.$element.append( this.$button );
14931 };
14932
14933 /* Setup */
14934
14935 OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.DecoratedOptionWidget );
14936 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.mixin.ButtonElement );
14937 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.mixin.TabIndexedElement );
14938
14939 /* Static Properties */
14940
14941 // Allow button mouse down events to pass through so they can be handled by the parent select widget
14942 OO.ui.ButtonOptionWidget.static.cancelButtonMouseDownEvents = false;
14943
14944 OO.ui.ButtonOptionWidget.static.highlightable = false;
14945
14946 /* Methods */
14947
14948 /**
14949 * @inheritdoc
14950 */
14951 OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) {
14952 OO.ui.ButtonOptionWidget.super.prototype.setSelected.call( this, state );
14953
14954 if ( this.constructor.static.selectable ) {
14955 this.setActive( state );
14956 }
14957
14958 return this;
14959 };
14960
14961 /**
14962 * RadioOptionWidget is an option widget that looks like a radio button.
14963 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
14964 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
14965 *
14966 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
14967 *
14968 * @class
14969 * @extends OO.ui.OptionWidget
14970 *
14971 * @constructor
14972 * @param {Object} [config] Configuration options
14973 */
14974 OO.ui.RadioOptionWidget = function OoUiRadioOptionWidget( config ) {
14975 // Configuration initialization
14976 config = config || {};
14977
14978 // Properties (must be done before parent constructor which calls #setDisabled)
14979 this.radio = new OO.ui.RadioInputWidget( { value: config.data, tabIndex: -1 } );
14980
14981 // Parent constructor
14982 OO.ui.RadioOptionWidget.super.call( this, config );
14983
14984 // Events
14985 this.radio.$input.on( 'focus', this.onInputFocus.bind( this ) );
14986
14987 // Initialization
14988 this.$element
14989 .addClass( 'oo-ui-radioOptionWidget' )
14990 .prepend( this.radio.$element );
14991 };
14992
14993 /* Setup */
14994
14995 OO.inheritClass( OO.ui.RadioOptionWidget, OO.ui.OptionWidget );
14996
14997 /* Static Properties */
14998
14999 OO.ui.RadioOptionWidget.static.highlightable = false;
15000
15001 OO.ui.RadioOptionWidget.static.scrollIntoViewOnSelect = true;
15002
15003 OO.ui.RadioOptionWidget.static.pressable = false;
15004
15005 OO.ui.RadioOptionWidget.static.tagName = 'label';
15006
15007 /* Methods */
15008
15009 /**
15010 * @param {jQuery.Event} e Focus event
15011 * @private
15012 */
15013 OO.ui.RadioOptionWidget.prototype.onInputFocus = function () {
15014 this.radio.$input.blur();
15015 this.$element.parent().focus();
15016 };
15017
15018 /**
15019 * @inheritdoc
15020 */
15021 OO.ui.RadioOptionWidget.prototype.setSelected = function ( state ) {
15022 OO.ui.RadioOptionWidget.super.prototype.setSelected.call( this, state );
15023
15024 this.radio.setSelected( state );
15025
15026 return this;
15027 };
15028
15029 /**
15030 * @inheritdoc
15031 */
15032 OO.ui.RadioOptionWidget.prototype.setDisabled = function ( disabled ) {
15033 OO.ui.RadioOptionWidget.super.prototype.setDisabled.call( this, disabled );
15034
15035 this.radio.setDisabled( this.isDisabled() );
15036
15037 return this;
15038 };
15039
15040 /**
15041 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
15042 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
15043 * the [OOjs UI documentation on MediaWiki] [1] for more information.
15044 *
15045 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
15046 *
15047 * @class
15048 * @extends OO.ui.DecoratedOptionWidget
15049 *
15050 * @constructor
15051 * @param {Object} [config] Configuration options
15052 */
15053 OO.ui.MenuOptionWidget = function OoUiMenuOptionWidget( config ) {
15054 // Configuration initialization
15055 config = $.extend( { icon: 'check' }, config );
15056
15057 // Parent constructor
15058 OO.ui.MenuOptionWidget.super.call( this, config );
15059
15060 // Initialization
15061 this.$element
15062 .attr( 'role', 'menuitem' )
15063 .addClass( 'oo-ui-menuOptionWidget' );
15064 };
15065
15066 /* Setup */
15067
15068 OO.inheritClass( OO.ui.MenuOptionWidget, OO.ui.DecoratedOptionWidget );
15069
15070 /* Static Properties */
15071
15072 OO.ui.MenuOptionWidget.static.scrollIntoViewOnSelect = true;
15073
15074 /**
15075 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
15076 * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
15077 *
15078 * @example
15079 * var myDropdown = new OO.ui.DropdownWidget( {
15080 * menu: {
15081 * items: [
15082 * new OO.ui.MenuSectionOptionWidget( {
15083 * label: 'Dogs'
15084 * } ),
15085 * new OO.ui.MenuOptionWidget( {
15086 * data: 'corgi',
15087 * label: 'Welsh Corgi'
15088 * } ),
15089 * new OO.ui.MenuOptionWidget( {
15090 * data: 'poodle',
15091 * label: 'Standard Poodle'
15092 * } ),
15093 * new OO.ui.MenuSectionOptionWidget( {
15094 * label: 'Cats'
15095 * } ),
15096 * new OO.ui.MenuOptionWidget( {
15097 * data: 'lion',
15098 * label: 'Lion'
15099 * } )
15100 * ]
15101 * }
15102 * } );
15103 * $( 'body' ).append( myDropdown.$element );
15104 *
15105 *
15106 * @class
15107 * @extends OO.ui.DecoratedOptionWidget
15108 *
15109 * @constructor
15110 * @param {Object} [config] Configuration options
15111 */
15112 OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( config ) {
15113 // Parent constructor
15114 OO.ui.MenuSectionOptionWidget.super.call( this, config );
15115
15116 // Initialization
15117 this.$element.addClass( 'oo-ui-menuSectionOptionWidget' );
15118 };
15119
15120 /* Setup */
15121
15122 OO.inheritClass( OO.ui.MenuSectionOptionWidget, OO.ui.DecoratedOptionWidget );
15123
15124 /* Static Properties */
15125
15126 OO.ui.MenuSectionOptionWidget.static.selectable = false;
15127
15128 OO.ui.MenuSectionOptionWidget.static.highlightable = false;
15129
15130 /**
15131 * OutlineOptionWidget is an item in an {@link OO.ui.OutlineSelectWidget OutlineSelectWidget}.
15132 *
15133 * Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}, which contain
15134 * {@link OO.ui.PageLayout page layouts}. See {@link OO.ui.BookletLayout BookletLayout}
15135 * for an example.
15136 *
15137 * @class
15138 * @extends OO.ui.DecoratedOptionWidget
15139 *
15140 * @constructor
15141 * @param {Object} [config] Configuration options
15142 * @cfg {number} [level] Indentation level
15143 * @cfg {boolean} [movable] Allow modification from {@link OO.ui.OutlineControlsWidget outline controls}.
15144 */
15145 OO.ui.OutlineOptionWidget = function OoUiOutlineOptionWidget( config ) {
15146 // Configuration initialization
15147 config = config || {};
15148
15149 // Parent constructor
15150 OO.ui.OutlineOptionWidget.super.call( this, config );
15151
15152 // Properties
15153 this.level = 0;
15154 this.movable = !!config.movable;
15155 this.removable = !!config.removable;
15156
15157 // Initialization
15158 this.$element.addClass( 'oo-ui-outlineOptionWidget' );
15159 this.setLevel( config.level );
15160 };
15161
15162 /* Setup */
15163
15164 OO.inheritClass( OO.ui.OutlineOptionWidget, OO.ui.DecoratedOptionWidget );
15165
15166 /* Static Properties */
15167
15168 OO.ui.OutlineOptionWidget.static.highlightable = false;
15169
15170 OO.ui.OutlineOptionWidget.static.scrollIntoViewOnSelect = true;
15171
15172 OO.ui.OutlineOptionWidget.static.levelClass = 'oo-ui-outlineOptionWidget-level-';
15173
15174 OO.ui.OutlineOptionWidget.static.levels = 3;
15175
15176 /* Methods */
15177
15178 /**
15179 * Check if item is movable.
15180 *
15181 * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
15182 *
15183 * @return {boolean} Item is movable
15184 */
15185 OO.ui.OutlineOptionWidget.prototype.isMovable = function () {
15186 return this.movable;
15187 };
15188
15189 /**
15190 * Check if item is removable.
15191 *
15192 * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
15193 *
15194 * @return {boolean} Item is removable
15195 */
15196 OO.ui.OutlineOptionWidget.prototype.isRemovable = function () {
15197 return this.removable;
15198 };
15199
15200 /**
15201 * Get indentation level.
15202 *
15203 * @return {number} Indentation level
15204 */
15205 OO.ui.OutlineOptionWidget.prototype.getLevel = function () {
15206 return this.level;
15207 };
15208
15209 /**
15210 * Set movability.
15211 *
15212 * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
15213 *
15214 * @param {boolean} movable Item is movable
15215 * @chainable
15216 */
15217 OO.ui.OutlineOptionWidget.prototype.setMovable = function ( movable ) {
15218 this.movable = !!movable;
15219 this.updateThemeClasses();
15220 return this;
15221 };
15222
15223 /**
15224 * Set removability.
15225 *
15226 * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
15227 *
15228 * @param {boolean} movable Item is removable
15229 * @chainable
15230 */
15231 OO.ui.OutlineOptionWidget.prototype.setRemovable = function ( removable ) {
15232 this.removable = !!removable;
15233 this.updateThemeClasses();
15234 return this;
15235 };
15236
15237 /**
15238 * Set indentation level.
15239 *
15240 * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
15241 * @chainable
15242 */
15243 OO.ui.OutlineOptionWidget.prototype.setLevel = function ( level ) {
15244 var levels = this.constructor.static.levels,
15245 levelClass = this.constructor.static.levelClass,
15246 i = levels;
15247
15248 this.level = level ? Math.max( 0, Math.min( levels - 1, level ) ) : 0;
15249 while ( i-- ) {
15250 if ( this.level === i ) {
15251 this.$element.addClass( levelClass + i );
15252 } else {
15253 this.$element.removeClass( levelClass + i );
15254 }
15255 }
15256 this.updateThemeClasses();
15257
15258 return this;
15259 };
15260
15261 /**
15262 * TabOptionWidget is an item in a {@link OO.ui.TabSelectWidget TabSelectWidget}.
15263 *
15264 * Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}, which contain
15265 * {@link OO.ui.CardLayout card layouts}. See {@link OO.ui.IndexLayout IndexLayout}
15266 * for an example.
15267 *
15268 * @class
15269 * @extends OO.ui.OptionWidget
15270 *
15271 * @constructor
15272 * @param {Object} [config] Configuration options
15273 */
15274 OO.ui.TabOptionWidget = function OoUiTabOptionWidget( config ) {
15275 // Configuration initialization
15276 config = config || {};
15277
15278 // Parent constructor
15279 OO.ui.TabOptionWidget.super.call( this, config );
15280
15281 // Initialization
15282 this.$element.addClass( 'oo-ui-tabOptionWidget' );
15283 };
15284
15285 /* Setup */
15286
15287 OO.inheritClass( OO.ui.TabOptionWidget, OO.ui.OptionWidget );
15288
15289 /* Static Properties */
15290
15291 OO.ui.TabOptionWidget.static.highlightable = false;
15292
15293 /**
15294 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
15295 * By default, each popup has an anchor that points toward its origin.
15296 * Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples.
15297 *
15298 * @example
15299 * // A popup widget.
15300 * var popup = new OO.ui.PopupWidget( {
15301 * $content: $( '<p>Hi there!</p>' ),
15302 * padded: true,
15303 * width: 300
15304 * } );
15305 *
15306 * $( 'body' ).append( popup.$element );
15307 * // To display the popup, toggle the visibility to 'true'.
15308 * popup.toggle( true );
15309 *
15310 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups
15311 *
15312 * @class
15313 * @extends OO.ui.Widget
15314 * @mixins OO.ui.mixin.LabelElement
15315 *
15316 * @constructor
15317 * @param {Object} [config] Configuration options
15318 * @cfg {number} [width=320] Width of popup in pixels
15319 * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
15320 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
15321 * @cfg {string} [align='center'] Alignment of the popup: `center`, `force-left`, `force-right`, `backwards` or `forwards`.
15322 * If the popup is forced-left the popup body is leaning towards the left. For force-right alignment, the body of the
15323 * popup is leaning towards the right of the screen.
15324 * Using 'backwards' is a logical direction which will result in the popup leaning towards the beginning of the sentence
15325 * in the given language, which means it will flip to the correct positioning in right-to-left languages.
15326 * Using 'forward' will also result in a logical alignment where the body of the popup leans towards the end of the
15327 * sentence in the given language.
15328 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
15329 * See the [OOjs UI docs on MediaWiki][3] for an example.
15330 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
15331 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
15332 * @cfg {jQuery} [$content] Content to append to the popup's body
15333 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
15334 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
15335 * This config option is only relevant if #autoClose is set to `true`. See the [OOjs UI docs on MediaWiki][2]
15336 * for an example.
15337 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#autocloseExample
15338 * @cfg {boolean} [head] Show a popup header that contains a #label (if specified) and close
15339 * button.
15340 * @cfg {boolean} [padded] Add padding to the popup's body
15341 */
15342 OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
15343 // Configuration initialization
15344 config = config || {};
15345
15346 // Parent constructor
15347 OO.ui.PopupWidget.super.call( this, config );
15348
15349 // Properties (must be set before ClippableElement constructor call)
15350 this.$body = $( '<div>' );
15351
15352 // Mixin constructors
15353 OO.ui.mixin.LabelElement.call( this, config );
15354 OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$body } ) );
15355
15356 // Properties
15357 this.$popup = $( '<div>' );
15358 this.$head = $( '<div>' );
15359 this.$anchor = $( '<div>' );
15360 // If undefined, will be computed lazily in updateDimensions()
15361 this.$container = config.$container;
15362 this.containerPadding = config.containerPadding !== undefined ? config.containerPadding : 10;
15363 this.autoClose = !!config.autoClose;
15364 this.$autoCloseIgnore = config.$autoCloseIgnore;
15365 this.transitionTimeout = null;
15366 this.anchor = null;
15367 this.width = config.width !== undefined ? config.width : 320;
15368 this.height = config.height !== undefined ? config.height : null;
15369 this.setAlignment( config.align );
15370 this.closeButton = new OO.ui.ButtonWidget( { framed: false, icon: 'close' } );
15371 this.onMouseDownHandler = this.onMouseDown.bind( this );
15372 this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
15373
15374 // Events
15375 this.closeButton.connect( this, { click: 'onCloseButtonClick' } );
15376
15377 // Initialization
15378 this.toggleAnchor( config.anchor === undefined || config.anchor );
15379 this.$body.addClass( 'oo-ui-popupWidget-body' );
15380 this.$anchor.addClass( 'oo-ui-popupWidget-anchor' );
15381 this.$head
15382 .addClass( 'oo-ui-popupWidget-head' )
15383 .append( this.$label, this.closeButton.$element );
15384 if ( !config.head ) {
15385 this.$head.addClass( 'oo-ui-element-hidden' );
15386 }
15387 this.$popup
15388 .addClass( 'oo-ui-popupWidget-popup' )
15389 .append( this.$head, this.$body );
15390 this.$element
15391 .addClass( 'oo-ui-popupWidget' )
15392 .append( this.$popup, this.$anchor );
15393 // Move content, which was added to #$element by OO.ui.Widget, to the body
15394 if ( config.$content instanceof jQuery ) {
15395 this.$body.append( config.$content );
15396 }
15397 if ( config.padded ) {
15398 this.$body.addClass( 'oo-ui-popupWidget-body-padded' );
15399 }
15400
15401 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
15402 // that reference properties not initialized at that time of parent class construction
15403 // TODO: Find a better way to handle post-constructor setup
15404 this.visible = false;
15405 this.$element.addClass( 'oo-ui-element-hidden' );
15406 };
15407
15408 /* Setup */
15409
15410 OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
15411 OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.LabelElement );
15412 OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.ClippableElement );
15413
15414 /* Methods */
15415
15416 /**
15417 * Handles mouse down events.
15418 *
15419 * @private
15420 * @param {MouseEvent} e Mouse down event
15421 */
15422 OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
15423 if (
15424 this.isVisible() &&
15425 !$.contains( this.$element[ 0 ], e.target ) &&
15426 ( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length )
15427 ) {
15428 this.toggle( false );
15429 }
15430 };
15431
15432 /**
15433 * Bind mouse down listener.
15434 *
15435 * @private
15436 */
15437 OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
15438 // Capture clicks outside popup
15439 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
15440 };
15441
15442 /**
15443 * Handles close button click events.
15444 *
15445 * @private
15446 */
15447 OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
15448 if ( this.isVisible() ) {
15449 this.toggle( false );
15450 }
15451 };
15452
15453 /**
15454 * Unbind mouse down listener.
15455 *
15456 * @private
15457 */
15458 OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
15459 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
15460 };
15461
15462 /**
15463 * Handles key down events.
15464 *
15465 * @private
15466 * @param {KeyboardEvent} e Key down event
15467 */
15468 OO.ui.PopupWidget.prototype.onDocumentKeyDown = function ( e ) {
15469 if (
15470 e.which === OO.ui.Keys.ESCAPE &&
15471 this.isVisible()
15472 ) {
15473 this.toggle( false );
15474 e.preventDefault();
15475 e.stopPropagation();
15476 }
15477 };
15478
15479 /**
15480 * Bind key down listener.
15481 *
15482 * @private
15483 */
15484 OO.ui.PopupWidget.prototype.bindKeyDownListener = function () {
15485 this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
15486 };
15487
15488 /**
15489 * Unbind key down listener.
15490 *
15491 * @private
15492 */
15493 OO.ui.PopupWidget.prototype.unbindKeyDownListener = function () {
15494 this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
15495 };
15496
15497 /**
15498 * Show, hide, or toggle the visibility of the anchor.
15499 *
15500 * @param {boolean} [show] Show anchor, omit to toggle
15501 */
15502 OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) {
15503 show = show === undefined ? !this.anchored : !!show;
15504
15505 if ( this.anchored !== show ) {
15506 if ( show ) {
15507 this.$element.addClass( 'oo-ui-popupWidget-anchored' );
15508 } else {
15509 this.$element.removeClass( 'oo-ui-popupWidget-anchored' );
15510 }
15511 this.anchored = show;
15512 }
15513 };
15514
15515 /**
15516 * Check if the anchor is visible.
15517 *
15518 * @return {boolean} Anchor is visible
15519 */
15520 OO.ui.PopupWidget.prototype.hasAnchor = function () {
15521 return this.anchor;
15522 };
15523
15524 /**
15525 * @inheritdoc
15526 */
15527 OO.ui.PopupWidget.prototype.toggle = function ( show ) {
15528 show = show === undefined ? !this.isVisible() : !!show;
15529
15530 var change = show !== this.isVisible();
15531
15532 // Parent method
15533 OO.ui.PopupWidget.super.prototype.toggle.call( this, show );
15534
15535 if ( change ) {
15536 if ( show ) {
15537 if ( this.autoClose ) {
15538 this.bindMouseDownListener();
15539 this.bindKeyDownListener();
15540 }
15541 this.updateDimensions();
15542 this.toggleClipping( true );
15543 } else {
15544 this.toggleClipping( false );
15545 if ( this.autoClose ) {
15546 this.unbindMouseDownListener();
15547 this.unbindKeyDownListener();
15548 }
15549 }
15550 }
15551
15552 return this;
15553 };
15554
15555 /**
15556 * Set the size of the popup.
15557 *
15558 * Changing the size may also change the popup's position depending on the alignment.
15559 *
15560 * @param {number} width Width in pixels
15561 * @param {number} height Height in pixels
15562 * @param {boolean} [transition=false] Use a smooth transition
15563 * @chainable
15564 */
15565 OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) {
15566 this.width = width;
15567 this.height = height !== undefined ? height : null;
15568 if ( this.isVisible() ) {
15569 this.updateDimensions( transition );
15570 }
15571 };
15572
15573 /**
15574 * Update the size and position.
15575 *
15576 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
15577 * be called automatically.
15578 *
15579 * @param {boolean} [transition=false] Use a smooth transition
15580 * @chainable
15581 */
15582 OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
15583 var popupOffset, originOffset, containerLeft, containerWidth, containerRight,
15584 popupLeft, popupRight, overlapLeft, overlapRight, anchorWidth,
15585 align = this.align,
15586 widget = this;
15587
15588 if ( !this.$container ) {
15589 // Lazy-initialize $container if not specified in constructor
15590 this.$container = $( this.getClosestScrollableElementContainer() );
15591 }
15592
15593 // Set height and width before measuring things, since it might cause our measurements
15594 // to change (e.g. due to scrollbars appearing or disappearing)
15595 this.$popup.css( {
15596 width: this.width,
15597 height: this.height !== null ? this.height : 'auto'
15598 } );
15599
15600 // If we are in RTL, we need to flip the alignment, unless it is center
15601 if ( align === 'forwards' || align === 'backwards' ) {
15602 if ( this.$container.css( 'direction' ) === 'rtl' ) {
15603 align = ( { forwards: 'force-left', backwards: 'force-right' } )[ this.align ];
15604 } else {
15605 align = ( { forwards: 'force-right', backwards: 'force-left' } )[ this.align ];
15606 }
15607
15608 }
15609
15610 // Compute initial popupOffset based on alignment
15611 popupOffset = this.width * ( { 'force-left': -1, center: -0.5, 'force-right': 0 } )[ align ];
15612
15613 // Figure out if this will cause the popup to go beyond the edge of the container
15614 originOffset = this.$element.offset().left;
15615 containerLeft = this.$container.offset().left;
15616 containerWidth = this.$container.innerWidth();
15617 containerRight = containerLeft + containerWidth;
15618 popupLeft = popupOffset - this.containerPadding;
15619 popupRight = popupOffset + this.containerPadding + this.width + this.containerPadding;
15620 overlapLeft = ( originOffset + popupLeft ) - containerLeft;
15621 overlapRight = containerRight - ( originOffset + popupRight );
15622
15623 // Adjust offset to make the popup not go beyond the edge, if needed
15624 if ( overlapRight < 0 ) {
15625 popupOffset += overlapRight;
15626 } else if ( overlapLeft < 0 ) {
15627 popupOffset -= overlapLeft;
15628 }
15629
15630 // Adjust offset to avoid anchor being rendered too close to the edge
15631 // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
15632 // TODO: Find a measurement that works for CSS anchors and image anchors
15633 anchorWidth = this.$anchor[ 0 ].scrollWidth * 2;
15634 if ( popupOffset + this.width < anchorWidth ) {
15635 popupOffset = anchorWidth - this.width;
15636 } else if ( -popupOffset < anchorWidth ) {
15637 popupOffset = -anchorWidth;
15638 }
15639
15640 // Prevent transition from being interrupted
15641 clearTimeout( this.transitionTimeout );
15642 if ( transition ) {
15643 // Enable transition
15644 this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
15645 }
15646
15647 // Position body relative to anchor
15648 this.$popup.css( 'margin-left', popupOffset );
15649
15650 if ( transition ) {
15651 // Prevent transitioning after transition is complete
15652 this.transitionTimeout = setTimeout( function () {
15653 widget.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
15654 }, 200 );
15655 } else {
15656 // Prevent transitioning immediately
15657 this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
15658 }
15659
15660 // Reevaluate clipping state since we've relocated and resized the popup
15661 this.clip();
15662
15663 return this;
15664 };
15665
15666 /**
15667 * Set popup alignment
15668 * @param {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
15669 * `backwards` or `forwards`.
15670 */
15671 OO.ui.PopupWidget.prototype.setAlignment = function ( align ) {
15672 // Validate alignment and transform deprecated values
15673 if ( [ 'left', 'right', 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align ) > -1 ) {
15674 this.align = { left: 'force-right', right: 'force-left' }[ align ] || align;
15675 } else {
15676 this.align = 'center';
15677 }
15678 };
15679
15680 /**
15681 * Get popup alignment
15682 * @return {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
15683 * `backwards` or `forwards`.
15684 */
15685 OO.ui.PopupWidget.prototype.getAlignment = function () {
15686 return this.align;
15687 };
15688
15689 /**
15690 * Progress bars visually display the status of an operation, such as a download,
15691 * and can be either determinate or indeterminate:
15692 *
15693 * - **determinate** process bars show the percent of an operation that is complete.
15694 *
15695 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
15696 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
15697 * not use percentages.
15698 *
15699 * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
15700 *
15701 * @example
15702 * // Examples of determinate and indeterminate progress bars.
15703 * var progressBar1 = new OO.ui.ProgressBarWidget( {
15704 * progress: 33
15705 * } );
15706 * var progressBar2 = new OO.ui.ProgressBarWidget();
15707 *
15708 * // Create a FieldsetLayout to layout progress bars
15709 * var fieldset = new OO.ui.FieldsetLayout;
15710 * fieldset.addItems( [
15711 * new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
15712 * new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
15713 * ] );
15714 * $( 'body' ).append( fieldset.$element );
15715 *
15716 * @class
15717 * @extends OO.ui.Widget
15718 *
15719 * @constructor
15720 * @param {Object} [config] Configuration options
15721 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
15722 * To create a determinate progress bar, specify a number that reflects the initial percent complete.
15723 * By default, the progress bar is indeterminate.
15724 */
15725 OO.ui.ProgressBarWidget = function OoUiProgressBarWidget( config ) {
15726 // Configuration initialization
15727 config = config || {};
15728
15729 // Parent constructor
15730 OO.ui.ProgressBarWidget.super.call( this, config );
15731
15732 // Properties
15733 this.$bar = $( '<div>' );
15734 this.progress = null;
15735
15736 // Initialization
15737 this.setProgress( config.progress !== undefined ? config.progress : false );
15738 this.$bar.addClass( 'oo-ui-progressBarWidget-bar' );
15739 this.$element
15740 .attr( {
15741 role: 'progressbar',
15742 'aria-valuemin': 0,
15743 'aria-valuemax': 100
15744 } )
15745 .addClass( 'oo-ui-progressBarWidget' )
15746 .append( this.$bar );
15747 };
15748
15749 /* Setup */
15750
15751 OO.inheritClass( OO.ui.ProgressBarWidget, OO.ui.Widget );
15752
15753 /* Static Properties */
15754
15755 OO.ui.ProgressBarWidget.static.tagName = 'div';
15756
15757 /* Methods */
15758
15759 /**
15760 * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
15761 *
15762 * @return {number|boolean} Progress percent
15763 */
15764 OO.ui.ProgressBarWidget.prototype.getProgress = function () {
15765 return this.progress;
15766 };
15767
15768 /**
15769 * Set the percent of the process completed or `false` for an indeterminate process.
15770 *
15771 * @param {number|boolean} progress Progress percent or `false` for indeterminate
15772 */
15773 OO.ui.ProgressBarWidget.prototype.setProgress = function ( progress ) {
15774 this.progress = progress;
15775
15776 if ( progress !== false ) {
15777 this.$bar.css( 'width', this.progress + '%' );
15778 this.$element.attr( 'aria-valuenow', this.progress );
15779 } else {
15780 this.$bar.css( 'width', '' );
15781 this.$element.removeAttr( 'aria-valuenow' );
15782 }
15783 this.$element.toggleClass( 'oo-ui-progressBarWidget-indeterminate', !progress );
15784 };
15785
15786 /**
15787 * SearchWidgets combine a {@link OO.ui.TextInputWidget text input field}, where users can type a search query,
15788 * and a {@link OO.ui.TextInputMenuSelectWidget menu} of search results, which is displayed beneath the query
15789 * field. Unlike {@link OO.ui.mixin.LookupElement lookup menus}, search result menus are always visible to the user.
15790 * Users can choose an item from the menu or type a query into the text field to search for a matching result item.
15791 * In general, search widgets are used inside a separate {@link OO.ui.Dialog dialog} window.
15792 *
15793 * Each time the query is changed, the search result menu is cleared and repopulated. Please see
15794 * the [OOjs UI demos][1] for an example.
15795 *
15796 * [1]: https://tools.wmflabs.org/oojs-ui/oojs-ui/demos/#dialogs-mediawiki-vector-ltr
15797 *
15798 * @class
15799 * @extends OO.ui.Widget
15800 *
15801 * @constructor
15802 * @param {Object} [config] Configuration options
15803 * @cfg {string|jQuery} [placeholder] Placeholder text for query input
15804 * @cfg {string} [value] Initial query value
15805 */
15806 OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
15807 // Configuration initialization
15808 config = config || {};
15809
15810 // Parent constructor
15811 OO.ui.SearchWidget.super.call( this, config );
15812
15813 // Properties
15814 this.query = new OO.ui.TextInputWidget( {
15815 icon: 'search',
15816 placeholder: config.placeholder,
15817 value: config.value
15818 } );
15819 this.results = new OO.ui.SelectWidget();
15820 this.$query = $( '<div>' );
15821 this.$results = $( '<div>' );
15822
15823 // Events
15824 this.query.connect( this, {
15825 change: 'onQueryChange',
15826 enter: 'onQueryEnter'
15827 } );
15828 this.results.connect( this, {
15829 highlight: 'onResultsHighlight',
15830 select: 'onResultsSelect'
15831 } );
15832 this.query.$input.on( 'keydown', this.onQueryKeydown.bind( this ) );
15833
15834 // Initialization
15835 this.$query
15836 .addClass( 'oo-ui-searchWidget-query' )
15837 .append( this.query.$element );
15838 this.$results
15839 .addClass( 'oo-ui-searchWidget-results' )
15840 .append( this.results.$element );
15841 this.$element
15842 .addClass( 'oo-ui-searchWidget' )
15843 .append( this.$results, this.$query );
15844 };
15845
15846 /* Setup */
15847
15848 OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget );
15849
15850 /* Events */
15851
15852 /**
15853 * A 'highlight' event is emitted when an item is highlighted. The highlight indicates which
15854 * item will be selected. When a user mouses over a menu item, it is highlighted. If a search
15855 * string is typed into the query field instead, the first menu item that matches the query
15856 * will be highlighted.
15857
15858 * @event highlight
15859 * @deprecated Connect straight to getResults() events instead
15860 * @param {Object|null} item Item data or null if no item is highlighted
15861 */
15862
15863 /**
15864 * A 'select' event is emitted when an item is selected. A menu item is selected when it is clicked,
15865 * or when a user types a search query, a menu result is highlighted, and the user presses enter.
15866 *
15867 * @event select
15868 * @deprecated Connect straight to getResults() events instead
15869 * @param {Object|null} item Item data or null if no item is selected
15870 */
15871
15872 /* Methods */
15873
15874 /**
15875 * Handle query key down events.
15876 *
15877 * @private
15878 * @param {jQuery.Event} e Key down event
15879 */
15880 OO.ui.SearchWidget.prototype.onQueryKeydown = function ( e ) {
15881 var highlightedItem, nextItem,
15882 dir = e.which === OO.ui.Keys.DOWN ? 1 : ( e.which === OO.ui.Keys.UP ? -1 : 0 );
15883
15884 if ( dir ) {
15885 highlightedItem = this.results.getHighlightedItem();
15886 if ( !highlightedItem ) {
15887 highlightedItem = this.results.getSelectedItem();
15888 }
15889 nextItem = this.results.getRelativeSelectableItem( highlightedItem, dir );
15890 this.results.highlightItem( nextItem );
15891 nextItem.scrollElementIntoView();
15892 }
15893 };
15894
15895 /**
15896 * Handle select widget select events.
15897 *
15898 * Clears existing results. Subclasses should repopulate items according to new query.
15899 *
15900 * @private
15901 * @param {string} value New value
15902 */
15903 OO.ui.SearchWidget.prototype.onQueryChange = function () {
15904 // Reset
15905 this.results.clearItems();
15906 };
15907
15908 /**
15909 * Handle select widget enter key events.
15910 *
15911 * Selects highlighted item.
15912 *
15913 * @private
15914 * @param {string} value New value
15915 */
15916 OO.ui.SearchWidget.prototype.onQueryEnter = function () {
15917 // Reset
15918 this.results.selectItem( this.results.getHighlightedItem() );
15919 };
15920
15921 /**
15922 * Handle select widget highlight events.
15923 *
15924 * @private
15925 * @deprecated Connect straight to getResults() events instead
15926 * @param {OO.ui.OptionWidget} item Highlighted item
15927 * @fires highlight
15928 */
15929 OO.ui.SearchWidget.prototype.onResultsHighlight = function ( item ) {
15930 this.emit( 'highlight', item ? item.getData() : null );
15931 };
15932
15933 /**
15934 * Handle select widget select events.
15935 *
15936 * @private
15937 * @deprecated Connect straight to getResults() events instead
15938 * @param {OO.ui.OptionWidget} item Selected item
15939 * @fires select
15940 */
15941 OO.ui.SearchWidget.prototype.onResultsSelect = function ( item ) {
15942 this.emit( 'select', item ? item.getData() : null );
15943 };
15944
15945 /**
15946 * Get the query input.
15947 *
15948 * @return {OO.ui.TextInputWidget} Query input
15949 */
15950 OO.ui.SearchWidget.prototype.getQuery = function () {
15951 return this.query;
15952 };
15953
15954 /**
15955 * Get the search results menu.
15956 *
15957 * @return {OO.ui.SelectWidget} Menu of search results
15958 */
15959 OO.ui.SearchWidget.prototype.getResults = function () {
15960 return this.results;
15961 };
15962
15963 /**
15964 * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
15965 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
15966 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
15967 * menu selects}.
15968 *
15969 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
15970 * information, please see the [OOjs UI documentation on MediaWiki][1].
15971 *
15972 * @example
15973 * // Example of a select widget with three options
15974 * var select = new OO.ui.SelectWidget( {
15975 * items: [
15976 * new OO.ui.OptionWidget( {
15977 * data: 'a',
15978 * label: 'Option One',
15979 * } ),
15980 * new OO.ui.OptionWidget( {
15981 * data: 'b',
15982 * label: 'Option Two',
15983 * } ),
15984 * new OO.ui.OptionWidget( {
15985 * data: 'c',
15986 * label: 'Option Three',
15987 * } )
15988 * ]
15989 * } );
15990 * $( 'body' ).append( select.$element );
15991 *
15992 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
15993 *
15994 * @abstract
15995 * @class
15996 * @extends OO.ui.Widget
15997 * @mixins OO.ui.mixin.GroupElement
15998 *
15999 * @constructor
16000 * @param {Object} [config] Configuration options
16001 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
16002 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
16003 * the [OOjs UI documentation on MediaWiki] [2] for examples.
16004 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
16005 */
16006 OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
16007 // Configuration initialization
16008 config = config || {};
16009
16010 // Parent constructor
16011 OO.ui.SelectWidget.super.call( this, config );
16012
16013 // Mixin constructors
16014 OO.ui.mixin.GroupWidget.call( this, $.extend( {}, config, { $group: this.$element } ) );
16015
16016 // Properties
16017 this.pressed = false;
16018 this.selecting = null;
16019 this.onMouseUpHandler = this.onMouseUp.bind( this );
16020 this.onMouseMoveHandler = this.onMouseMove.bind( this );
16021 this.onKeyDownHandler = this.onKeyDown.bind( this );
16022 this.onKeyPressHandler = this.onKeyPress.bind( this );
16023 this.keyPressBuffer = '';
16024 this.keyPressBufferTimer = null;
16025
16026 // Events
16027 this.connect( this, {
16028 toggle: 'onToggle'
16029 } );
16030 this.$element.on( {
16031 mousedown: this.onMouseDown.bind( this ),
16032 mouseover: this.onMouseOver.bind( this ),
16033 mouseleave: this.onMouseLeave.bind( this )
16034 } );
16035
16036 // Initialization
16037 this.$element
16038 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
16039 .attr( 'role', 'listbox' );
16040 if ( Array.isArray( config.items ) ) {
16041 this.addItems( config.items );
16042 }
16043 };
16044
16045 /* Setup */
16046
16047 OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
16048
16049 // Need to mixin base class as well
16050 OO.mixinClass( OO.ui.SelectWidget, OO.ui.mixin.GroupElement );
16051 OO.mixinClass( OO.ui.SelectWidget, OO.ui.mixin.GroupWidget );
16052
16053 /* Static */
16054 OO.ui.SelectWidget.static.passAllFilter = function () {
16055 return true;
16056 };
16057
16058 /* Events */
16059
16060 /**
16061 * @event highlight
16062 *
16063 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
16064 *
16065 * @param {OO.ui.OptionWidget|null} item Highlighted item
16066 */
16067
16068 /**
16069 * @event press
16070 *
16071 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
16072 * pressed state of an option.
16073 *
16074 * @param {OO.ui.OptionWidget|null} item Pressed item
16075 */
16076
16077 /**
16078 * @event select
16079 *
16080 * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
16081 *
16082 * @param {OO.ui.OptionWidget|null} item Selected item
16083 */
16084
16085 /**
16086 * @event choose
16087 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
16088 * @param {OO.ui.OptionWidget} item Chosen item
16089 */
16090
16091 /**
16092 * @event add
16093 *
16094 * An `add` event is emitted when options are added to the select with the #addItems method.
16095 *
16096 * @param {OO.ui.OptionWidget[]} items Added items
16097 * @param {number} index Index of insertion point
16098 */
16099
16100 /**
16101 * @event remove
16102 *
16103 * A `remove` event is emitted when options are removed from the select with the #clearItems
16104 * or #removeItems methods.
16105 *
16106 * @param {OO.ui.OptionWidget[]} items Removed items
16107 */
16108
16109 /* Methods */
16110
16111 /**
16112 * Handle mouse down events.
16113 *
16114 * @private
16115 * @param {jQuery.Event} e Mouse down event
16116 */
16117 OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
16118 var item;
16119
16120 if ( !this.isDisabled() && e.which === 1 ) {
16121 this.togglePressed( true );
16122 item = this.getTargetItem( e );
16123 if ( item && item.isSelectable() ) {
16124 this.pressItem( item );
16125 this.selecting = item;
16126 this.getElementDocument().addEventListener(
16127 'mouseup',
16128 this.onMouseUpHandler,
16129 true
16130 );
16131 this.getElementDocument().addEventListener(
16132 'mousemove',
16133 this.onMouseMoveHandler,
16134 true
16135 );
16136 }
16137 }
16138 return false;
16139 };
16140
16141 /**
16142 * Handle mouse up events.
16143 *
16144 * @private
16145 * @param {jQuery.Event} e Mouse up event
16146 */
16147 OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
16148 var item;
16149
16150 this.togglePressed( false );
16151 if ( !this.selecting ) {
16152 item = this.getTargetItem( e );
16153 if ( item && item.isSelectable() ) {
16154 this.selecting = item;
16155 }
16156 }
16157 if ( !this.isDisabled() && e.which === 1 && this.selecting ) {
16158 this.pressItem( null );
16159 this.chooseItem( this.selecting );
16160 this.selecting = null;
16161 }
16162
16163 this.getElementDocument().removeEventListener(
16164 'mouseup',
16165 this.onMouseUpHandler,
16166 true
16167 );
16168 this.getElementDocument().removeEventListener(
16169 'mousemove',
16170 this.onMouseMoveHandler,
16171 true
16172 );
16173
16174 return false;
16175 };
16176
16177 /**
16178 * Handle mouse move events.
16179 *
16180 * @private
16181 * @param {jQuery.Event} e Mouse move event
16182 */
16183 OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
16184 var item;
16185
16186 if ( !this.isDisabled() && this.pressed ) {
16187 item = this.getTargetItem( e );
16188 if ( item && item !== this.selecting && item.isSelectable() ) {
16189 this.pressItem( item );
16190 this.selecting = item;
16191 }
16192 }
16193 return false;
16194 };
16195
16196 /**
16197 * Handle mouse over events.
16198 *
16199 * @private
16200 * @param {jQuery.Event} e Mouse over event
16201 */
16202 OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
16203 var item;
16204
16205 if ( !this.isDisabled() ) {
16206 item = this.getTargetItem( e );
16207 this.highlightItem( item && item.isHighlightable() ? item : null );
16208 }
16209 return false;
16210 };
16211
16212 /**
16213 * Handle mouse leave events.
16214 *
16215 * @private
16216 * @param {jQuery.Event} e Mouse over event
16217 */
16218 OO.ui.SelectWidget.prototype.onMouseLeave = function () {
16219 if ( !this.isDisabled() ) {
16220 this.highlightItem( null );
16221 }
16222 return false;
16223 };
16224
16225 /**
16226 * Handle key down events.
16227 *
16228 * @protected
16229 * @param {jQuery.Event} e Key down event
16230 */
16231 OO.ui.SelectWidget.prototype.onKeyDown = function ( e ) {
16232 var nextItem,
16233 handled = false,
16234 currentItem = this.getHighlightedItem() || this.getSelectedItem();
16235
16236 if ( !this.isDisabled() && this.isVisible() ) {
16237 switch ( e.keyCode ) {
16238 case OO.ui.Keys.ENTER:
16239 if ( currentItem && currentItem.constructor.static.highlightable ) {
16240 // Was only highlighted, now let's select it. No-op if already selected.
16241 this.chooseItem( currentItem );
16242 handled = true;
16243 }
16244 break;
16245 case OO.ui.Keys.UP:
16246 case OO.ui.Keys.LEFT:
16247 this.clearKeyPressBuffer();
16248 nextItem = this.getRelativeSelectableItem( currentItem, -1 );
16249 handled = true;
16250 break;
16251 case OO.ui.Keys.DOWN:
16252 case OO.ui.Keys.RIGHT:
16253 this.clearKeyPressBuffer();
16254 nextItem = this.getRelativeSelectableItem( currentItem, 1 );
16255 handled = true;
16256 break;
16257 case OO.ui.Keys.ESCAPE:
16258 case OO.ui.Keys.TAB:
16259 if ( currentItem && currentItem.constructor.static.highlightable ) {
16260 currentItem.setHighlighted( false );
16261 }
16262 this.unbindKeyDownListener();
16263 this.unbindKeyPressListener();
16264 // Don't prevent tabbing away / defocusing
16265 handled = false;
16266 break;
16267 }
16268
16269 if ( nextItem ) {
16270 if ( nextItem.constructor.static.highlightable ) {
16271 this.highlightItem( nextItem );
16272 } else {
16273 this.chooseItem( nextItem );
16274 }
16275 nextItem.scrollElementIntoView();
16276 }
16277
16278 if ( handled ) {
16279 // Can't just return false, because e is not always a jQuery event
16280 e.preventDefault();
16281 e.stopPropagation();
16282 }
16283 }
16284 };
16285
16286 /**
16287 * Bind key down listener.
16288 *
16289 * @protected
16290 */
16291 OO.ui.SelectWidget.prototype.bindKeyDownListener = function () {
16292 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
16293 };
16294
16295 /**
16296 * Unbind key down listener.
16297 *
16298 * @protected
16299 */
16300 OO.ui.SelectWidget.prototype.unbindKeyDownListener = function () {
16301 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
16302 };
16303
16304 /**
16305 * Clear the key-press buffer
16306 *
16307 * @protected
16308 */
16309 OO.ui.SelectWidget.prototype.clearKeyPressBuffer = function () {
16310 if ( this.keyPressBufferTimer ) {
16311 clearTimeout( this.keyPressBufferTimer );
16312 this.keyPressBufferTimer = null;
16313 }
16314 this.keyPressBuffer = '';
16315 };
16316
16317 /**
16318 * Handle key press events.
16319 *
16320 * @protected
16321 * @param {jQuery.Event} e Key press event
16322 */
16323 OO.ui.SelectWidget.prototype.onKeyPress = function ( e ) {
16324 var c, filter, item;
16325
16326 if ( !e.charCode ) {
16327 if ( e.keyCode === OO.ui.Keys.BACKSPACE && this.keyPressBuffer !== '' ) {
16328 this.keyPressBuffer = this.keyPressBuffer.substr( 0, this.keyPressBuffer.length - 1 );
16329 return false;
16330 }
16331 return;
16332 }
16333 if ( String.fromCodePoint ) {
16334 c = String.fromCodePoint( e.charCode );
16335 } else {
16336 c = String.fromCharCode( e.charCode );
16337 }
16338
16339 if ( this.keyPressBufferTimer ) {
16340 clearTimeout( this.keyPressBufferTimer );
16341 }
16342 this.keyPressBufferTimer = setTimeout( this.clearKeyPressBuffer.bind( this ), 1500 );
16343
16344 item = this.getHighlightedItem() || this.getSelectedItem();
16345
16346 if ( this.keyPressBuffer === c ) {
16347 // Common (if weird) special case: typing "xxxx" will cycle through all
16348 // the items beginning with "x".
16349 if ( item ) {
16350 item = this.getRelativeSelectableItem( item, 1 );
16351 }
16352 } else {
16353 this.keyPressBuffer += c;
16354 }
16355
16356 filter = this.getItemMatcher( this.keyPressBuffer );
16357 if ( !item || !filter( item ) ) {
16358 item = this.getRelativeSelectableItem( item, 1, filter );
16359 }
16360 if ( item ) {
16361 if ( item.constructor.static.highlightable ) {
16362 this.highlightItem( item );
16363 } else {
16364 this.chooseItem( item );
16365 }
16366 item.scrollElementIntoView();
16367 }
16368
16369 return false;
16370 };
16371
16372 /**
16373 * Get a matcher for the specific string
16374 *
16375 * @protected
16376 * @param {string} s String to match against items
16377 * @return {Function} function ( OO.ui.OptionItem ) => boolean
16378 */
16379 OO.ui.SelectWidget.prototype.getItemMatcher = function ( s ) {
16380 var re;
16381
16382 if ( s.normalize ) {
16383 s = s.normalize();
16384 }
16385 re = new RegExp( '^\s*' + s.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' ), 'i' );
16386 return function ( item ) {
16387 var l = item.getLabel();
16388 if ( typeof l !== 'string' ) {
16389 l = item.$label.text();
16390 }
16391 if ( l.normalize ) {
16392 l = l.normalize();
16393 }
16394 return re.test( l );
16395 };
16396 };
16397
16398 /**
16399 * Bind key press listener.
16400 *
16401 * @protected
16402 */
16403 OO.ui.SelectWidget.prototype.bindKeyPressListener = function () {
16404 this.getElementWindow().addEventListener( 'keypress', this.onKeyPressHandler, true );
16405 };
16406
16407 /**
16408 * Unbind key down listener.
16409 *
16410 * If you override this, be sure to call this.clearKeyPressBuffer() from your
16411 * implementation.
16412 *
16413 * @protected
16414 */
16415 OO.ui.SelectWidget.prototype.unbindKeyPressListener = function () {
16416 this.getElementWindow().removeEventListener( 'keypress', this.onKeyPressHandler, true );
16417 this.clearKeyPressBuffer();
16418 };
16419
16420 /**
16421 * Visibility change handler
16422 *
16423 * @protected
16424 * @param {boolean} visible
16425 */
16426 OO.ui.SelectWidget.prototype.onToggle = function ( visible ) {
16427 if ( !visible ) {
16428 this.clearKeyPressBuffer();
16429 }
16430 };
16431
16432 /**
16433 * Get the closest item to a jQuery.Event.
16434 *
16435 * @private
16436 * @param {jQuery.Event} e
16437 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
16438 */
16439 OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) {
16440 return $( e.target ).closest( '.oo-ui-optionWidget' ).data( 'oo-ui-optionWidget' ) || null;
16441 };
16442
16443 /**
16444 * Get selected item.
16445 *
16446 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
16447 */
16448 OO.ui.SelectWidget.prototype.getSelectedItem = function () {
16449 var i, len;
16450
16451 for ( i = 0, len = this.items.length; i < len; i++ ) {
16452 if ( this.items[ i ].isSelected() ) {
16453 return this.items[ i ];
16454 }
16455 }
16456 return null;
16457 };
16458
16459 /**
16460 * Get highlighted item.
16461 *
16462 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
16463 */
16464 OO.ui.SelectWidget.prototype.getHighlightedItem = function () {
16465 var i, len;
16466
16467 for ( i = 0, len = this.items.length; i < len; i++ ) {
16468 if ( this.items[ i ].isHighlighted() ) {
16469 return this.items[ i ];
16470 }
16471 }
16472 return null;
16473 };
16474
16475 /**
16476 * Toggle pressed state.
16477 *
16478 * Press is a state that occurs when a user mouses down on an item, but
16479 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
16480 * until the user releases the mouse.
16481 *
16482 * @param {boolean} pressed An option is being pressed
16483 */
16484 OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) {
16485 if ( pressed === undefined ) {
16486 pressed = !this.pressed;
16487 }
16488 if ( pressed !== this.pressed ) {
16489 this.$element
16490 .toggleClass( 'oo-ui-selectWidget-pressed', pressed )
16491 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed );
16492 this.pressed = pressed;
16493 }
16494 };
16495
16496 /**
16497 * Highlight an option. If the `item` param is omitted, no options will be highlighted
16498 * and any existing highlight will be removed. The highlight is mutually exclusive.
16499 *
16500 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
16501 * @fires highlight
16502 * @chainable
16503 */
16504 OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
16505 var i, len, highlighted,
16506 changed = false;
16507
16508 for ( i = 0, len = this.items.length; i < len; i++ ) {
16509 highlighted = this.items[ i ] === item;
16510 if ( this.items[ i ].isHighlighted() !== highlighted ) {
16511 this.items[ i ].setHighlighted( highlighted );
16512 changed = true;
16513 }
16514 }
16515 if ( changed ) {
16516 this.emit( 'highlight', item );
16517 }
16518
16519 return this;
16520 };
16521
16522 /**
16523 * Programmatically select an option by its data. If the `data` parameter is omitted,
16524 * or if the item does not exist, all options will be deselected.
16525 *
16526 * @param {Object|string} [data] Value of the item to select, omit to deselect all
16527 * @fires select
16528 * @chainable
16529 */
16530 OO.ui.SelectWidget.prototype.selectItemByData = function ( data ) {
16531 var itemFromData = this.getItemFromData( data );
16532 if ( data === undefined || !itemFromData ) {
16533 return this.selectItem();
16534 }
16535 return this.selectItem( itemFromData );
16536 };
16537
16538 /**
16539 * Programmatically select an option by its reference. If the `item` parameter is omitted,
16540 * all options will be deselected.
16541 *
16542 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
16543 * @fires select
16544 * @chainable
16545 */
16546 OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
16547 var i, len, selected,
16548 changed = false;
16549
16550 for ( i = 0, len = this.items.length; i < len; i++ ) {
16551 selected = this.items[ i ] === item;
16552 if ( this.items[ i ].isSelected() !== selected ) {
16553 this.items[ i ].setSelected( selected );
16554 changed = true;
16555 }
16556 }
16557 if ( changed ) {
16558 this.emit( 'select', item );
16559 }
16560
16561 return this;
16562 };
16563
16564 /**
16565 * Press an item.
16566 *
16567 * Press is a state that occurs when a user mouses down on an item, but has not
16568 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
16569 * releases the mouse.
16570 *
16571 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
16572 * @fires press
16573 * @chainable
16574 */
16575 OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
16576 var i, len, pressed,
16577 changed = false;
16578
16579 for ( i = 0, len = this.items.length; i < len; i++ ) {
16580 pressed = this.items[ i ] === item;
16581 if ( this.items[ i ].isPressed() !== pressed ) {
16582 this.items[ i ].setPressed( pressed );
16583 changed = true;
16584 }
16585 }
16586 if ( changed ) {
16587 this.emit( 'press', item );
16588 }
16589
16590 return this;
16591 };
16592
16593 /**
16594 * Choose an item.
16595 *
16596 * Note that ‘choose’ should never be modified programmatically. A user can choose
16597 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
16598 * use the #selectItem method.
16599 *
16600 * This method is identical to #selectItem, but may vary in subclasses that take additional action
16601 * when users choose an item with the keyboard or mouse.
16602 *
16603 * @param {OO.ui.OptionWidget} item Item to choose
16604 * @fires choose
16605 * @chainable
16606 */
16607 OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
16608 this.selectItem( item );
16609 this.emit( 'choose', item );
16610
16611 return this;
16612 };
16613
16614 /**
16615 * Get an option by its position relative to the specified item (or to the start of the option array,
16616 * if item is `null`). The direction in which to search through the option array is specified with a
16617 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
16618 * `null` if there are no options in the array.
16619 *
16620 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
16621 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
16622 * @param {Function} filter Only consider items for which this function returns
16623 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
16624 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
16625 */
16626 OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction, filter ) {
16627 var currentIndex, nextIndex, i,
16628 increase = direction > 0 ? 1 : -1,
16629 len = this.items.length;
16630
16631 if ( !$.isFunction( filter ) ) {
16632 filter = OO.ui.SelectWidget.static.passAllFilter;
16633 }
16634
16635 if ( item instanceof OO.ui.OptionWidget ) {
16636 currentIndex = $.inArray( item, this.items );
16637 nextIndex = ( currentIndex + increase + len ) % len;
16638 } else {
16639 // If no item is selected and moving forward, start at the beginning.
16640 // If moving backward, start at the end.
16641 nextIndex = direction > 0 ? 0 : len - 1;
16642 }
16643
16644 for ( i = 0; i < len; i++ ) {
16645 item = this.items[ nextIndex ];
16646 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() && filter( item ) ) {
16647 return item;
16648 }
16649 nextIndex = ( nextIndex + increase + len ) % len;
16650 }
16651 return null;
16652 };
16653
16654 /**
16655 * Get the next selectable item or `null` if there are no selectable items.
16656 * Disabled options and menu-section markers and breaks are not selectable.
16657 *
16658 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
16659 */
16660 OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
16661 var i, len, item;
16662
16663 for ( i = 0, len = this.items.length; i < len; i++ ) {
16664 item = this.items[ i ];
16665 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
16666 return item;
16667 }
16668 }
16669
16670 return null;
16671 };
16672
16673 /**
16674 * Add an array of options to the select. Optionally, an index number can be used to
16675 * specify an insertion point.
16676 *
16677 * @param {OO.ui.OptionWidget[]} items Items to add
16678 * @param {number} [index] Index to insert items after
16679 * @fires add
16680 * @chainable
16681 */
16682 OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
16683 // Mixin method
16684 OO.ui.mixin.GroupWidget.prototype.addItems.call( this, items, index );
16685
16686 // Always provide an index, even if it was omitted
16687 this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index );
16688
16689 return this;
16690 };
16691
16692 /**
16693 * Remove the specified array of options from the select. Options will be detached
16694 * from the DOM, not removed, so they can be reused later. To remove all options from
16695 * the select, you may wish to use the #clearItems method instead.
16696 *
16697 * @param {OO.ui.OptionWidget[]} items Items to remove
16698 * @fires remove
16699 * @chainable
16700 */
16701 OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
16702 var i, len, item;
16703
16704 // Deselect items being removed
16705 for ( i = 0, len = items.length; i < len; i++ ) {
16706 item = items[ i ];
16707 if ( item.isSelected() ) {
16708 this.selectItem( null );
16709 }
16710 }
16711
16712 // Mixin method
16713 OO.ui.mixin.GroupWidget.prototype.removeItems.call( this, items );
16714
16715 this.emit( 'remove', items );
16716
16717 return this;
16718 };
16719
16720 /**
16721 * Clear all options from the select. Options will be detached from the DOM, not removed,
16722 * so that they can be reused later. To remove a subset of options from the select, use
16723 * the #removeItems method.
16724 *
16725 * @fires remove
16726 * @chainable
16727 */
16728 OO.ui.SelectWidget.prototype.clearItems = function () {
16729 var items = this.items.slice();
16730
16731 // Mixin method
16732 OO.ui.mixin.GroupWidget.prototype.clearItems.call( this );
16733
16734 // Clear selection
16735 this.selectItem( null );
16736
16737 this.emit( 'remove', items );
16738
16739 return this;
16740 };
16741
16742 /**
16743 * ButtonSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains
16744 * button options and is used together with
16745 * OO.ui.ButtonOptionWidget. The ButtonSelectWidget provides an interface for
16746 * highlighting, choosing, and selecting mutually exclusive options. Please see
16747 * the [OOjs UI documentation on MediaWiki] [1] for more information.
16748 *
16749 * @example
16750 * // Example: A ButtonSelectWidget that contains three ButtonOptionWidgets
16751 * var option1 = new OO.ui.ButtonOptionWidget( {
16752 * data: 1,
16753 * label: 'Option 1',
16754 * title: 'Button option 1'
16755 * } );
16756 *
16757 * var option2 = new OO.ui.ButtonOptionWidget( {
16758 * data: 2,
16759 * label: 'Option 2',
16760 * title: 'Button option 2'
16761 * } );
16762 *
16763 * var option3 = new OO.ui.ButtonOptionWidget( {
16764 * data: 3,
16765 * label: 'Option 3',
16766 * title: 'Button option 3'
16767 * } );
16768 *
16769 * var buttonSelect=new OO.ui.ButtonSelectWidget( {
16770 * items: [ option1, option2, option3 ]
16771 * } );
16772 * $( 'body' ).append( buttonSelect.$element );
16773 *
16774 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
16775 *
16776 * @class
16777 * @extends OO.ui.SelectWidget
16778 * @mixins OO.ui.mixin.TabIndexedElement
16779 *
16780 * @constructor
16781 * @param {Object} [config] Configuration options
16782 */
16783 OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
16784 // Parent constructor
16785 OO.ui.ButtonSelectWidget.super.call( this, config );
16786
16787 // Mixin constructors
16788 OO.ui.mixin.TabIndexedElement.call( this, config );
16789
16790 // Events
16791 this.$element.on( {
16792 focus: this.bindKeyDownListener.bind( this ),
16793 blur: this.unbindKeyDownListener.bind( this )
16794 } );
16795
16796 // Initialization
16797 this.$element.addClass( 'oo-ui-buttonSelectWidget' );
16798 };
16799
16800 /* Setup */
16801
16802 OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
16803 OO.mixinClass( OO.ui.ButtonSelectWidget, OO.ui.mixin.TabIndexedElement );
16804
16805 /**
16806 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
16807 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
16808 * an interface for adding, removing and selecting options.
16809 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
16810 *
16811 * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
16812 * OO.ui.RadioSelectInputWidget instead.
16813 *
16814 * @example
16815 * // A RadioSelectWidget with RadioOptions.
16816 * var option1 = new OO.ui.RadioOptionWidget( {
16817 * data: 'a',
16818 * label: 'Selected radio option'
16819 * } );
16820 *
16821 * var option2 = new OO.ui.RadioOptionWidget( {
16822 * data: 'b',
16823 * label: 'Unselected radio option'
16824 * } );
16825 *
16826 * var radioSelect=new OO.ui.RadioSelectWidget( {
16827 * items: [ option1, option2 ]
16828 * } );
16829 *
16830 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
16831 * radioSelect.selectItem( option1 );
16832 *
16833 * $( 'body' ).append( radioSelect.$element );
16834 *
16835 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
16836
16837 *
16838 * @class
16839 * @extends OO.ui.SelectWidget
16840 * @mixins OO.ui.mixin.TabIndexedElement
16841 *
16842 * @constructor
16843 * @param {Object} [config] Configuration options
16844 */
16845 OO.ui.RadioSelectWidget = function OoUiRadioSelectWidget( config ) {
16846 // Parent constructor
16847 OO.ui.RadioSelectWidget.super.call( this, config );
16848
16849 // Mixin constructors
16850 OO.ui.mixin.TabIndexedElement.call( this, config );
16851
16852 // Events
16853 this.$element.on( {
16854 focus: this.bindKeyDownListener.bind( this ),
16855 blur: this.unbindKeyDownListener.bind( this )
16856 } );
16857
16858 // Initialization
16859 this.$element.addClass( 'oo-ui-radioSelectWidget' );
16860 };
16861
16862 /* Setup */
16863
16864 OO.inheritClass( OO.ui.RadioSelectWidget, OO.ui.SelectWidget );
16865 OO.mixinClass( OO.ui.RadioSelectWidget, OO.ui.mixin.TabIndexedElement );
16866
16867 /**
16868 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
16869 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
16870 * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxWidget ComboBoxWidget},
16871 * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
16872 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
16873 * and customized to be opened, closed, and displayed as needed.
16874 *
16875 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
16876 * mouse outside the menu.
16877 *
16878 * Menus also have support for keyboard interaction:
16879 *
16880 * - Enter/Return key: choose and select a menu option
16881 * - Up-arrow key: highlight the previous menu option
16882 * - Down-arrow key: highlight the next menu option
16883 * - Esc key: hide the menu
16884 *
16885 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
16886 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
16887 *
16888 * @class
16889 * @extends OO.ui.SelectWidget
16890 * @mixins OO.ui.mixin.ClippableElement
16891 *
16892 * @constructor
16893 * @param {Object} [config] Configuration options
16894 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
16895 * the text the user types. This config is used by {@link OO.ui.ComboBoxWidget ComboBoxWidget}
16896 * and {@link OO.ui.mixin.LookupElement LookupElement}
16897 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu’s active state. If the user clicks the mouse
16898 * anywhere on the page outside of this widget, the menu is hidden.
16899 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
16900 */
16901 OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) {
16902 // Configuration initialization
16903 config = config || {};
16904
16905 // Parent constructor
16906 OO.ui.MenuSelectWidget.super.call( this, config );
16907
16908 // Mixin constructors
16909 OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
16910
16911 // Properties
16912 this.newItems = null;
16913 this.autoHide = config.autoHide === undefined || !!config.autoHide;
16914 this.$input = config.input ? config.input.$input : null;
16915 this.$widget = config.widget ? config.widget.$element : null;
16916 this.onDocumentMouseDownHandler = this.onDocumentMouseDown.bind( this );
16917
16918 // Initialization
16919 this.$element
16920 .addClass( 'oo-ui-menuSelectWidget' )
16921 .attr( 'role', 'menu' );
16922
16923 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
16924 // that reference properties not initialized at that time of parent class construction
16925 // TODO: Find a better way to handle post-constructor setup
16926 this.visible = false;
16927 this.$element.addClass( 'oo-ui-element-hidden' );
16928 };
16929
16930 /* Setup */
16931
16932 OO.inheritClass( OO.ui.MenuSelectWidget, OO.ui.SelectWidget );
16933 OO.mixinClass( OO.ui.MenuSelectWidget, OO.ui.mixin.ClippableElement );
16934
16935 /* Methods */
16936
16937 /**
16938 * Handles document mouse down events.
16939 *
16940 * @protected
16941 * @param {jQuery.Event} e Key down event
16942 */
16943 OO.ui.MenuSelectWidget.prototype.onDocumentMouseDown = function ( e ) {
16944 if (
16945 !OO.ui.contains( this.$element[ 0 ], e.target, true ) &&
16946 ( !this.$widget || !OO.ui.contains( this.$widget[ 0 ], e.target, true ) )
16947 ) {
16948 this.toggle( false );
16949 }
16950 };
16951
16952 /**
16953 * @inheritdoc
16954 */
16955 OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) {
16956 var currentItem = this.getHighlightedItem() || this.getSelectedItem();
16957
16958 if ( !this.isDisabled() && this.isVisible() ) {
16959 switch ( e.keyCode ) {
16960 case OO.ui.Keys.LEFT:
16961 case OO.ui.Keys.RIGHT:
16962 // Do nothing if a text field is associated, arrow keys will be handled natively
16963 if ( !this.$input ) {
16964 OO.ui.MenuSelectWidget.super.prototype.onKeyDown.call( this, e );
16965 }
16966 break;
16967 case OO.ui.Keys.ESCAPE:
16968 case OO.ui.Keys.TAB:
16969 if ( currentItem ) {
16970 currentItem.setHighlighted( false );
16971 }
16972 this.toggle( false );
16973 // Don't prevent tabbing away, prevent defocusing
16974 if ( e.keyCode === OO.ui.Keys.ESCAPE ) {
16975 e.preventDefault();
16976 e.stopPropagation();
16977 }
16978 break;
16979 default:
16980 OO.ui.MenuSelectWidget.super.prototype.onKeyDown.call( this, e );
16981 return;
16982 }
16983 }
16984 };
16985
16986 /**
16987 * @inheritdoc
16988 */
16989 OO.ui.MenuSelectWidget.prototype.bindKeyDownListener = function () {
16990 if ( this.$input ) {
16991 this.$input.on( 'keydown', this.onKeyDownHandler );
16992 } else {
16993 OO.ui.MenuSelectWidget.super.prototype.bindKeyDownListener.call( this );
16994 }
16995 };
16996
16997 /**
16998 * @inheritdoc
16999 */
17000 OO.ui.MenuSelectWidget.prototype.unbindKeyDownListener = function () {
17001 if ( this.$input ) {
17002 this.$input.off( 'keydown', this.onKeyDownHandler );
17003 } else {
17004 OO.ui.MenuSelectWidget.super.prototype.unbindKeyDownListener.call( this );
17005 }
17006 };
17007
17008 /**
17009 * @inheritdoc
17010 */
17011 OO.ui.MenuSelectWidget.prototype.bindKeyPressListener = function () {
17012 if ( !this.$input ) {
17013 OO.ui.MenuSelectWidget.super.prototype.bindKeyPressListener.call( this );
17014 }
17015 };
17016
17017 /**
17018 * @inheritdoc
17019 */
17020 OO.ui.MenuSelectWidget.prototype.unbindKeyPressListener = function () {
17021 if ( this.$input ) {
17022 this.clearKeyPressBuffer();
17023 } else {
17024 OO.ui.MenuSelectWidget.super.prototype.unbindKeyPressListener.call( this );
17025 }
17026 };
17027
17028 /**
17029 * Choose an item.
17030 *
17031 * When a user chooses an item, the menu is closed.
17032 *
17033 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
17034 * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
17035 * @param {OO.ui.OptionWidget} item Item to choose
17036 * @chainable
17037 */
17038 OO.ui.MenuSelectWidget.prototype.chooseItem = function ( item ) {
17039 OO.ui.MenuSelectWidget.super.prototype.chooseItem.call( this, item );
17040 this.toggle( false );
17041 return this;
17042 };
17043
17044 /**
17045 * @inheritdoc
17046 */
17047 OO.ui.MenuSelectWidget.prototype.addItems = function ( items, index ) {
17048 var i, len, item;
17049
17050 // Parent method
17051 OO.ui.MenuSelectWidget.super.prototype.addItems.call( this, items, index );
17052
17053 // Auto-initialize
17054 if ( !this.newItems ) {
17055 this.newItems = [];
17056 }
17057
17058 for ( i = 0, len = items.length; i < len; i++ ) {
17059 item = items[ i ];
17060 if ( this.isVisible() ) {
17061 // Defer fitting label until item has been attached
17062 item.fitLabel();
17063 } else {
17064 this.newItems.push( item );
17065 }
17066 }
17067
17068 // Reevaluate clipping
17069 this.clip();
17070
17071 return this;
17072 };
17073
17074 /**
17075 * @inheritdoc
17076 */
17077 OO.ui.MenuSelectWidget.prototype.removeItems = function ( items ) {
17078 // Parent method
17079 OO.ui.MenuSelectWidget.super.prototype.removeItems.call( this, items );
17080
17081 // Reevaluate clipping
17082 this.clip();
17083
17084 return this;
17085 };
17086
17087 /**
17088 * @inheritdoc
17089 */
17090 OO.ui.MenuSelectWidget.prototype.clearItems = function () {
17091 // Parent method
17092 OO.ui.MenuSelectWidget.super.prototype.clearItems.call( this );
17093
17094 // Reevaluate clipping
17095 this.clip();
17096
17097 return this;
17098 };
17099
17100 /**
17101 * @inheritdoc
17102 */
17103 OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
17104 visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
17105
17106 var i, len,
17107 change = visible !== this.isVisible();
17108
17109 // Parent method
17110 OO.ui.MenuSelectWidget.super.prototype.toggle.call( this, visible );
17111
17112 if ( change ) {
17113 if ( visible ) {
17114 this.bindKeyDownListener();
17115 this.bindKeyPressListener();
17116
17117 if ( this.newItems && this.newItems.length ) {
17118 for ( i = 0, len = this.newItems.length; i < len; i++ ) {
17119 this.newItems[ i ].fitLabel();
17120 }
17121 this.newItems = null;
17122 }
17123 this.toggleClipping( true );
17124
17125 // Auto-hide
17126 if ( this.autoHide ) {
17127 this.getElementDocument().addEventListener(
17128 'mousedown', this.onDocumentMouseDownHandler, true
17129 );
17130 }
17131 } else {
17132 this.unbindKeyDownListener();
17133 this.unbindKeyPressListener();
17134 this.getElementDocument().removeEventListener(
17135 'mousedown', this.onDocumentMouseDownHandler, true
17136 );
17137 this.toggleClipping( false );
17138 }
17139 }
17140
17141 return this;
17142 };
17143
17144 /**
17145 * TextInputMenuSelectWidget is a menu that is specially designed to be positioned beneath
17146 * a {@link OO.ui.TextInputWidget text input} field. The menu's position is automatically
17147 * calculated and maintained when the menu is toggled or the window is resized.
17148 * See OO.ui.ComboBoxWidget for an example of a widget that uses this class.
17149 *
17150 * @class
17151 * @extends OO.ui.MenuSelectWidget
17152 *
17153 * @constructor
17154 * @param {OO.ui.TextInputWidget} inputWidget Text input widget to provide menu for
17155 * @param {Object} [config] Configuration options
17156 * @cfg {jQuery} [$container=input.$element] Element to render menu under
17157 */
17158 OO.ui.TextInputMenuSelectWidget = function OoUiTextInputMenuSelectWidget( inputWidget, config ) {
17159 // Allow passing positional parameters inside the config object
17160 if ( OO.isPlainObject( inputWidget ) && config === undefined ) {
17161 config = inputWidget;
17162 inputWidget = config.inputWidget;
17163 }
17164
17165 // Configuration initialization
17166 config = config || {};
17167
17168 // Parent constructor
17169 OO.ui.TextInputMenuSelectWidget.super.call( this, config );
17170
17171 // Properties
17172 this.inputWidget = inputWidget;
17173 this.$container = config.$container || this.inputWidget.$element;
17174 this.onWindowResizeHandler = this.onWindowResize.bind( this );
17175
17176 // Initialization
17177 this.$element.addClass( 'oo-ui-textInputMenuSelectWidget' );
17178 };
17179
17180 /* Setup */
17181
17182 OO.inheritClass( OO.ui.TextInputMenuSelectWidget, OO.ui.MenuSelectWidget );
17183
17184 /* Methods */
17185
17186 /**
17187 * Handle window resize event.
17188 *
17189 * @private
17190 * @param {jQuery.Event} e Window resize event
17191 */
17192 OO.ui.TextInputMenuSelectWidget.prototype.onWindowResize = function () {
17193 this.position();
17194 };
17195
17196 /**
17197 * @inheritdoc
17198 */
17199 OO.ui.TextInputMenuSelectWidget.prototype.toggle = function ( visible ) {
17200 visible = visible === undefined ? !this.isVisible() : !!visible;
17201
17202 var change = visible !== this.isVisible();
17203
17204 if ( change && visible ) {
17205 // Make sure the width is set before the parent method runs.
17206 // After this we have to call this.position(); again to actually
17207 // position ourselves correctly.
17208 this.position();
17209 }
17210
17211 // Parent method
17212 OO.ui.TextInputMenuSelectWidget.super.prototype.toggle.call( this, visible );
17213
17214 if ( change ) {
17215 if ( this.isVisible() ) {
17216 this.position();
17217 $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
17218 } else {
17219 $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
17220 }
17221 }
17222
17223 return this;
17224 };
17225
17226 /**
17227 * Position the menu.
17228 *
17229 * @private
17230 * @chainable
17231 */
17232 OO.ui.TextInputMenuSelectWidget.prototype.position = function () {
17233 var $container = this.$container,
17234 pos = OO.ui.Element.static.getRelativePosition( $container, this.$element.offsetParent() );
17235
17236 // Position under input
17237 pos.top += $container.height();
17238 this.$element.css( pos );
17239
17240 // Set width
17241 this.setIdealSize( $container.width() );
17242 // We updated the position, so re-evaluate the clipping state
17243 this.clip();
17244
17245 return this;
17246 };
17247
17248 /**
17249 * OutlineSelectWidget is a structured list that contains {@link OO.ui.OutlineOptionWidget outline options}
17250 * A set of controls can be provided with an {@link OO.ui.OutlineControlsWidget outline controls} widget.
17251 *
17252 * ####Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.####
17253 *
17254 * @class
17255 * @extends OO.ui.SelectWidget
17256 * @mixins OO.ui.mixin.TabIndexedElement
17257 *
17258 * @constructor
17259 * @param {Object} [config] Configuration options
17260 */
17261 OO.ui.OutlineSelectWidget = function OoUiOutlineSelectWidget( config ) {
17262 // Parent constructor
17263 OO.ui.OutlineSelectWidget.super.call( this, config );
17264
17265 // Mixin constructors
17266 OO.ui.mixin.TabIndexedElement.call( this, config );
17267
17268 // Events
17269 this.$element.on( {
17270 focus: this.bindKeyDownListener.bind( this ),
17271 blur: this.unbindKeyDownListener.bind( this )
17272 } );
17273
17274 // Initialization
17275 this.$element.addClass( 'oo-ui-outlineSelectWidget' );
17276 };
17277
17278 /* Setup */
17279
17280 OO.inheritClass( OO.ui.OutlineSelectWidget, OO.ui.SelectWidget );
17281 OO.mixinClass( OO.ui.OutlineSelectWidget, OO.ui.mixin.TabIndexedElement );
17282
17283 /**
17284 * TabSelectWidget is a list that contains {@link OO.ui.TabOptionWidget tab options}
17285 *
17286 * ####Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}.####
17287 *
17288 * @class
17289 * @extends OO.ui.SelectWidget
17290 * @mixins OO.ui.mixin.TabIndexedElement
17291 *
17292 * @constructor
17293 * @param {Object} [config] Configuration options
17294 */
17295 OO.ui.TabSelectWidget = function OoUiTabSelectWidget( config ) {
17296 // Parent constructor
17297 OO.ui.TabSelectWidget.super.call( this, config );
17298
17299 // Mixin constructors
17300 OO.ui.mixin.TabIndexedElement.call( this, config );
17301
17302 // Events
17303 this.$element.on( {
17304 focus: this.bindKeyDownListener.bind( this ),
17305 blur: this.unbindKeyDownListener.bind( this )
17306 } );
17307
17308 // Initialization
17309 this.$element.addClass( 'oo-ui-tabSelectWidget' );
17310 };
17311
17312 /* Setup */
17313
17314 OO.inheritClass( OO.ui.TabSelectWidget, OO.ui.SelectWidget );
17315 OO.mixinClass( OO.ui.TabSelectWidget, OO.ui.mixin.TabIndexedElement );
17316
17317 /**
17318 * NumberInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
17319 * can be entered manually) and two {@link OO.ui.ButtonWidget button widgets}
17320 * (to adjust the value in increments) to allow the user to enter a number.
17321 *
17322 * @example
17323 * // Example: A NumberInputWidget.
17324 * var numberInput = new OO.ui.NumberInputWidget( {
17325 * label: 'NumberInputWidget',
17326 * input: { value: 5, min: 1, max: 10 }
17327 * } );
17328 * $( 'body' ).append( numberInput.$element );
17329 *
17330 * @class
17331 * @extends OO.ui.Widget
17332 *
17333 * @constructor
17334 * @param {Object} [config] Configuration options
17335 * @cfg {Object} [input] Configuration options to pass to the {@link OO.ui.TextInputWidget text input widget}.
17336 * @cfg {Object} [minusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget decrementing button widget}.
17337 * @cfg {Object} [plusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget incrementing button widget}.
17338 * @cfg {boolean} [isInteger=false] Whether the field accepts only integer values.
17339 * @cfg {number} [min=-Infinity] Minimum allowed value
17340 * @cfg {number} [max=Infinity] Maximum allowed value
17341 * @cfg {number} [step=1] Delta when using the buttons or up/down arrow keys
17342 * @cfg {number|null} [pageStep] Delta when using the page-up/page-down keys. Defaults to 10 times #step.
17343 */
17344 OO.ui.NumberInputWidget = function OoUiNumberInputWidget( config ) {
17345 // Configuration initialization
17346 config = $.extend( {
17347 isInteger: false,
17348 min: -Infinity,
17349 max: Infinity,
17350 step: 1,
17351 pageStep: null
17352 }, config );
17353
17354 // Parent constructor
17355 OO.ui.NumberInputWidget.super.call( this, config );
17356
17357 // Properties
17358 this.input = new OO.ui.TextInputWidget( $.extend(
17359 {
17360 disabled: this.isDisabled()
17361 },
17362 config.input
17363 ) );
17364 this.minusButton = new OO.ui.ButtonWidget( $.extend(
17365 {
17366 disabled: this.isDisabled(),
17367 tabIndex: -1
17368 },
17369 config.minusButton,
17370 {
17371 classes: [ 'oo-ui-numberInputWidget-minusButton' ],
17372 label: '−'
17373 }
17374 ) );
17375 this.plusButton = new OO.ui.ButtonWidget( $.extend(
17376 {
17377 disabled: this.isDisabled(),
17378 tabIndex: -1
17379 },
17380 config.plusButton,
17381 {
17382 classes: [ 'oo-ui-numberInputWidget-plusButton' ],
17383 label: '+'
17384 }
17385 ) );
17386
17387 // Events
17388 this.input.connect( this, {
17389 change: this.emit.bind( this, 'change' ),
17390 enter: this.emit.bind( this, 'enter' )
17391 } );
17392 this.input.$input.on( {
17393 keydown: this.onKeyDown.bind( this ),
17394 'wheel mousewheel DOMMouseScroll': this.onWheel.bind( this )
17395 } );
17396 this.plusButton.connect( this, {
17397 click: [ 'onButtonClick', +1 ]
17398 } );
17399 this.minusButton.connect( this, {
17400 click: [ 'onButtonClick', -1 ]
17401 } );
17402
17403 // Initialization
17404 this.setIsInteger( !!config.isInteger );
17405 this.setRange( config.min, config.max );
17406 this.setStep( config.step, config.pageStep );
17407
17408 this.$field = $( '<div>' ).addClass( 'oo-ui-numberInputWidget-field' )
17409 .append(
17410 this.minusButton.$element,
17411 this.input.$element,
17412 this.plusButton.$element
17413 );
17414 this.$element.addClass( 'oo-ui-numberInputWidget' ).append( this.$field );
17415 this.input.setValidation( this.validateNumber.bind( this ) );
17416 };
17417
17418 /* Setup */
17419
17420 OO.inheritClass( OO.ui.NumberInputWidget, OO.ui.Widget );
17421
17422 /* Events */
17423
17424 /**
17425 * A `change` event is emitted when the value of the input changes.
17426 *
17427 * @event change
17428 */
17429
17430 /**
17431 * An `enter` event is emitted when the user presses 'enter' inside the text box.
17432 *
17433 * @event enter
17434 */
17435
17436 /* Methods */
17437
17438 /**
17439 * Set whether only integers are allowed
17440 * @param {boolean} flag
17441 */
17442 OO.ui.NumberInputWidget.prototype.setIsInteger = function ( flag ) {
17443 this.isInteger = !!flag;
17444 this.input.setValidityFlag();
17445 };
17446
17447 /**
17448 * Get whether only integers are allowed
17449 * @return {boolean} Flag value
17450 */
17451 OO.ui.NumberInputWidget.prototype.getIsInteger = function () {
17452 return this.isInteger;
17453 };
17454
17455 /**
17456 * Set the range of allowed values
17457 * @param {number} min Minimum allowed value
17458 * @param {number} max Maximum allowed value
17459 */
17460 OO.ui.NumberInputWidget.prototype.setRange = function ( min, max ) {
17461 if ( min > max ) {
17462 throw new Error( 'Minimum (' + min + ') must not be greater than maximum (' + max + ')' );
17463 }
17464 this.min = min;
17465 this.max = max;
17466 this.input.setValidityFlag();
17467 };
17468
17469 /**
17470 * Get the current range
17471 * @return {number[]} Minimum and maximum values
17472 */
17473 OO.ui.NumberInputWidget.prototype.getRange = function () {
17474 return [ this.min, this.max ];
17475 };
17476
17477 /**
17478 * Set the stepping deltas
17479 * @param {number} step Normal step
17480 * @param {number|null} pageStep Page step. If null, 10 * step will be used.
17481 */
17482 OO.ui.NumberInputWidget.prototype.setStep = function ( step, pageStep ) {
17483 if ( step <= 0 ) {
17484 throw new Error( 'Step value must be positive' );
17485 }
17486 if ( pageStep === null ) {
17487 pageStep = step * 10;
17488 } else if ( pageStep <= 0 ) {
17489 throw new Error( 'Page step value must be positive' );
17490 }
17491 this.step = step;
17492 this.pageStep = pageStep;
17493 };
17494
17495 /**
17496 * Get the current stepping values
17497 * @return {number[]} Step and page step
17498 */
17499 OO.ui.NumberInputWidget.prototype.getStep = function () {
17500 return [ this.step, this.pageStep ];
17501 };
17502
17503 /**
17504 * Get the current value of the widget
17505 * @return {string}
17506 */
17507 OO.ui.NumberInputWidget.prototype.getValue = function () {
17508 return this.input.getValue();
17509 };
17510
17511 /**
17512 * Get the current value of the widget as a number
17513 * @return {number} May be NaN, or an invalid number
17514 */
17515 OO.ui.NumberInputWidget.prototype.getNumericValue = function () {
17516 return +this.input.getValue();
17517 };
17518
17519 /**
17520 * Set the value of the widget
17521 * @param {string} value Invalid values are allowed
17522 */
17523 OO.ui.NumberInputWidget.prototype.setValue = function ( value ) {
17524 this.input.setValue( value );
17525 };
17526
17527 /**
17528 * Adjust the value of the widget
17529 * @param {number} delta Adjustment amount
17530 */
17531 OO.ui.NumberInputWidget.prototype.adjustValue = function ( delta ) {
17532 var n, v = this.getNumericValue();
17533
17534 delta = +delta;
17535 if ( isNaN( delta ) || !isFinite( delta ) ) {
17536 throw new Error( 'Delta must be a finite number' );
17537 }
17538
17539 if ( isNaN( v ) ) {
17540 n = 0;
17541 } else {
17542 n = v + delta;
17543 n = Math.max( Math.min( n, this.max ), this.min );
17544 if ( this.isInteger ) {
17545 n = Math.round( n );
17546 }
17547 }
17548
17549 if ( n !== v ) {
17550 this.setValue( n );
17551 }
17552 };
17553
17554 /**
17555 * Validate input
17556 * @private
17557 * @param {string} value Field value
17558 * @return {boolean}
17559 */
17560 OO.ui.NumberInputWidget.prototype.validateNumber = function ( value ) {
17561 var n = +value;
17562 if ( isNaN( n ) || !isFinite( n ) ) {
17563 return false;
17564 }
17565
17566 /*jshint bitwise: false */
17567 if ( this.isInteger && ( n | 0 ) !== n ) {
17568 return false;
17569 }
17570 /*jshint bitwise: true */
17571
17572 if ( n < this.min || n > this.max ) {
17573 return false;
17574 }
17575
17576 return true;
17577 };
17578
17579 /**
17580 * Handle mouse click events.
17581 *
17582 * @private
17583 * @param {number} dir +1 or -1
17584 */
17585 OO.ui.NumberInputWidget.prototype.onButtonClick = function ( dir ) {
17586 this.adjustValue( dir * this.step );
17587 };
17588
17589 /**
17590 * Handle mouse wheel events.
17591 *
17592 * @private
17593 * @param {jQuery.Event} event
17594 */
17595 OO.ui.NumberInputWidget.prototype.onWheel = function ( event ) {
17596 var delta = 0;
17597
17598 // Standard 'wheel' event
17599 if ( event.originalEvent.deltaMode !== undefined ) {
17600 this.sawWheelEvent = true;
17601 }
17602 if ( event.originalEvent.deltaY ) {
17603 delta = -event.originalEvent.deltaY;
17604 } else if ( event.originalEvent.deltaX ) {
17605 delta = event.originalEvent.deltaX;
17606 }
17607
17608 // Non-standard events
17609 if ( !this.sawWheelEvent ) {
17610 if ( event.originalEvent.wheelDeltaX ) {
17611 delta = -event.originalEvent.wheelDeltaX;
17612 } else if ( event.originalEvent.wheelDeltaY ) {
17613 delta = event.originalEvent.wheelDeltaY;
17614 } else if ( event.originalEvent.wheelDelta ) {
17615 delta = event.originalEvent.wheelDelta;
17616 } else if ( event.originalEvent.detail ) {
17617 delta = -event.originalEvent.detail;
17618 }
17619 }
17620
17621 if ( delta ) {
17622 this.adjustValue( Math.sign( delta ) * this.step );
17623 }
17624
17625 return false;
17626 };
17627
17628 /**
17629 * Handle key down events.
17630 *
17631 *
17632 * @private
17633 * @param {jQuery.Event} e Key down event
17634 */
17635 OO.ui.NumberInputWidget.prototype.onKeyDown = function ( e ) {
17636 if ( !this.isDisabled() ) {
17637 switch ( e.which ) {
17638 case OO.ui.Keys.UP:
17639 this.adjustValue( this.step );
17640 return false;
17641 case OO.ui.Keys.DOWN:
17642 this.adjustValue( -this.step );
17643 return false;
17644 case OO.ui.Keys.PAGEUP:
17645 this.adjustValue( this.pageStep );
17646 return false;
17647 case OO.ui.Keys.PAGEDOWN:
17648 this.adjustValue( -this.pageStep );
17649 return false;
17650 }
17651 }
17652 };
17653
17654 /**
17655 * @inheritdoc
17656 */
17657 OO.ui.NumberInputWidget.prototype.setDisabled = function ( disabled ) {
17658 // Parent method
17659 OO.ui.NumberInputWidget.super.prototype.setDisabled.call( this, disabled );
17660
17661 if ( this.input ) {
17662 this.input.setDisabled( this.isDisabled() );
17663 }
17664 if ( this.minusButton ) {
17665 this.minusButton.setDisabled( this.isDisabled() );
17666 }
17667 if ( this.plusButton ) {
17668 this.plusButton.setDisabled( this.isDisabled() );
17669 }
17670
17671 return this;
17672 };
17673
17674 /**
17675 * ToggleSwitches are switches that slide on and off. Their state is represented by a Boolean
17676 * value (`true` for ‘on’, and `false` otherwise, the default). The ‘off’ state is represented
17677 * visually by a slider in the leftmost position.
17678 *
17679 * @example
17680 * // Toggle switches in the 'off' and 'on' position.
17681 * var toggleSwitch1 = new OO.ui.ToggleSwitchWidget();
17682 * var toggleSwitch2 = new OO.ui.ToggleSwitchWidget( {
17683 * value: true
17684 * } );
17685 *
17686 * // Create a FieldsetLayout to layout and label switches
17687 * var fieldset = new OO.ui.FieldsetLayout( {
17688 * label: 'Toggle switches'
17689 * } );
17690 * fieldset.addItems( [
17691 * new OO.ui.FieldLayout( toggleSwitch1, { label: 'Off', align: 'top' } ),
17692 * new OO.ui.FieldLayout( toggleSwitch2, { label: 'On', align: 'top' } )
17693 * ] );
17694 * $( 'body' ).append( fieldset.$element );
17695 *
17696 * @class
17697 * @extends OO.ui.ToggleWidget
17698 * @mixins OO.ui.mixin.TabIndexedElement
17699 *
17700 * @constructor
17701 * @param {Object} [config] Configuration options
17702 * @cfg {boolean} [value=false] The toggle switch’s initial on/off state.
17703 * By default, the toggle switch is in the 'off' position.
17704 */
17705 OO.ui.ToggleSwitchWidget = function OoUiToggleSwitchWidget( config ) {
17706 // Parent constructor
17707 OO.ui.ToggleSwitchWidget.super.call( this, config );
17708
17709 // Mixin constructors
17710 OO.ui.mixin.TabIndexedElement.call( this, config );
17711
17712 // Properties
17713 this.dragging = false;
17714 this.dragStart = null;
17715 this.sliding = false;
17716 this.$glow = $( '<span>' );
17717 this.$grip = $( '<span>' );
17718
17719 // Events
17720 this.$element.on( {
17721 click: this.onClick.bind( this ),
17722 keypress: this.onKeyPress.bind( this )
17723 } );
17724
17725 // Initialization
17726 this.$glow.addClass( 'oo-ui-toggleSwitchWidget-glow' );
17727 this.$grip.addClass( 'oo-ui-toggleSwitchWidget-grip' );
17728 this.$element
17729 .addClass( 'oo-ui-toggleSwitchWidget' )
17730 .attr( 'role', 'checkbox' )
17731 .append( this.$glow, this.$grip );
17732 };
17733
17734 /* Setup */
17735
17736 OO.inheritClass( OO.ui.ToggleSwitchWidget, OO.ui.ToggleWidget );
17737 OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.mixin.TabIndexedElement );
17738
17739 /* Methods */
17740
17741 /**
17742 * Handle mouse click events.
17743 *
17744 * @private
17745 * @param {jQuery.Event} e Mouse click event
17746 */
17747 OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
17748 if ( !this.isDisabled() && e.which === 1 ) {
17749 this.setValue( !this.value );
17750 }
17751 return false;
17752 };
17753
17754 /**
17755 * Handle key press events.
17756 *
17757 * @private
17758 * @param {jQuery.Event} e Key press event
17759 */
17760 OO.ui.ToggleSwitchWidget.prototype.onKeyPress = function ( e ) {
17761 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
17762 this.setValue( !this.value );
17763 return false;
17764 }
17765 };
17766
17767 /*!
17768 * Deprecated aliases for classes in the `OO.ui.mixin` namespace.
17769 */
17770
17771 /**
17772 * @inheritdoc OO.ui.mixin.ButtonElement
17773 * @deprecated Use {@link OO.ui.mixin.ButtonElement} instead.
17774 */
17775 OO.ui.ButtonElement = OO.ui.mixin.ButtonElement;
17776
17777 /**
17778 * @inheritdoc OO.ui.mixin.ClippableElement
17779 * @deprecated Use {@link OO.ui.mixin.ClippableElement} instead.
17780 */
17781 OO.ui.ClippableElement = OO.ui.mixin.ClippableElement;
17782
17783 /**
17784 * @inheritdoc OO.ui.mixin.DraggableElement
17785 * @deprecated Use {@link OO.ui.mixin.DraggableElement} instead.
17786 */
17787 OO.ui.DraggableElement = OO.ui.mixin.DraggableElement;
17788
17789 /**
17790 * @inheritdoc OO.ui.mixin.DraggableGroupElement
17791 * @deprecated Use {@link OO.ui.mixin.DraggableGroupElement} instead.
17792 */
17793 OO.ui.DraggableGroupElement = OO.ui.mixin.DraggableGroupElement;
17794
17795 /**
17796 * @inheritdoc OO.ui.mixin.FlaggedElement
17797 * @deprecated Use {@link OO.ui.mixin.FlaggedElement} instead.
17798 */
17799 OO.ui.FlaggedElement = OO.ui.mixin.FlaggedElement;
17800
17801 /**
17802 * @inheritdoc OO.ui.mixin.GroupElement
17803 * @deprecated Use {@link OO.ui.mixin.GroupElement} instead.
17804 */
17805 OO.ui.GroupElement = OO.ui.mixin.GroupElement;
17806
17807 /**
17808 * @inheritdoc OO.ui.mixin.GroupWidget
17809 * @deprecated Use {@link OO.ui.mixin.GroupWidget} instead.
17810 */
17811 OO.ui.GroupWidget = OO.ui.mixin.GroupWidget;
17812
17813 /**
17814 * @inheritdoc OO.ui.mixin.IconElement
17815 * @deprecated Use {@link OO.ui.mixin.IconElement} instead.
17816 */
17817 OO.ui.IconElement = OO.ui.mixin.IconElement;
17818
17819 /**
17820 * @inheritdoc OO.ui.mixin.IndicatorElement
17821 * @deprecated Use {@link OO.ui.mixin.IndicatorElement} instead.
17822 */
17823 OO.ui.IndicatorElement = OO.ui.mixin.IndicatorElement;
17824
17825 /**
17826 * @inheritdoc OO.ui.mixin.ItemWidget
17827 * @deprecated Use {@link OO.ui.mixin.ItemWidget} instead.
17828 */
17829 OO.ui.ItemWidget = OO.ui.mixin.ItemWidget;
17830
17831 /**
17832 * @inheritdoc OO.ui.mixin.LabelElement
17833 * @deprecated Use {@link OO.ui.mixin.LabelElement} instead.
17834 */
17835 OO.ui.LabelElement = OO.ui.mixin.LabelElement;
17836
17837 /**
17838 * @inheritdoc OO.ui.mixin.LookupElement
17839 * @deprecated Use {@link OO.ui.mixin.LookupElement} instead.
17840 */
17841 OO.ui.LookupElement = OO.ui.mixin.LookupElement;
17842
17843 /**
17844 * @inheritdoc OO.ui.mixin.PendingElement
17845 * @deprecated Use {@link OO.ui.mixin.PendingElement} instead.
17846 */
17847 OO.ui.PendingElement = OO.ui.mixin.PendingElement;
17848
17849 /**
17850 * @inheritdoc OO.ui.mixin.PopupElement
17851 * @deprecated Use {@link OO.ui.mixin.PopupElement} instead.
17852 */
17853 OO.ui.PopupElement = OO.ui.mixin.PopupElement;
17854
17855 /**
17856 * @inheritdoc OO.ui.mixin.TabIndexedElement
17857 * @deprecated Use {@link OO.ui.mixin.TabIndexedElement} instead.
17858 */
17859 OO.ui.TabIndexedElement = OO.ui.mixin.TabIndexedElement;
17860
17861 /**
17862 * @inheritdoc OO.ui.mixin.TitledElement
17863 * @deprecated Use {@link OO.ui.mixin.TitledElement} instead.
17864 */
17865 OO.ui.TitledElement = OO.ui.mixin.TitledElement;
17866
17867 }( OO ) );