Merge "Make ForeignTitle properties private"
[lhc/web/wiklou.git] / resources / lib / oojs-ui / oojs-ui.js
1 /*!
2 * OOjs UI v0.15.1
3 * https://www.mediawiki.org/wiki/OOjs_UI
4 *
5 * Copyright 2011–2016 OOjs UI Team and other contributors.
6 * Released under the MIT license
7 * http://oojs.mit-license.org
8 *
9 * Date: 2016-01-26T21:14:23Z
10 */
11 ( function ( OO ) {
12
13 'use strict';
14
15 /**
16 * Namespace for all classes, static methods and static properties.
17 *
18 * @class
19 * @singleton
20 */
21 OO.ui = {};
22
23 OO.ui.bind = $.proxy;
24
25 /**
26 * @property {Object}
27 */
28 OO.ui.Keys = {
29 UNDEFINED: 0,
30 BACKSPACE: 8,
31 DELETE: 46,
32 LEFT: 37,
33 RIGHT: 39,
34 UP: 38,
35 DOWN: 40,
36 ENTER: 13,
37 END: 35,
38 HOME: 36,
39 TAB: 9,
40 PAGEUP: 33,
41 PAGEDOWN: 34,
42 ESCAPE: 27,
43 SHIFT: 16,
44 SPACE: 32
45 };
46
47 /**
48 * Constants for MouseEvent.which
49 *
50 * @property {Object}
51 */
52 OO.ui.MouseButtons = {
53 LEFT: 1,
54 MIDDLE: 2,
55 RIGHT: 3
56 };
57
58 /**
59 * @property {Number}
60 */
61 OO.ui.elementId = 0;
62
63 /**
64 * Generate a unique ID for element
65 *
66 * @return {String} [id]
67 */
68 OO.ui.generateElementId = function () {
69 OO.ui.elementId += 1;
70 return 'oojsui-' + OO.ui.elementId;
71 };
72
73 /**
74 * Check if an element is focusable.
75 * Inspired from :focusable in jQueryUI v1.11.4 - 2015-04-14
76 *
77 * @param {jQuery} element Element to test
78 * @return {boolean}
79 */
80 OO.ui.isFocusableElement = function ( $element ) {
81 var nodeName,
82 element = $element[ 0 ];
83
84 // Anything disabled is not focusable
85 if ( element.disabled ) {
86 return false;
87 }
88
89 // Check if the element is visible
90 if ( !(
91 // This is quicker than calling $element.is( ':visible' )
92 $.expr.filters.visible( element ) &&
93 // Check that all parents are visible
94 !$element.parents().addBack().filter( function () {
95 return $.css( this, 'visibility' ) === 'hidden';
96 } ).length
97 ) ) {
98 return false;
99 }
100
101 // Check if the element is ContentEditable, which is the string 'true'
102 if ( element.contentEditable === 'true' ) {
103 return true;
104 }
105
106 // Anything with a non-negative numeric tabIndex is focusable.
107 // Use .prop to avoid browser bugs
108 if ( $element.prop( 'tabIndex' ) >= 0 ) {
109 return true;
110 }
111
112 // Some element types are naturally focusable
113 // (indexOf is much faster than regex in Chrome and about the
114 // same in FF: https://jsperf.com/regex-vs-indexof-array2)
115 nodeName = element.nodeName.toLowerCase();
116 if ( [ 'input', 'select', 'textarea', 'button', 'object' ].indexOf( nodeName ) !== -1 ) {
117 return true;
118 }
119
120 // Links and areas are focusable if they have an href
121 if ( ( nodeName === 'a' || nodeName === 'area' ) && $element.attr( 'href' ) !== undefined ) {
122 return true;
123 }
124
125 return false;
126 };
127
128 /**
129 * Find a focusable child
130 *
131 * @param {jQuery} $container Container to search in
132 * @param {boolean} [backwards] Search backwards
133 * @return {jQuery} Focusable child, an empty jQuery object if none found
134 */
135 OO.ui.findFocusable = function ( $container, backwards ) {
136 var $focusable = $( [] ),
137 // $focusableCandidates is a superset of things that
138 // could get matched by isFocusableElement
139 $focusableCandidates = $container
140 .find( 'input, select, textarea, button, object, a, area, [contenteditable], [tabindex]' );
141
142 if ( backwards ) {
143 $focusableCandidates = Array.prototype.reverse.call( $focusableCandidates );
144 }
145
146 $focusableCandidates.each( function () {
147 var $this = $( this );
148 if ( OO.ui.isFocusableElement( $this ) ) {
149 $focusable = $this;
150 return false;
151 }
152 } );
153 return $focusable;
154 };
155
156 /**
157 * Get the user's language and any fallback languages.
158 *
159 * These language codes are used to localize user interface elements in the user's language.
160 *
161 * In environments that provide a localization system, this function should be overridden to
162 * return the user's language(s). The default implementation returns English (en) only.
163 *
164 * @return {string[]} Language codes, in descending order of priority
165 */
166 OO.ui.getUserLanguages = function () {
167 return [ 'en' ];
168 };
169
170 /**
171 * Get a value in an object keyed by language code.
172 *
173 * @param {Object.<string,Mixed>} obj Object keyed by language code
174 * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
175 * @param {string} [fallback] Fallback code, used if no matching language can be found
176 * @return {Mixed} Local value
177 */
178 OO.ui.getLocalValue = function ( obj, lang, fallback ) {
179 var i, len, langs;
180
181 // Requested language
182 if ( obj[ lang ] ) {
183 return obj[ lang ];
184 }
185 // Known user language
186 langs = OO.ui.getUserLanguages();
187 for ( i = 0, len = langs.length; i < len; i++ ) {
188 lang = langs[ i ];
189 if ( obj[ lang ] ) {
190 return obj[ lang ];
191 }
192 }
193 // Fallback language
194 if ( obj[ fallback ] ) {
195 return obj[ fallback ];
196 }
197 // First existing language
198 for ( lang in obj ) {
199 return obj[ lang ];
200 }
201
202 return undefined;
203 };
204
205 /**
206 * Check if a node is contained within another node
207 *
208 * Similar to jQuery#contains except a list of containers can be supplied
209 * and a boolean argument allows you to include the container in the match list
210 *
211 * @param {HTMLElement|HTMLElement[]} containers Container node(s) to search in
212 * @param {HTMLElement} contained Node to find
213 * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match, otherwise only match descendants
214 * @return {boolean} The node is in the list of target nodes
215 */
216 OO.ui.contains = function ( containers, contained, matchContainers ) {
217 var i;
218 if ( !Array.isArray( containers ) ) {
219 containers = [ containers ];
220 }
221 for ( i = containers.length - 1; i >= 0; i-- ) {
222 if ( ( matchContainers && contained === containers[ i ] ) || $.contains( containers[ i ], contained ) ) {
223 return true;
224 }
225 }
226 return false;
227 };
228
229 /**
230 * Return a function, that, as long as it continues to be invoked, will not
231 * be triggered. The function will be called after it stops being called for
232 * N milliseconds. If `immediate` is passed, trigger the function on the
233 * leading edge, instead of the trailing.
234 *
235 * Ported from: http://underscorejs.org/underscore.js
236 *
237 * @param {Function} func
238 * @param {number} wait
239 * @param {boolean} immediate
240 * @return {Function}
241 */
242 OO.ui.debounce = function ( func, wait, immediate ) {
243 var timeout;
244 return function () {
245 var context = this,
246 args = arguments,
247 later = function () {
248 timeout = null;
249 if ( !immediate ) {
250 func.apply( context, args );
251 }
252 };
253 if ( immediate && !timeout ) {
254 func.apply( context, args );
255 }
256 clearTimeout( timeout );
257 timeout = setTimeout( later, wait );
258 };
259 };
260
261 /**
262 * Proxy for `node.addEventListener( eventName, handler, true )`.
263 *
264 * @param {HTMLElement} node
265 * @param {string} eventName
266 * @param {Function} handler
267 * @deprecated
268 */
269 OO.ui.addCaptureEventListener = function ( node, eventName, handler ) {
270 node.addEventListener( eventName, handler, true );
271 };
272
273 /**
274 * Proxy for `node.removeEventListener( eventName, handler, true )`.
275 *
276 * @param {HTMLElement} node
277 * @param {string} eventName
278 * @param {Function} handler
279 * @deprecated
280 */
281 OO.ui.removeCaptureEventListener = function ( node, eventName, handler ) {
282 node.removeEventListener( eventName, handler, true );
283 };
284
285 /**
286 * Reconstitute a JavaScript object corresponding to a widget created by
287 * the PHP implementation.
288 *
289 * This is an alias for `OO.ui.Element.static.infuse()`.
290 *
291 * @param {string|HTMLElement|jQuery} idOrNode
292 * A DOM id (if a string) or node for the widget to infuse.
293 * @return {OO.ui.Element}
294 * The `OO.ui.Element` corresponding to this (infusable) document node.
295 */
296 OO.ui.infuse = function ( idOrNode ) {
297 return OO.ui.Element.static.infuse( idOrNode );
298 };
299
300 ( function () {
301 /**
302 * Message store for the default implementation of OO.ui.msg
303 *
304 * Environments that provide a localization system should not use this, but should override
305 * OO.ui.msg altogether.
306 *
307 * @private
308 */
309 var messages = {
310 // Tool tip for a button that moves items in a list down one place
311 'ooui-outline-control-move-down': 'Move item down',
312 // Tool tip for a button that moves items in a list up one place
313 'ooui-outline-control-move-up': 'Move item up',
314 // Tool tip for a button that removes items from a list
315 'ooui-outline-control-remove': 'Remove item',
316 // Label for the toolbar group that contains a list of all other available tools
317 'ooui-toolbar-more': 'More',
318 // Label for the fake tool that expands the full list of tools in a toolbar group
319 'ooui-toolgroup-expand': 'More',
320 // Label for the fake tool that collapses the full list of tools in a toolbar group
321 'ooui-toolgroup-collapse': 'Fewer',
322 // Default label for the accept button of a confirmation dialog
323 'ooui-dialog-message-accept': 'OK',
324 // Default label for the reject button of a confirmation dialog
325 'ooui-dialog-message-reject': 'Cancel',
326 // Title for process dialog error description
327 'ooui-dialog-process-error': 'Something went wrong',
328 // Label for process dialog dismiss error button, visible when describing errors
329 'ooui-dialog-process-dismiss': 'Dismiss',
330 // Label for process dialog retry action button, visible when describing only recoverable errors
331 'ooui-dialog-process-retry': 'Try again',
332 // Label for process dialog retry action button, visible when describing only warnings
333 'ooui-dialog-process-continue': 'Continue',
334 // Label for the file selection widget's select file button
335 'ooui-selectfile-button-select': 'Select a file',
336 // Label for the file selection widget if file selection is not supported
337 'ooui-selectfile-not-supported': 'File selection is not supported',
338 // Label for the file selection widget when no file is currently selected
339 'ooui-selectfile-placeholder': 'No file is selected',
340 // Label for the file selection widget's drop target
341 'ooui-selectfile-dragdrop-placeholder': 'Drop file here'
342 };
343
344 /**
345 * Get a localized message.
346 *
347 * In environments that provide a localization system, this function should be overridden to
348 * return the message translated in the user's language. The default implementation always returns
349 * English messages.
350 *
351 * After the message key, message parameters may optionally be passed. In the default implementation,
352 * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
353 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
354 * they support unnamed, ordered message parameters.
355 *
356 * @param {string} key Message key
357 * @param {Mixed...} [params] Message parameters
358 * @return {string} Translated message with parameters substituted
359 */
360 OO.ui.msg = function ( key ) {
361 var message = messages[ key ],
362 params = Array.prototype.slice.call( arguments, 1 );
363 if ( typeof message === 'string' ) {
364 // Perform $1 substitution
365 message = message.replace( /\$(\d+)/g, function ( unused, n ) {
366 var i = parseInt( n, 10 );
367 return params[ i - 1 ] !== undefined ? params[ i - 1 ] : '$' + n;
368 } );
369 } else {
370 // Return placeholder if message not found
371 message = '[' + key + ']';
372 }
373 return message;
374 };
375 } )();
376
377 /**
378 * Package a message and arguments for deferred resolution.
379 *
380 * Use this when you are statically specifying a message and the message may not yet be present.
381 *
382 * @param {string} key Message key
383 * @param {Mixed...} [params] Message parameters
384 * @return {Function} Function that returns the resolved message when executed
385 */
386 OO.ui.deferMsg = function () {
387 var args = arguments;
388 return function () {
389 return OO.ui.msg.apply( OO.ui, args );
390 };
391 };
392
393 /**
394 * Resolve a message.
395 *
396 * If the message is a function it will be executed, otherwise it will pass through directly.
397 *
398 * @param {Function|string} msg Deferred message, or message text
399 * @return {string} Resolved message
400 */
401 OO.ui.resolveMsg = function ( msg ) {
402 if ( $.isFunction( msg ) ) {
403 return msg();
404 }
405 return msg;
406 };
407
408 /**
409 * @param {string} url
410 * @return {boolean}
411 */
412 OO.ui.isSafeUrl = function ( url ) {
413 // Keep this function in sync with php/Tag.php
414 var i, protocolWhitelist;
415
416 function stringStartsWith( haystack, needle ) {
417 return haystack.substr( 0, needle.length ) === needle;
418 }
419
420 protocolWhitelist = [
421 'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs',
422 'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh',
423 'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp'
424 ];
425
426 if ( url === '' ) {
427 return true;
428 }
429
430 for ( i = 0; i < protocolWhitelist.length; i++ ) {
431 if ( stringStartsWith( url, protocolWhitelist[ i ] + ':' ) ) {
432 return true;
433 }
434 }
435
436 // This matches '//' too
437 if ( stringStartsWith( url, '/' ) || stringStartsWith( url, './' ) ) {
438 return true;
439 }
440 if ( stringStartsWith( url, '?' ) || stringStartsWith( url, '#' ) ) {
441 return true;
442 }
443
444 return false;
445 };
446
447 /**
448 * Lazy-initialize and return a global OO.ui.WindowManager instance, used by OO.ui.alert and
449 * OO.ui.confirm.
450 *
451 * @private
452 * @return {OO.ui.WindowManager}
453 */
454 OO.ui.getWindowManager = function () {
455 if ( !OO.ui.windowManager ) {
456 OO.ui.windowManager = new OO.ui.WindowManager();
457 $( 'body' ).append( OO.ui.windowManager.$element );
458 OO.ui.windowManager.addWindows( {
459 messageDialog: new OO.ui.MessageDialog()
460 } );
461 }
462 return OO.ui.windowManager;
463 };
464
465 /**
466 * Display a quick modal alert dialog, using a OO.ui.MessageDialog. While the dialog is open, the
467 * rest of the page will be dimmed out and the user won't be able to interact with it. The dialog
468 * has only one action button, labelled "OK", clicking it will simply close the dialog.
469 *
470 * A window manager is created automatically when this function is called for the first time.
471 *
472 * @example
473 * OO.ui.alert( 'Something happened!' ).done( function () {
474 * console.log( 'User closed the dialog.' );
475 * } );
476 *
477 * @param {jQuery|string} text Message text to display
478 * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
479 * @return {jQuery.Promise} Promise resolved when the user closes the dialog
480 */
481 OO.ui.alert = function ( text, options ) {
482 return OO.ui.getWindowManager().openWindow( 'messageDialog', $.extend( {
483 message: text,
484 verbose: true,
485 actions: [ OO.ui.MessageDialog.static.actions[ 0 ] ]
486 }, options ) ).then( function ( opened ) {
487 return opened.then( function ( closing ) {
488 return closing.then( function () {
489 return $.Deferred().resolve();
490 } );
491 } );
492 } );
493 };
494
495 /**
496 * Display a quick modal confirmation dialog, using a OO.ui.MessageDialog. While the dialog is open,
497 * the rest of the page will be dimmed out and the user won't be able to interact with it. The
498 * dialog has two action buttons, one to confirm an operation (labelled "OK") and one to cancel it
499 * (labelled "Cancel").
500 *
501 * A window manager is created automatically when this function is called for the first time.
502 *
503 * @example
504 * OO.ui.confirm( 'Are you sure?' ).done( function ( confirmed ) {
505 * if ( confirmed ) {
506 * console.log( 'User clicked "OK"!' );
507 * } else {
508 * console.log( 'User clicked "Cancel" or closed the dialog.' );
509 * }
510 * } );
511 *
512 * @param {jQuery|string} text Message text to display
513 * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
514 * @return {jQuery.Promise} Promise resolved when the user closes the dialog. If the user chose to
515 * confirm, the promise will resolve to boolean `true`; otherwise, it will resolve to boolean
516 * `false`.
517 */
518 OO.ui.confirm = function ( text, options ) {
519 return OO.ui.getWindowManager().openWindow( 'messageDialog', $.extend( {
520 message: text,
521 verbose: true
522 }, options ) ).then( function ( opened ) {
523 return opened.then( function ( closing ) {
524 return closing.then( function ( data ) {
525 return $.Deferred().resolve( !!( data && data.action === 'accept' ) );
526 } );
527 } );
528 } );
529 };
530
531 /*!
532 * Mixin namespace.
533 */
534
535 /**
536 * Namespace for OOjs UI mixins.
537 *
538 * Mixins are named according to the type of object they are intended to
539 * be mixed in to. For example, OO.ui.mixin.GroupElement is intended to be
540 * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
541 * is intended to be mixed in to an instance of OO.ui.Widget.
542 *
543 * @class
544 * @singleton
545 */
546 OO.ui.mixin = {};
547
548 /**
549 * PendingElement is a mixin that is used to create elements that notify users that something is happening
550 * and that they should wait before proceeding. The pending state is visually represented with a pending
551 * texture that appears in the head of a pending {@link OO.ui.ProcessDialog process dialog} or in the input
552 * field of a {@link OO.ui.TextInputWidget text input widget}.
553 *
554 * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked as pending, but only when
555 * used in {@link OO.ui.MessageDialog message dialogs}. The behavior is not currently supported for action widgets used
556 * in process dialogs.
557 *
558 * @example
559 * function MessageDialog( config ) {
560 * MessageDialog.parent.call( this, config );
561 * }
562 * OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
563 *
564 * MessageDialog.static.actions = [
565 * { action: 'save', label: 'Done', flags: 'primary' },
566 * { label: 'Cancel', flags: 'safe' }
567 * ];
568 *
569 * MessageDialog.prototype.initialize = function () {
570 * MessageDialog.parent.prototype.initialize.apply( this, arguments );
571 * this.content = new OO.ui.PanelLayout( { $: this.$, padded: true } );
572 * 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>' );
573 * this.$body.append( this.content.$element );
574 * };
575 * MessageDialog.prototype.getBodyHeight = function () {
576 * return 100;
577 * }
578 * MessageDialog.prototype.getActionProcess = function ( action ) {
579 * var dialog = this;
580 * if ( action === 'save' ) {
581 * dialog.getActions().get({actions: 'save'})[0].pushPending();
582 * return new OO.ui.Process()
583 * .next( 1000 )
584 * .next( function () {
585 * dialog.getActions().get({actions: 'save'})[0].popPending();
586 * } );
587 * }
588 * return MessageDialog.parent.prototype.getActionProcess.call( this, action );
589 * };
590 *
591 * var windowManager = new OO.ui.WindowManager();
592 * $( 'body' ).append( windowManager.$element );
593 *
594 * var dialog = new MessageDialog();
595 * windowManager.addWindows( [ dialog ] );
596 * windowManager.openWindow( dialog );
597 *
598 * @abstract
599 * @class
600 *
601 * @constructor
602 * @param {Object} [config] Configuration options
603 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
604 */
605 OO.ui.mixin.PendingElement = function OoUiMixinPendingElement( config ) {
606 // Configuration initialization
607 config = config || {};
608
609 // Properties
610 this.pending = 0;
611 this.$pending = null;
612
613 // Initialisation
614 this.setPendingElement( config.$pending || this.$element );
615 };
616
617 /* Setup */
618
619 OO.initClass( OO.ui.mixin.PendingElement );
620
621 /* Methods */
622
623 /**
624 * Set the pending element (and clean up any existing one).
625 *
626 * @param {jQuery} $pending The element to set to pending.
627 */
628 OO.ui.mixin.PendingElement.prototype.setPendingElement = function ( $pending ) {
629 if ( this.$pending ) {
630 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
631 }
632
633 this.$pending = $pending;
634 if ( this.pending > 0 ) {
635 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
636 }
637 };
638
639 /**
640 * Check if an element is pending.
641 *
642 * @return {boolean} Element is pending
643 */
644 OO.ui.mixin.PendingElement.prototype.isPending = function () {
645 return !!this.pending;
646 };
647
648 /**
649 * Increase the pending counter. The pending state will remain active until the counter is zero
650 * (i.e., the number of calls to #pushPending and #popPending is the same).
651 *
652 * @chainable
653 */
654 OO.ui.mixin.PendingElement.prototype.pushPending = function () {
655 if ( this.pending === 0 ) {
656 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
657 this.updateThemeClasses();
658 }
659 this.pending++;
660
661 return this;
662 };
663
664 /**
665 * Decrease the pending counter. The pending state will remain active until the counter is zero
666 * (i.e., the number of calls to #pushPending and #popPending is the same).
667 *
668 * @chainable
669 */
670 OO.ui.mixin.PendingElement.prototype.popPending = function () {
671 if ( this.pending === 1 ) {
672 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
673 this.updateThemeClasses();
674 }
675 this.pending = Math.max( 0, this.pending - 1 );
676
677 return this;
678 };
679
680 /**
681 * ActionSets manage the behavior of the {@link OO.ui.ActionWidget action widgets} that comprise them.
682 * Actions can be made available for specific contexts (modes) and circumstances
683 * (abilities). Action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
684 *
685 * ActionSets contain two types of actions:
686 *
687 * - 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.
688 * - Other: Other actions include all non-special visible actions.
689 *
690 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
691 *
692 * @example
693 * // Example: An action set used in a process dialog
694 * function MyProcessDialog( config ) {
695 * MyProcessDialog.parent.call( this, config );
696 * }
697 * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
698 * MyProcessDialog.static.title = 'An action set in a process dialog';
699 * // An action set that uses modes ('edit' and 'help' mode, in this example).
700 * MyProcessDialog.static.actions = [
701 * { action: 'continue', modes: 'edit', label: 'Continue', flags: [ 'primary', 'constructive' ] },
702 * { action: 'help', modes: 'edit', label: 'Help' },
703 * { modes: 'edit', label: 'Cancel', flags: 'safe' },
704 * { action: 'back', modes: 'help', label: 'Back', flags: 'safe' }
705 * ];
706 *
707 * MyProcessDialog.prototype.initialize = function () {
708 * MyProcessDialog.parent.prototype.initialize.apply( this, arguments );
709 * this.panel1 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
710 * 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>' );
711 * this.panel2 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
712 * 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>' );
713 * this.stackLayout = new OO.ui.StackLayout( {
714 * items: [ this.panel1, this.panel2 ]
715 * } );
716 * this.$body.append( this.stackLayout.$element );
717 * };
718 * MyProcessDialog.prototype.getSetupProcess = function ( data ) {
719 * return MyProcessDialog.parent.prototype.getSetupProcess.call( this, data )
720 * .next( function () {
721 * this.actions.setMode( 'edit' );
722 * }, this );
723 * };
724 * MyProcessDialog.prototype.getActionProcess = function ( action ) {
725 * if ( action === 'help' ) {
726 * this.actions.setMode( 'help' );
727 * this.stackLayout.setItem( this.panel2 );
728 * } else if ( action === 'back' ) {
729 * this.actions.setMode( 'edit' );
730 * this.stackLayout.setItem( this.panel1 );
731 * } else if ( action === 'continue' ) {
732 * var dialog = this;
733 * return new OO.ui.Process( function () {
734 * dialog.close();
735 * } );
736 * }
737 * return MyProcessDialog.parent.prototype.getActionProcess.call( this, action );
738 * };
739 * MyProcessDialog.prototype.getBodyHeight = function () {
740 * return this.panel1.$element.outerHeight( true );
741 * };
742 * var windowManager = new OO.ui.WindowManager();
743 * $( 'body' ).append( windowManager.$element );
744 * var dialog = new MyProcessDialog( {
745 * size: 'medium'
746 * } );
747 * windowManager.addWindows( [ dialog ] );
748 * windowManager.openWindow( dialog );
749 *
750 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
751 *
752 * @abstract
753 * @class
754 * @mixins OO.EventEmitter
755 *
756 * @constructor
757 * @param {Object} [config] Configuration options
758 */
759 OO.ui.ActionSet = function OoUiActionSet( config ) {
760 // Configuration initialization
761 config = config || {};
762
763 // Mixin constructors
764 OO.EventEmitter.call( this );
765
766 // Properties
767 this.list = [];
768 this.categories = {
769 actions: 'getAction',
770 flags: 'getFlags',
771 modes: 'getModes'
772 };
773 this.categorized = {};
774 this.special = {};
775 this.others = [];
776 this.organized = false;
777 this.changing = false;
778 this.changed = false;
779 };
780
781 /* Setup */
782
783 OO.mixinClass( OO.ui.ActionSet, OO.EventEmitter );
784
785 /* Static Properties */
786
787 /**
788 * Symbolic name of the flags used to identify special actions. Special actions are displayed in the
789 * header of a {@link OO.ui.ProcessDialog process dialog}.
790 * See the [OOjs UI documentation on MediaWiki][2] for more information and examples.
791 *
792 * [2]:https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
793 *
794 * @abstract
795 * @static
796 * @inheritable
797 * @property {string}
798 */
799 OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ];
800
801 /* Events */
802
803 /**
804 * @event click
805 *
806 * A 'click' event is emitted when an action is clicked.
807 *
808 * @param {OO.ui.ActionWidget} action Action that was clicked
809 */
810
811 /**
812 * @event resize
813 *
814 * A 'resize' event is emitted when an action widget is resized.
815 *
816 * @param {OO.ui.ActionWidget} action Action that was resized
817 */
818
819 /**
820 * @event add
821 *
822 * An 'add' event is emitted when actions are {@link #method-add added} to the action set.
823 *
824 * @param {OO.ui.ActionWidget[]} added Actions added
825 */
826
827 /**
828 * @event remove
829 *
830 * A 'remove' event is emitted when actions are {@link #method-remove removed}
831 * or {@link #clear cleared}.
832 *
833 * @param {OO.ui.ActionWidget[]} added Actions removed
834 */
835
836 /**
837 * @event change
838 *
839 * A 'change' event is emitted when actions are {@link #method-add added}, {@link #clear cleared},
840 * or {@link #method-remove removed} from the action set or when the {@link #setMode mode} is changed.
841 *
842 */
843
844 /* Methods */
845
846 /**
847 * Handle action change events.
848 *
849 * @private
850 * @fires change
851 */
852 OO.ui.ActionSet.prototype.onActionChange = function () {
853 this.organized = false;
854 if ( this.changing ) {
855 this.changed = true;
856 } else {
857 this.emit( 'change' );
858 }
859 };
860
861 /**
862 * Check if an action is one of the special actions.
863 *
864 * @param {OO.ui.ActionWidget} action Action to check
865 * @return {boolean} Action is special
866 */
867 OO.ui.ActionSet.prototype.isSpecial = function ( action ) {
868 var flag;
869
870 for ( flag in this.special ) {
871 if ( action === this.special[ flag ] ) {
872 return true;
873 }
874 }
875
876 return false;
877 };
878
879 /**
880 * Get action widgets based on the specified filter: ‘actions’, ‘flags’, ‘modes’, ‘visible’,
881 * or ‘disabled’.
882 *
883 * @param {Object} [filters] Filters to use, omit to get all actions
884 * @param {string|string[]} [filters.actions] Actions that action widgets must have
885 * @param {string|string[]} [filters.flags] Flags that action widgets must have (e.g., 'safe')
886 * @param {string|string[]} [filters.modes] Modes that action widgets must have
887 * @param {boolean} [filters.visible] Action widgets must be visible
888 * @param {boolean} [filters.disabled] Action widgets must be disabled
889 * @return {OO.ui.ActionWidget[]} Action widgets matching all criteria
890 */
891 OO.ui.ActionSet.prototype.get = function ( filters ) {
892 var i, len, list, category, actions, index, match, matches;
893
894 if ( filters ) {
895 this.organize();
896
897 // Collect category candidates
898 matches = [];
899 for ( category in this.categorized ) {
900 list = filters[ category ];
901 if ( list ) {
902 if ( !Array.isArray( list ) ) {
903 list = [ list ];
904 }
905 for ( i = 0, len = list.length; i < len; i++ ) {
906 actions = this.categorized[ category ][ list[ i ] ];
907 if ( Array.isArray( actions ) ) {
908 matches.push.apply( matches, actions );
909 }
910 }
911 }
912 }
913 // Remove by boolean filters
914 for ( i = 0, len = matches.length; i < len; i++ ) {
915 match = matches[ i ];
916 if (
917 ( filters.visible !== undefined && match.isVisible() !== filters.visible ) ||
918 ( filters.disabled !== undefined && match.isDisabled() !== filters.disabled )
919 ) {
920 matches.splice( i, 1 );
921 len--;
922 i--;
923 }
924 }
925 // Remove duplicates
926 for ( i = 0, len = matches.length; i < len; i++ ) {
927 match = matches[ i ];
928 index = matches.lastIndexOf( match );
929 while ( index !== i ) {
930 matches.splice( index, 1 );
931 len--;
932 index = matches.lastIndexOf( match );
933 }
934 }
935 return matches;
936 }
937 return this.list.slice();
938 };
939
940 /**
941 * Get 'special' actions.
942 *
943 * Special actions are the first visible action widgets with special flags, such as 'safe' and 'primary'.
944 * Special flags can be configured in subclasses by changing the static #specialFlags property.
945 *
946 * @return {OO.ui.ActionWidget[]|null} 'Special' action widgets.
947 */
948 OO.ui.ActionSet.prototype.getSpecial = function () {
949 this.organize();
950 return $.extend( {}, this.special );
951 };
952
953 /**
954 * Get 'other' actions.
955 *
956 * Other actions include all non-special visible action widgets.
957 *
958 * @return {OO.ui.ActionWidget[]} 'Other' action widgets
959 */
960 OO.ui.ActionSet.prototype.getOthers = function () {
961 this.organize();
962 return this.others.slice();
963 };
964
965 /**
966 * Set the mode (e.g., ‘edit’ or ‘view’). Only {@link OO.ui.ActionWidget#modes actions} configured
967 * to be available in the specified mode will be made visible. All other actions will be hidden.
968 *
969 * @param {string} mode The mode. Only actions configured to be available in the specified
970 * mode will be made visible.
971 * @chainable
972 * @fires toggle
973 * @fires change
974 */
975 OO.ui.ActionSet.prototype.setMode = function ( mode ) {
976 var i, len, action;
977
978 this.changing = true;
979 for ( i = 0, len = this.list.length; i < len; i++ ) {
980 action = this.list[ i ];
981 action.toggle( action.hasMode( mode ) );
982 }
983
984 this.organized = false;
985 this.changing = false;
986 this.emit( 'change' );
987
988 return this;
989 };
990
991 /**
992 * Set the abilities of the specified actions.
993 *
994 * Action widgets that are configured with the specified actions will be enabled
995 * or disabled based on the boolean values specified in the `actions`
996 * parameter.
997 *
998 * @param {Object.<string,boolean>} actions A list keyed by action name with boolean
999 * values that indicate whether or not the action should be enabled.
1000 * @chainable
1001 */
1002 OO.ui.ActionSet.prototype.setAbilities = function ( actions ) {
1003 var i, len, action, item;
1004
1005 for ( i = 0, len = this.list.length; i < len; i++ ) {
1006 item = this.list[ i ];
1007 action = item.getAction();
1008 if ( actions[ action ] !== undefined ) {
1009 item.setDisabled( !actions[ action ] );
1010 }
1011 }
1012
1013 return this;
1014 };
1015
1016 /**
1017 * Executes a function once per action.
1018 *
1019 * When making changes to multiple actions, use this method instead of iterating over the actions
1020 * manually to defer emitting a #change event until after all actions have been changed.
1021 *
1022 * @param {Object|null} actions Filters to use to determine which actions to iterate over; see #get
1023 * @param {Function} callback Callback to run for each action; callback is invoked with three
1024 * arguments: the action, the action's index, the list of actions being iterated over
1025 * @chainable
1026 */
1027 OO.ui.ActionSet.prototype.forEach = function ( filter, callback ) {
1028 this.changed = false;
1029 this.changing = true;
1030 this.get( filter ).forEach( callback );
1031 this.changing = false;
1032 if ( this.changed ) {
1033 this.emit( 'change' );
1034 }
1035
1036 return this;
1037 };
1038
1039 /**
1040 * Add action widgets to the action set.
1041 *
1042 * @param {OO.ui.ActionWidget[]} actions Action widgets to add
1043 * @chainable
1044 * @fires add
1045 * @fires change
1046 */
1047 OO.ui.ActionSet.prototype.add = function ( actions ) {
1048 var i, len, action;
1049
1050 this.changing = true;
1051 for ( i = 0, len = actions.length; i < len; i++ ) {
1052 action = actions[ i ];
1053 action.connect( this, {
1054 click: [ 'emit', 'click', action ],
1055 resize: [ 'emit', 'resize', action ],
1056 toggle: [ 'onActionChange' ]
1057 } );
1058 this.list.push( action );
1059 }
1060 this.organized = false;
1061 this.emit( 'add', actions );
1062 this.changing = false;
1063 this.emit( 'change' );
1064
1065 return this;
1066 };
1067
1068 /**
1069 * Remove action widgets from the set.
1070 *
1071 * To remove all actions, you may wish to use the #clear method instead.
1072 *
1073 * @param {OO.ui.ActionWidget[]} actions Action widgets to remove
1074 * @chainable
1075 * @fires remove
1076 * @fires change
1077 */
1078 OO.ui.ActionSet.prototype.remove = function ( actions ) {
1079 var i, len, index, action;
1080
1081 this.changing = true;
1082 for ( i = 0, len = actions.length; i < len; i++ ) {
1083 action = actions[ i ];
1084 index = this.list.indexOf( action );
1085 if ( index !== -1 ) {
1086 action.disconnect( this );
1087 this.list.splice( index, 1 );
1088 }
1089 }
1090 this.organized = false;
1091 this.emit( 'remove', actions );
1092 this.changing = false;
1093 this.emit( 'change' );
1094
1095 return this;
1096 };
1097
1098 /**
1099 * Remove all action widets from the set.
1100 *
1101 * To remove only specified actions, use the {@link #method-remove remove} method instead.
1102 *
1103 * @chainable
1104 * @fires remove
1105 * @fires change
1106 */
1107 OO.ui.ActionSet.prototype.clear = function () {
1108 var i, len, action,
1109 removed = this.list.slice();
1110
1111 this.changing = true;
1112 for ( i = 0, len = this.list.length; i < len; i++ ) {
1113 action = this.list[ i ];
1114 action.disconnect( this );
1115 }
1116
1117 this.list = [];
1118
1119 this.organized = false;
1120 this.emit( 'remove', removed );
1121 this.changing = false;
1122 this.emit( 'change' );
1123
1124 return this;
1125 };
1126
1127 /**
1128 * Organize actions.
1129 *
1130 * This is called whenever organized information is requested. It will only reorganize the actions
1131 * if something has changed since the last time it ran.
1132 *
1133 * @private
1134 * @chainable
1135 */
1136 OO.ui.ActionSet.prototype.organize = function () {
1137 var i, iLen, j, jLen, flag, action, category, list, item, special,
1138 specialFlags = this.constructor.static.specialFlags;
1139
1140 if ( !this.organized ) {
1141 this.categorized = {};
1142 this.special = {};
1143 this.others = [];
1144 for ( i = 0, iLen = this.list.length; i < iLen; i++ ) {
1145 action = this.list[ i ];
1146 if ( action.isVisible() ) {
1147 // Populate categories
1148 for ( category in this.categories ) {
1149 if ( !this.categorized[ category ] ) {
1150 this.categorized[ category ] = {};
1151 }
1152 list = action[ this.categories[ category ] ]();
1153 if ( !Array.isArray( list ) ) {
1154 list = [ list ];
1155 }
1156 for ( j = 0, jLen = list.length; j < jLen; j++ ) {
1157 item = list[ j ];
1158 if ( !this.categorized[ category ][ item ] ) {
1159 this.categorized[ category ][ item ] = [];
1160 }
1161 this.categorized[ category ][ item ].push( action );
1162 }
1163 }
1164 // Populate special/others
1165 special = false;
1166 for ( j = 0, jLen = specialFlags.length; j < jLen; j++ ) {
1167 flag = specialFlags[ j ];
1168 if ( !this.special[ flag ] && action.hasFlag( flag ) ) {
1169 this.special[ flag ] = action;
1170 special = true;
1171 break;
1172 }
1173 }
1174 if ( !special ) {
1175 this.others.push( action );
1176 }
1177 }
1178 }
1179 this.organized = true;
1180 }
1181
1182 return this;
1183 };
1184
1185 /**
1186 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
1187 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
1188 * connected to them and can't be interacted with.
1189 *
1190 * @abstract
1191 * @class
1192 *
1193 * @constructor
1194 * @param {Object} [config] Configuration options
1195 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
1196 * to the top level (e.g., the outermost div) of the element. See the [OOjs UI documentation on MediaWiki][2]
1197 * for an example.
1198 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#cssExample
1199 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
1200 * @cfg {string} [text] Text to insert
1201 * @cfg {Array} [content] An array of content elements to append (after #text).
1202 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
1203 * Instances of OO.ui.Element will have their $element appended.
1204 * @cfg {jQuery} [$content] Content elements to append (after #text).
1205 * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName.
1206 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
1207 * Data can also be specified with the #setData method.
1208 */
1209 OO.ui.Element = function OoUiElement( config ) {
1210 // Configuration initialization
1211 config = config || {};
1212
1213 // Properties
1214 this.$ = $;
1215 this.visible = true;
1216 this.data = config.data;
1217 this.$element = config.$element ||
1218 $( document.createElement( this.getTagName() ) );
1219 this.elementGroup = null;
1220 this.debouncedUpdateThemeClassesHandler = OO.ui.debounce( this.debouncedUpdateThemeClasses );
1221
1222 // Initialization
1223 if ( Array.isArray( config.classes ) ) {
1224 this.$element.addClass( config.classes.join( ' ' ) );
1225 }
1226 if ( config.id ) {
1227 this.$element.attr( 'id', config.id );
1228 }
1229 if ( config.text ) {
1230 this.$element.text( config.text );
1231 }
1232 if ( config.content ) {
1233 // The `content` property treats plain strings as text; use an
1234 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
1235 // appropriate $element appended.
1236 this.$element.append( config.content.map( function ( v ) {
1237 if ( typeof v === 'string' ) {
1238 // Escape string so it is properly represented in HTML.
1239 return document.createTextNode( v );
1240 } else if ( v instanceof OO.ui.HtmlSnippet ) {
1241 // Bypass escaping.
1242 return v.toString();
1243 } else if ( v instanceof OO.ui.Element ) {
1244 return v.$element;
1245 }
1246 return v;
1247 } ) );
1248 }
1249 if ( config.$content ) {
1250 // The `$content` property treats plain strings as HTML.
1251 this.$element.append( config.$content );
1252 }
1253 };
1254
1255 /* Setup */
1256
1257 OO.initClass( OO.ui.Element );
1258
1259 /* Static Properties */
1260
1261 /**
1262 * The name of the HTML tag used by the element.
1263 *
1264 * The static value may be ignored if the #getTagName method is overridden.
1265 *
1266 * @static
1267 * @inheritable
1268 * @property {string}
1269 */
1270 OO.ui.Element.static.tagName = 'div';
1271
1272 /* Static Methods */
1273
1274 /**
1275 * Reconstitute a JavaScript object corresponding to a widget created
1276 * by the PHP implementation.
1277 *
1278 * @param {string|HTMLElement|jQuery} idOrNode
1279 * A DOM id (if a string) or node for the widget to infuse.
1280 * @return {OO.ui.Element}
1281 * The `OO.ui.Element` corresponding to this (infusable) document node.
1282 * For `Tag` objects emitted on the HTML side (used occasionally for content)
1283 * the value returned is a newly-created Element wrapping around the existing
1284 * DOM node.
1285 */
1286 OO.ui.Element.static.infuse = function ( idOrNode ) {
1287 var obj = OO.ui.Element.static.unsafeInfuse( idOrNode, false );
1288 // Verify that the type matches up.
1289 // FIXME: uncomment after T89721 is fixed (see T90929)
1290 /*
1291 if ( !( obj instanceof this['class'] ) ) {
1292 throw new Error( 'Infusion type mismatch!' );
1293 }
1294 */
1295 return obj;
1296 };
1297
1298 /**
1299 * Implementation helper for `infuse`; skips the type check and has an
1300 * extra property so that only the top-level invocation touches the DOM.
1301 * @private
1302 * @param {string|HTMLElement|jQuery} idOrNode
1303 * @param {jQuery.Promise|boolean} domPromise A promise that will be resolved
1304 * when the top-level widget of this infusion is inserted into DOM,
1305 * replacing the original node; or false for top-level invocation.
1306 * @return {OO.ui.Element}
1307 */
1308 OO.ui.Element.static.unsafeInfuse = function ( idOrNode, domPromise ) {
1309 // look for a cached result of a previous infusion.
1310 var id, $elem, data, cls, parts, parent, obj, top, state;
1311 if ( typeof idOrNode === 'string' ) {
1312 id = idOrNode;
1313 $elem = $( document.getElementById( id ) );
1314 } else {
1315 $elem = $( idOrNode );
1316 id = $elem.attr( 'id' );
1317 }
1318 if ( !$elem.length ) {
1319 throw new Error( 'Widget not found: ' + id );
1320 }
1321 data = $elem.data( 'ooui-infused' ) || $elem[ 0 ].oouiInfused;
1322 if ( data ) {
1323 // cached!
1324 if ( data === true ) {
1325 throw new Error( 'Circular dependency! ' + id );
1326 }
1327 return data;
1328 }
1329 data = $elem.attr( 'data-ooui' );
1330 if ( !data ) {
1331 throw new Error( 'No infusion data found: ' + id );
1332 }
1333 try {
1334 data = $.parseJSON( data );
1335 } catch ( _ ) {
1336 data = null;
1337 }
1338 if ( !( data && data._ ) ) {
1339 throw new Error( 'No valid infusion data found: ' + id );
1340 }
1341 if ( data._ === 'Tag' ) {
1342 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
1343 return new OO.ui.Element( { $element: $elem } );
1344 }
1345 parts = data._.split( '.' );
1346 cls = OO.getProp.apply( OO, [ window ].concat( parts ) );
1347 if ( cls === undefined ) {
1348 // The PHP output might be old and not including the "OO.ui" prefix
1349 // TODO: Remove this back-compat after next major release
1350 cls = OO.getProp.apply( OO, [ OO.ui ].concat( parts ) );
1351 if ( cls === undefined ) {
1352 throw new Error( 'Unknown widget type: id: ' + id + ', class: ' + data._ );
1353 }
1354 }
1355
1356 // Verify that we're creating an OO.ui.Element instance
1357 parent = cls.parent;
1358
1359 while ( parent !== undefined ) {
1360 if ( parent === OO.ui.Element ) {
1361 // Safe
1362 break;
1363 }
1364
1365 parent = parent.parent;
1366 }
1367
1368 if ( parent !== OO.ui.Element ) {
1369 throw new Error( 'Unknown widget type: id: ' + id + ', class: ' + data._ );
1370 }
1371
1372 if ( domPromise === false ) {
1373 top = $.Deferred();
1374 domPromise = top.promise();
1375 }
1376 $elem.data( 'ooui-infused', true ); // prevent loops
1377 data.id = id; // implicit
1378 data = OO.copy( data, null, function deserialize( value ) {
1379 if ( OO.isPlainObject( value ) ) {
1380 if ( value.tag ) {
1381 return OO.ui.Element.static.unsafeInfuse( value.tag, domPromise );
1382 }
1383 if ( value.html ) {
1384 return new OO.ui.HtmlSnippet( value.html );
1385 }
1386 }
1387 } );
1388 // allow widgets to reuse parts of the DOM
1389 data = cls.static.reusePreInfuseDOM( $elem[ 0 ], data );
1390 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
1391 state = cls.static.gatherPreInfuseState( $elem[ 0 ], data );
1392 // rebuild widget
1393 // jscs:disable requireCapitalizedConstructors
1394 obj = new cls( data );
1395 // jscs:enable requireCapitalizedConstructors
1396 // now replace old DOM with this new DOM.
1397 if ( top ) {
1398 // An efficient constructor might be able to reuse the entire DOM tree of the original element,
1399 // so only mutate the DOM if we need to.
1400 if ( $elem[ 0 ] !== obj.$element[ 0 ] ) {
1401 $elem.replaceWith( obj.$element );
1402 // This element is now gone from the DOM, but if anyone is holding a reference to it,
1403 // let's allow them to OO.ui.infuse() it and do what they expect (T105828).
1404 // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
1405 $elem[ 0 ].oouiInfused = obj;
1406 }
1407 top.resolve();
1408 }
1409 obj.$element.data( 'ooui-infused', obj );
1410 // set the 'data-ooui' attribute so we can identify infused widgets
1411 obj.$element.attr( 'data-ooui', '' );
1412 // restore dynamic state after the new element is inserted into DOM
1413 domPromise.done( obj.restorePreInfuseState.bind( obj, state ) );
1414 return obj;
1415 };
1416
1417 /**
1418 * Pick out parts of `node`'s DOM to be reused when infusing a widget.
1419 *
1420 * This method **must not** make any changes to the DOM, only find interesting pieces and add them
1421 * to `config` (which should then be returned). Actual DOM juggling should then be done by the
1422 * constructor, which will be given the enhanced config.
1423 *
1424 * @protected
1425 * @param {HTMLElement} node
1426 * @param {Object} config
1427 * @return {Object}
1428 */
1429 OO.ui.Element.static.reusePreInfuseDOM = function ( node, config ) {
1430 return config;
1431 };
1432
1433 /**
1434 * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of a HTML DOM node
1435 * (and its children) that represent an Element of the same class and the given configuration,
1436 * generated by the PHP implementation.
1437 *
1438 * This method is called just before `node` is detached from the DOM. The return value of this
1439 * function will be passed to #restorePreInfuseState after the newly created widget's #$element
1440 * is inserted into DOM to replace `node`.
1441 *
1442 * @protected
1443 * @param {HTMLElement} node
1444 * @param {Object} config
1445 * @return {Object}
1446 */
1447 OO.ui.Element.static.gatherPreInfuseState = function () {
1448 return {};
1449 };
1450
1451 /**
1452 * Get a jQuery function within a specific document.
1453 *
1454 * @static
1455 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
1456 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
1457 * not in an iframe
1458 * @return {Function} Bound jQuery function
1459 */
1460 OO.ui.Element.static.getJQuery = function ( context, $iframe ) {
1461 function wrapper( selector ) {
1462 return $( selector, wrapper.context );
1463 }
1464
1465 wrapper.context = this.getDocument( context );
1466
1467 if ( $iframe ) {
1468 wrapper.$iframe = $iframe;
1469 }
1470
1471 return wrapper;
1472 };
1473
1474 /**
1475 * Get the document of an element.
1476 *
1477 * @static
1478 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
1479 * @return {HTMLDocument|null} Document object
1480 */
1481 OO.ui.Element.static.getDocument = function ( obj ) {
1482 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
1483 return ( obj[ 0 ] && obj[ 0 ].ownerDocument ) ||
1484 // Empty jQuery selections might have a context
1485 obj.context ||
1486 // HTMLElement
1487 obj.ownerDocument ||
1488 // Window
1489 obj.document ||
1490 // HTMLDocument
1491 ( obj.nodeType === 9 && obj ) ||
1492 null;
1493 };
1494
1495 /**
1496 * Get the window of an element or document.
1497 *
1498 * @static
1499 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
1500 * @return {Window} Window object
1501 */
1502 OO.ui.Element.static.getWindow = function ( obj ) {
1503 var doc = this.getDocument( obj );
1504 return doc.defaultView;
1505 };
1506
1507 /**
1508 * Get the direction of an element or document.
1509 *
1510 * @static
1511 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
1512 * @return {string} Text direction, either 'ltr' or 'rtl'
1513 */
1514 OO.ui.Element.static.getDir = function ( obj ) {
1515 var isDoc, isWin;
1516
1517 if ( obj instanceof jQuery ) {
1518 obj = obj[ 0 ];
1519 }
1520 isDoc = obj.nodeType === 9;
1521 isWin = obj.document !== undefined;
1522 if ( isDoc || isWin ) {
1523 if ( isWin ) {
1524 obj = obj.document;
1525 }
1526 obj = obj.body;
1527 }
1528 return $( obj ).css( 'direction' );
1529 };
1530
1531 /**
1532 * Get the offset between two frames.
1533 *
1534 * TODO: Make this function not use recursion.
1535 *
1536 * @static
1537 * @param {Window} from Window of the child frame
1538 * @param {Window} [to=window] Window of the parent frame
1539 * @param {Object} [offset] Offset to start with, used internally
1540 * @return {Object} Offset object, containing left and top properties
1541 */
1542 OO.ui.Element.static.getFrameOffset = function ( from, to, offset ) {
1543 var i, len, frames, frame, rect;
1544
1545 if ( !to ) {
1546 to = window;
1547 }
1548 if ( !offset ) {
1549 offset = { top: 0, left: 0 };
1550 }
1551 if ( from.parent === from ) {
1552 return offset;
1553 }
1554
1555 // Get iframe element
1556 frames = from.parent.document.getElementsByTagName( 'iframe' );
1557 for ( i = 0, len = frames.length; i < len; i++ ) {
1558 if ( frames[ i ].contentWindow === from ) {
1559 frame = frames[ i ];
1560 break;
1561 }
1562 }
1563
1564 // Recursively accumulate offset values
1565 if ( frame ) {
1566 rect = frame.getBoundingClientRect();
1567 offset.left += rect.left;
1568 offset.top += rect.top;
1569 if ( from !== to ) {
1570 this.getFrameOffset( from.parent, offset );
1571 }
1572 }
1573 return offset;
1574 };
1575
1576 /**
1577 * Get the offset between two elements.
1578 *
1579 * The two elements may be in a different frame, but in that case the frame $element is in must
1580 * be contained in the frame $anchor is in.
1581 *
1582 * @static
1583 * @param {jQuery} $element Element whose position to get
1584 * @param {jQuery} $anchor Element to get $element's position relative to
1585 * @return {Object} Translated position coordinates, containing top and left properties
1586 */
1587 OO.ui.Element.static.getRelativePosition = function ( $element, $anchor ) {
1588 var iframe, iframePos,
1589 pos = $element.offset(),
1590 anchorPos = $anchor.offset(),
1591 elementDocument = this.getDocument( $element ),
1592 anchorDocument = this.getDocument( $anchor );
1593
1594 // If $element isn't in the same document as $anchor, traverse up
1595 while ( elementDocument !== anchorDocument ) {
1596 iframe = elementDocument.defaultView.frameElement;
1597 if ( !iframe ) {
1598 throw new Error( '$element frame is not contained in $anchor frame' );
1599 }
1600 iframePos = $( iframe ).offset();
1601 pos.left += iframePos.left;
1602 pos.top += iframePos.top;
1603 elementDocument = iframe.ownerDocument;
1604 }
1605 pos.left -= anchorPos.left;
1606 pos.top -= anchorPos.top;
1607 return pos;
1608 };
1609
1610 /**
1611 * Get element border sizes.
1612 *
1613 * @static
1614 * @param {HTMLElement} el Element to measure
1615 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
1616 */
1617 OO.ui.Element.static.getBorders = function ( el ) {
1618 var doc = el.ownerDocument,
1619 win = doc.defaultView,
1620 style = win.getComputedStyle( el, null ),
1621 $el = $( el ),
1622 top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0,
1623 left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0,
1624 bottom = parseFloat( style ? style.borderBottomWidth : $el.css( 'borderBottomWidth' ) ) || 0,
1625 right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0;
1626
1627 return {
1628 top: top,
1629 left: left,
1630 bottom: bottom,
1631 right: right
1632 };
1633 };
1634
1635 /**
1636 * Get dimensions of an element or window.
1637 *
1638 * @static
1639 * @param {HTMLElement|Window} el Element to measure
1640 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
1641 */
1642 OO.ui.Element.static.getDimensions = function ( el ) {
1643 var $el, $win,
1644 doc = el.ownerDocument || el.document,
1645 win = doc.defaultView;
1646
1647 if ( win === el || el === doc.documentElement ) {
1648 $win = $( win );
1649 return {
1650 borders: { top: 0, left: 0, bottom: 0, right: 0 },
1651 scroll: {
1652 top: $win.scrollTop(),
1653 left: $win.scrollLeft()
1654 },
1655 scrollbar: { right: 0, bottom: 0 },
1656 rect: {
1657 top: 0,
1658 left: 0,
1659 bottom: $win.innerHeight(),
1660 right: $win.innerWidth()
1661 }
1662 };
1663 } else {
1664 $el = $( el );
1665 return {
1666 borders: this.getBorders( el ),
1667 scroll: {
1668 top: $el.scrollTop(),
1669 left: $el.scrollLeft()
1670 },
1671 scrollbar: {
1672 right: $el.innerWidth() - el.clientWidth,
1673 bottom: $el.innerHeight() - el.clientHeight
1674 },
1675 rect: el.getBoundingClientRect()
1676 };
1677 }
1678 };
1679
1680 /**
1681 * Get scrollable object parent
1682 *
1683 * documentElement can't be used to get or set the scrollTop
1684 * property on Blink. Changing and testing its value lets us
1685 * use 'body' or 'documentElement' based on what is working.
1686 *
1687 * https://code.google.com/p/chromium/issues/detail?id=303131
1688 *
1689 * @static
1690 * @param {HTMLElement} el Element to find scrollable parent for
1691 * @return {HTMLElement} Scrollable parent
1692 */
1693 OO.ui.Element.static.getRootScrollableElement = function ( el ) {
1694 var scrollTop, body;
1695
1696 if ( OO.ui.scrollableElement === undefined ) {
1697 body = el.ownerDocument.body;
1698 scrollTop = body.scrollTop;
1699 body.scrollTop = 1;
1700
1701 if ( body.scrollTop === 1 ) {
1702 body.scrollTop = scrollTop;
1703 OO.ui.scrollableElement = 'body';
1704 } else {
1705 OO.ui.scrollableElement = 'documentElement';
1706 }
1707 }
1708
1709 return el.ownerDocument[ OO.ui.scrollableElement ];
1710 };
1711
1712 /**
1713 * Get closest scrollable container.
1714 *
1715 * Traverses up until either a scrollable element or the root is reached, in which case the window
1716 * will be returned.
1717 *
1718 * @static
1719 * @param {HTMLElement} el Element to find scrollable container for
1720 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1721 * @return {HTMLElement} Closest scrollable container
1722 */
1723 OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) {
1724 var i, val,
1725 // props = [ 'overflow' ] doesn't work due to https://bugzilla.mozilla.org/show_bug.cgi?id=889091
1726 props = [ 'overflow-x', 'overflow-y' ],
1727 $parent = $( el ).parent();
1728
1729 if ( dimension === 'x' || dimension === 'y' ) {
1730 props = [ 'overflow-' + dimension ];
1731 }
1732
1733 while ( $parent.length ) {
1734 if ( $parent[ 0 ] === this.getRootScrollableElement( el ) ) {
1735 return $parent[ 0 ];
1736 }
1737 i = props.length;
1738 while ( i-- ) {
1739 val = $parent.css( props[ i ] );
1740 if ( val === 'auto' || val === 'scroll' ) {
1741 return $parent[ 0 ];
1742 }
1743 }
1744 $parent = $parent.parent();
1745 }
1746 return this.getDocument( el ).body;
1747 };
1748
1749 /**
1750 * Scroll element into view.
1751 *
1752 * @static
1753 * @param {HTMLElement} el Element to scroll into view
1754 * @param {Object} [config] Configuration options
1755 * @param {string} [config.duration] jQuery animation duration value
1756 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1757 * to scroll in both directions
1758 * @param {Function} [config.complete] Function to call when scrolling completes
1759 */
1760 OO.ui.Element.static.scrollIntoView = function ( el, config ) {
1761 var rel, anim, callback, sc, $sc, eld, scd, $win;
1762
1763 // Configuration initialization
1764 config = config || {};
1765
1766 anim = {};
1767 callback = typeof config.complete === 'function' && config.complete;
1768 sc = this.getClosestScrollableContainer( el, config.direction );
1769 $sc = $( sc );
1770 eld = this.getDimensions( el );
1771 scd = this.getDimensions( sc );
1772 $win = $( this.getWindow( el ) );
1773
1774 // Compute the distances between the edges of el and the edges of the scroll viewport
1775 if ( $sc.is( 'html, body' ) ) {
1776 // If the scrollable container is the root, this is easy
1777 rel = {
1778 top: eld.rect.top,
1779 bottom: $win.innerHeight() - eld.rect.bottom,
1780 left: eld.rect.left,
1781 right: $win.innerWidth() - eld.rect.right
1782 };
1783 } else {
1784 // Otherwise, we have to subtract el's coordinates from sc's coordinates
1785 rel = {
1786 top: eld.rect.top - ( scd.rect.top + scd.borders.top ),
1787 bottom: scd.rect.bottom - scd.borders.bottom - scd.scrollbar.bottom - eld.rect.bottom,
1788 left: eld.rect.left - ( scd.rect.left + scd.borders.left ),
1789 right: scd.rect.right - scd.borders.right - scd.scrollbar.right - eld.rect.right
1790 };
1791 }
1792
1793 if ( !config.direction || config.direction === 'y' ) {
1794 if ( rel.top < 0 ) {
1795 anim.scrollTop = scd.scroll.top + rel.top;
1796 } else if ( rel.top > 0 && rel.bottom < 0 ) {
1797 anim.scrollTop = scd.scroll.top + Math.min( rel.top, -rel.bottom );
1798 }
1799 }
1800 if ( !config.direction || config.direction === 'x' ) {
1801 if ( rel.left < 0 ) {
1802 anim.scrollLeft = scd.scroll.left + rel.left;
1803 } else if ( rel.left > 0 && rel.right < 0 ) {
1804 anim.scrollLeft = scd.scroll.left + Math.min( rel.left, -rel.right );
1805 }
1806 }
1807 if ( !$.isEmptyObject( anim ) ) {
1808 $sc.stop( true ).animate( anim, config.duration || 'fast' );
1809 if ( callback ) {
1810 $sc.queue( function ( next ) {
1811 callback();
1812 next();
1813 } );
1814 }
1815 } else {
1816 if ( callback ) {
1817 callback();
1818 }
1819 }
1820 };
1821
1822 /**
1823 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1824 * and reserve space for them, because it probably doesn't.
1825 *
1826 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1827 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1828 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1829 * and then reattach (or show) them back.
1830 *
1831 * @static
1832 * @param {HTMLElement} el Element to reconsider the scrollbars on
1833 */
1834 OO.ui.Element.static.reconsiderScrollbars = function ( el ) {
1835 var i, len, scrollLeft, scrollTop, nodes = [];
1836 // Save scroll position
1837 scrollLeft = el.scrollLeft;
1838 scrollTop = el.scrollTop;
1839 // Detach all children
1840 while ( el.firstChild ) {
1841 nodes.push( el.firstChild );
1842 el.removeChild( el.firstChild );
1843 }
1844 // Force reflow
1845 void el.offsetHeight;
1846 // Reattach all children
1847 for ( i = 0, len = nodes.length; i < len; i++ ) {
1848 el.appendChild( nodes[ i ] );
1849 }
1850 // Restore scroll position (no-op if scrollbars disappeared)
1851 el.scrollLeft = scrollLeft;
1852 el.scrollTop = scrollTop;
1853 };
1854
1855 /* Methods */
1856
1857 /**
1858 * Toggle visibility of an element.
1859 *
1860 * @param {boolean} [show] Make element visible, omit to toggle visibility
1861 * @fires visible
1862 * @chainable
1863 */
1864 OO.ui.Element.prototype.toggle = function ( show ) {
1865 show = show === undefined ? !this.visible : !!show;
1866
1867 if ( show !== this.isVisible() ) {
1868 this.visible = show;
1869 this.$element.toggleClass( 'oo-ui-element-hidden', !this.visible );
1870 this.emit( 'toggle', show );
1871 }
1872
1873 return this;
1874 };
1875
1876 /**
1877 * Check if element is visible.
1878 *
1879 * @return {boolean} element is visible
1880 */
1881 OO.ui.Element.prototype.isVisible = function () {
1882 return this.visible;
1883 };
1884
1885 /**
1886 * Get element data.
1887 *
1888 * @return {Mixed} Element data
1889 */
1890 OO.ui.Element.prototype.getData = function () {
1891 return this.data;
1892 };
1893
1894 /**
1895 * Set element data.
1896 *
1897 * @param {Mixed} Element data
1898 * @chainable
1899 */
1900 OO.ui.Element.prototype.setData = function ( data ) {
1901 this.data = data;
1902 return this;
1903 };
1904
1905 /**
1906 * Check if element supports one or more methods.
1907 *
1908 * @param {string|string[]} methods Method or list of methods to check
1909 * @return {boolean} All methods are supported
1910 */
1911 OO.ui.Element.prototype.supports = function ( methods ) {
1912 var i, len,
1913 support = 0;
1914
1915 methods = Array.isArray( methods ) ? methods : [ methods ];
1916 for ( i = 0, len = methods.length; i < len; i++ ) {
1917 if ( $.isFunction( this[ methods[ i ] ] ) ) {
1918 support++;
1919 }
1920 }
1921
1922 return methods.length === support;
1923 };
1924
1925 /**
1926 * Update the theme-provided classes.
1927 *
1928 * @localdoc This is called in element mixins and widget classes any time state changes.
1929 * Updating is debounced, minimizing overhead of changing multiple attributes and
1930 * guaranteeing that theme updates do not occur within an element's constructor
1931 */
1932 OO.ui.Element.prototype.updateThemeClasses = function () {
1933 this.debouncedUpdateThemeClassesHandler();
1934 };
1935
1936 /**
1937 * @private
1938 * @localdoc This method is called directly from the QUnit tests instead of #updateThemeClasses, to
1939 * make them synchronous.
1940 */
1941 OO.ui.Element.prototype.debouncedUpdateThemeClasses = function () {
1942 OO.ui.theme.updateElementClasses( this );
1943 };
1944
1945 /**
1946 * Get the HTML tag name.
1947 *
1948 * Override this method to base the result on instance information.
1949 *
1950 * @return {string} HTML tag name
1951 */
1952 OO.ui.Element.prototype.getTagName = function () {
1953 return this.constructor.static.tagName;
1954 };
1955
1956 /**
1957 * Check if the element is attached to the DOM
1958 * @return {boolean} The element is attached to the DOM
1959 */
1960 OO.ui.Element.prototype.isElementAttached = function () {
1961 return $.contains( this.getElementDocument(), this.$element[ 0 ] );
1962 };
1963
1964 /**
1965 * Get the DOM document.
1966 *
1967 * @return {HTMLDocument} Document object
1968 */
1969 OO.ui.Element.prototype.getElementDocument = function () {
1970 // Don't cache this in other ways either because subclasses could can change this.$element
1971 return OO.ui.Element.static.getDocument( this.$element );
1972 };
1973
1974 /**
1975 * Get the DOM window.
1976 *
1977 * @return {Window} Window object
1978 */
1979 OO.ui.Element.prototype.getElementWindow = function () {
1980 return OO.ui.Element.static.getWindow( this.$element );
1981 };
1982
1983 /**
1984 * Get closest scrollable container.
1985 */
1986 OO.ui.Element.prototype.getClosestScrollableElementContainer = function () {
1987 return OO.ui.Element.static.getClosestScrollableContainer( this.$element[ 0 ] );
1988 };
1989
1990 /**
1991 * Get group element is in.
1992 *
1993 * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1994 */
1995 OO.ui.Element.prototype.getElementGroup = function () {
1996 return this.elementGroup;
1997 };
1998
1999 /**
2000 * Set group element is in.
2001 *
2002 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
2003 * @chainable
2004 */
2005 OO.ui.Element.prototype.setElementGroup = function ( group ) {
2006 this.elementGroup = group;
2007 return this;
2008 };
2009
2010 /**
2011 * Scroll element into view.
2012 *
2013 * @param {Object} [config] Configuration options
2014 */
2015 OO.ui.Element.prototype.scrollElementIntoView = function ( config ) {
2016 return OO.ui.Element.static.scrollIntoView( this.$element[ 0 ], config );
2017 };
2018
2019 /**
2020 * Restore the pre-infusion dynamic state for this widget.
2021 *
2022 * This method is called after #$element has been inserted into DOM. The parameter is the return
2023 * value of #gatherPreInfuseState.
2024 *
2025 * @protected
2026 * @param {Object} state
2027 */
2028 OO.ui.Element.prototype.restorePreInfuseState = function () {
2029 };
2030
2031 /**
2032 * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way
2033 * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined.
2034 * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout},
2035 * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
2036 * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout} for more information and examples.
2037 *
2038 * @abstract
2039 * @class
2040 * @extends OO.ui.Element
2041 * @mixins OO.EventEmitter
2042 *
2043 * @constructor
2044 * @param {Object} [config] Configuration options
2045 */
2046 OO.ui.Layout = function OoUiLayout( config ) {
2047 // Configuration initialization
2048 config = config || {};
2049
2050 // Parent constructor
2051 OO.ui.Layout.parent.call( this, config );
2052
2053 // Mixin constructors
2054 OO.EventEmitter.call( this );
2055
2056 // Initialization
2057 this.$element.addClass( 'oo-ui-layout' );
2058 };
2059
2060 /* Setup */
2061
2062 OO.inheritClass( OO.ui.Layout, OO.ui.Element );
2063 OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
2064
2065 /**
2066 * Widgets are compositions of one or more OOjs UI elements that users can both view
2067 * and interact with. All widgets can be configured and modified via a standard API,
2068 * and their state can change dynamically according to a model.
2069 *
2070 * @abstract
2071 * @class
2072 * @extends OO.ui.Element
2073 * @mixins OO.EventEmitter
2074 *
2075 * @constructor
2076 * @param {Object} [config] Configuration options
2077 * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
2078 * appearance reflects this state.
2079 */
2080 OO.ui.Widget = function OoUiWidget( config ) {
2081 // Initialize config
2082 config = $.extend( { disabled: false }, config );
2083
2084 // Parent constructor
2085 OO.ui.Widget.parent.call( this, config );
2086
2087 // Mixin constructors
2088 OO.EventEmitter.call( this );
2089
2090 // Properties
2091 this.disabled = null;
2092 this.wasDisabled = null;
2093
2094 // Initialization
2095 this.$element.addClass( 'oo-ui-widget' );
2096 this.setDisabled( !!config.disabled );
2097 };
2098
2099 /* Setup */
2100
2101 OO.inheritClass( OO.ui.Widget, OO.ui.Element );
2102 OO.mixinClass( OO.ui.Widget, OO.EventEmitter );
2103
2104 /* Static Properties */
2105
2106 /**
2107 * Whether this widget will behave reasonably when wrapped in a HTML `<label>`. If this is true,
2108 * wrappers such as OO.ui.FieldLayout may use a `<label>` instead of implementing own label click
2109 * handling.
2110 *
2111 * @static
2112 * @inheritable
2113 * @property {boolean}
2114 */
2115 OO.ui.Widget.static.supportsSimpleLabel = false;
2116
2117 /* Events */
2118
2119 /**
2120 * @event disable
2121 *
2122 * A 'disable' event is emitted when the disabled state of the widget changes
2123 * (i.e. on disable **and** enable).
2124 *
2125 * @param {boolean} disabled Widget is disabled
2126 */
2127
2128 /**
2129 * @event toggle
2130 *
2131 * A 'toggle' event is emitted when the visibility of the widget changes.
2132 *
2133 * @param {boolean} visible Widget is visible
2134 */
2135
2136 /* Methods */
2137
2138 /**
2139 * Check if the widget is disabled.
2140 *
2141 * @return {boolean} Widget is disabled
2142 */
2143 OO.ui.Widget.prototype.isDisabled = function () {
2144 return this.disabled;
2145 };
2146
2147 /**
2148 * Set the 'disabled' state of the widget.
2149 *
2150 * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
2151 *
2152 * @param {boolean} disabled Disable widget
2153 * @chainable
2154 */
2155 OO.ui.Widget.prototype.setDisabled = function ( disabled ) {
2156 var isDisabled;
2157
2158 this.disabled = !!disabled;
2159 isDisabled = this.isDisabled();
2160 if ( isDisabled !== this.wasDisabled ) {
2161 this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled );
2162 this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled );
2163 this.$element.attr( 'aria-disabled', isDisabled.toString() );
2164 this.emit( 'disable', isDisabled );
2165 this.updateThemeClasses();
2166 }
2167 this.wasDisabled = isDisabled;
2168
2169 return this;
2170 };
2171
2172 /**
2173 * Update the disabled state, in case of changes in parent widget.
2174 *
2175 * @chainable
2176 */
2177 OO.ui.Widget.prototype.updateDisabled = function () {
2178 this.setDisabled( this.disabled );
2179 return this;
2180 };
2181
2182 /**
2183 * A window is a container for elements that are in a child frame. They are used with
2184 * a window manager (OO.ui.WindowManager), which is used to open and close the window and control
2185 * its presentation. The size of a window is specified using a symbolic name (e.g., ‘small’, ‘medium’,
2186 * ‘large’), which is interpreted by the window manager. If the requested size is not recognized,
2187 * the window manager will choose a sensible fallback.
2188 *
2189 * The lifecycle of a window has three primary stages (opening, opened, and closing) in which
2190 * different processes are executed:
2191 *
2192 * **opening**: The opening stage begins when the window manager's {@link OO.ui.WindowManager#openWindow
2193 * openWindow} or the window's {@link #open open} methods are used, and the window manager begins to open
2194 * the window.
2195 *
2196 * - {@link #getSetupProcess} method is called and its result executed
2197 * - {@link #getReadyProcess} method is called and its result executed
2198 *
2199 * **opened**: The window is now open
2200 *
2201 * **closing**: The closing stage begins when the window manager's
2202 * {@link OO.ui.WindowManager#closeWindow closeWindow}
2203 * or the window's {@link #close} methods are used, and the window manager begins to close the window.
2204 *
2205 * - {@link #getHoldProcess} method is called and its result executed
2206 * - {@link #getTeardownProcess} method is called and its result executed. The window is now closed
2207 *
2208 * Each of the window's processes (setup, ready, hold, and teardown) can be extended in subclasses
2209 * by overriding the window's #getSetupProcess, #getReadyProcess, #getHoldProcess and #getTeardownProcess
2210 * methods. Note that each {@link OO.ui.Process process} is executed in series, so asynchronous
2211 * processing can complete. Always assume window processes are executed asynchronously.
2212 *
2213 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
2214 *
2215 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows
2216 *
2217 * @abstract
2218 * @class
2219 * @extends OO.ui.Element
2220 * @mixins OO.EventEmitter
2221 *
2222 * @constructor
2223 * @param {Object} [config] Configuration options
2224 * @cfg {string} [size] Symbolic name of the dialog size: `small`, `medium`, `large`, `larger` or
2225 * `full`. If omitted, the value of the {@link #static-size static size} property will be used.
2226 */
2227 OO.ui.Window = function OoUiWindow( config ) {
2228 // Configuration initialization
2229 config = config || {};
2230
2231 // Parent constructor
2232 OO.ui.Window.parent.call( this, config );
2233
2234 // Mixin constructors
2235 OO.EventEmitter.call( this );
2236
2237 // Properties
2238 this.manager = null;
2239 this.size = config.size || this.constructor.static.size;
2240 this.$frame = $( '<div>' );
2241 this.$overlay = $( '<div>' );
2242 this.$content = $( '<div>' );
2243
2244 this.$focusTrapBefore = $( '<div>' ).prop( 'tabIndex', 0 );
2245 this.$focusTrapAfter = $( '<div>' ).prop( 'tabIndex', 0 );
2246 this.$focusTraps = this.$focusTrapBefore.add( this.$focusTrapAfter );
2247
2248 // Initialization
2249 this.$overlay.addClass( 'oo-ui-window-overlay' );
2250 this.$content
2251 .addClass( 'oo-ui-window-content' )
2252 .attr( 'tabindex', 0 );
2253 this.$frame
2254 .addClass( 'oo-ui-window-frame' )
2255 .append( this.$focusTrapBefore, this.$content, this.$focusTrapAfter );
2256
2257 this.$element
2258 .addClass( 'oo-ui-window' )
2259 .append( this.$frame, this.$overlay );
2260
2261 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
2262 // that reference properties not initialized at that time of parent class construction
2263 // TODO: Find a better way to handle post-constructor setup
2264 this.visible = false;
2265 this.$element.addClass( 'oo-ui-element-hidden' );
2266 };
2267
2268 /* Setup */
2269
2270 OO.inheritClass( OO.ui.Window, OO.ui.Element );
2271 OO.mixinClass( OO.ui.Window, OO.EventEmitter );
2272
2273 /* Static Properties */
2274
2275 /**
2276 * Symbolic name of the window size: `small`, `medium`, `large`, `larger` or `full`.
2277 *
2278 * The static size is used if no #size is configured during construction.
2279 *
2280 * @static
2281 * @inheritable
2282 * @property {string}
2283 */
2284 OO.ui.Window.static.size = 'medium';
2285
2286 /* Methods */
2287
2288 /**
2289 * Handle mouse down events.
2290 *
2291 * @private
2292 * @param {jQuery.Event} e Mouse down event
2293 */
2294 OO.ui.Window.prototype.onMouseDown = function ( e ) {
2295 // Prevent clicking on the click-block from stealing focus
2296 if ( e.target === this.$element[ 0 ] ) {
2297 return false;
2298 }
2299 };
2300
2301 /**
2302 * Check if the window has been initialized.
2303 *
2304 * Initialization occurs when a window is added to a manager.
2305 *
2306 * @return {boolean} Window has been initialized
2307 */
2308 OO.ui.Window.prototype.isInitialized = function () {
2309 return !!this.manager;
2310 };
2311
2312 /**
2313 * Check if the window is visible.
2314 *
2315 * @return {boolean} Window is visible
2316 */
2317 OO.ui.Window.prototype.isVisible = function () {
2318 return this.visible;
2319 };
2320
2321 /**
2322 * Check if the window is opening.
2323 *
2324 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpening isOpening}
2325 * method.
2326 *
2327 * @return {boolean} Window is opening
2328 */
2329 OO.ui.Window.prototype.isOpening = function () {
2330 return this.manager.isOpening( this );
2331 };
2332
2333 /**
2334 * Check if the window is closing.
2335 *
2336 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isClosing isClosing} method.
2337 *
2338 * @return {boolean} Window is closing
2339 */
2340 OO.ui.Window.prototype.isClosing = function () {
2341 return this.manager.isClosing( this );
2342 };
2343
2344 /**
2345 * Check if the window is opened.
2346 *
2347 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpened isOpened} method.
2348 *
2349 * @return {boolean} Window is opened
2350 */
2351 OO.ui.Window.prototype.isOpened = function () {
2352 return this.manager.isOpened( this );
2353 };
2354
2355 /**
2356 * Get the window manager.
2357 *
2358 * All windows must be attached to a window manager, which is used to open
2359 * and close the window and control its presentation.
2360 *
2361 * @return {OO.ui.WindowManager} Manager of window
2362 */
2363 OO.ui.Window.prototype.getManager = function () {
2364 return this.manager;
2365 };
2366
2367 /**
2368 * Get the symbolic name of the window size (e.g., `small` or `medium`).
2369 *
2370 * @return {string} Symbolic name of the size: `small`, `medium`, `large`, `larger`, `full`
2371 */
2372 OO.ui.Window.prototype.getSize = function () {
2373 var viewport = OO.ui.Element.static.getDimensions( this.getElementWindow() ),
2374 sizes = this.manager.constructor.static.sizes,
2375 size = this.size;
2376
2377 if ( !sizes[ size ] ) {
2378 size = this.manager.constructor.static.defaultSize;
2379 }
2380 if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[ size ].width ) {
2381 size = 'full';
2382 }
2383
2384 return size;
2385 };
2386
2387 /**
2388 * Get the size properties associated with the current window size
2389 *
2390 * @return {Object} Size properties
2391 */
2392 OO.ui.Window.prototype.getSizeProperties = function () {
2393 return this.manager.constructor.static.sizes[ this.getSize() ];
2394 };
2395
2396 /**
2397 * Disable transitions on window's frame for the duration of the callback function, then enable them
2398 * back.
2399 *
2400 * @private
2401 * @param {Function} callback Function to call while transitions are disabled
2402 */
2403 OO.ui.Window.prototype.withoutSizeTransitions = function ( callback ) {
2404 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
2405 // Disable transitions first, otherwise we'll get values from when the window was animating.
2406 var oldTransition,
2407 styleObj = this.$frame[ 0 ].style;
2408 oldTransition = styleObj.transition || styleObj.OTransition || styleObj.MsTransition ||
2409 styleObj.MozTransition || styleObj.WebkitTransition;
2410 styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
2411 styleObj.MozTransition = styleObj.WebkitTransition = 'none';
2412 callback();
2413 // Force reflow to make sure the style changes done inside callback really are not transitioned
2414 this.$frame.height();
2415 styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
2416 styleObj.MozTransition = styleObj.WebkitTransition = oldTransition;
2417 };
2418
2419 /**
2420 * Get the height of the full window contents (i.e., the window head, body and foot together).
2421 *
2422 * What consistitutes the head, body, and foot varies depending on the window type.
2423 * A {@link OO.ui.MessageDialog message dialog} displays a title and message in its body,
2424 * and any actions in the foot. A {@link OO.ui.ProcessDialog process dialog} displays a title
2425 * and special actions in the head, and dialog content in the body.
2426 *
2427 * To get just the height of the dialog body, use the #getBodyHeight method.
2428 *
2429 * @return {number} The height of the window contents (the dialog head, body and foot) in pixels
2430 */
2431 OO.ui.Window.prototype.getContentHeight = function () {
2432 var bodyHeight,
2433 win = this,
2434 bodyStyleObj = this.$body[ 0 ].style,
2435 frameStyleObj = this.$frame[ 0 ].style;
2436
2437 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
2438 // Disable transitions first, otherwise we'll get values from when the window was animating.
2439 this.withoutSizeTransitions( function () {
2440 var oldHeight = frameStyleObj.height,
2441 oldPosition = bodyStyleObj.position;
2442 frameStyleObj.height = '1px';
2443 // Force body to resize to new width
2444 bodyStyleObj.position = 'relative';
2445 bodyHeight = win.getBodyHeight();
2446 frameStyleObj.height = oldHeight;
2447 bodyStyleObj.position = oldPosition;
2448 } );
2449
2450 return (
2451 // Add buffer for border
2452 ( this.$frame.outerHeight() - this.$frame.innerHeight() ) +
2453 // Use combined heights of children
2454 ( this.$head.outerHeight( true ) + bodyHeight + this.$foot.outerHeight( true ) )
2455 );
2456 };
2457
2458 /**
2459 * Get the height of the window body.
2460 *
2461 * To get the height of the full window contents (the window body, head, and foot together),
2462 * use #getContentHeight.
2463 *
2464 * When this function is called, the window will temporarily have been resized
2465 * to height=1px, so .scrollHeight measurements can be taken accurately.
2466 *
2467 * @return {number} Height of the window body in pixels
2468 */
2469 OO.ui.Window.prototype.getBodyHeight = function () {
2470 return this.$body[ 0 ].scrollHeight;
2471 };
2472
2473 /**
2474 * Get the directionality of the frame (right-to-left or left-to-right).
2475 *
2476 * @return {string} Directionality: `'ltr'` or `'rtl'`
2477 */
2478 OO.ui.Window.prototype.getDir = function () {
2479 return OO.ui.Element.static.getDir( this.$content ) || 'ltr';
2480 };
2481
2482 /**
2483 * Get the 'setup' process.
2484 *
2485 * The setup process is used to set up a window for use in a particular context,
2486 * based on the `data` argument. This method is called during the opening phase of the window’s
2487 * lifecycle.
2488 *
2489 * Override this method to add additional steps to the ‘setup’ process the parent method provides
2490 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2491 * of OO.ui.Process.
2492 *
2493 * To add window content that persists between openings, you may wish to use the #initialize method
2494 * instead.
2495 *
2496 * @param {Object} [data] Window opening data
2497 * @return {OO.ui.Process} Setup process
2498 */
2499 OO.ui.Window.prototype.getSetupProcess = function () {
2500 return new OO.ui.Process();
2501 };
2502
2503 /**
2504 * Get the ‘ready’ process.
2505 *
2506 * The ready process is used to ready a window for use in a particular
2507 * context, based on the `data` argument. This method is called during the opening phase of
2508 * the window’s lifecycle, after the window has been {@link #getSetupProcess setup}.
2509 *
2510 * Override this method to add additional steps to the ‘ready’ process the parent method
2511 * provides using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next}
2512 * methods of OO.ui.Process.
2513 *
2514 * @param {Object} [data] Window opening data
2515 * @return {OO.ui.Process} Ready process
2516 */
2517 OO.ui.Window.prototype.getReadyProcess = function () {
2518 return new OO.ui.Process();
2519 };
2520
2521 /**
2522 * Get the 'hold' process.
2523 *
2524 * The hold proccess is used to keep a window from being used in a particular context,
2525 * based on the `data` argument. This method is called during the closing phase of the window’s
2526 * lifecycle.
2527 *
2528 * Override this method to add additional steps to the 'hold' process the parent method provides
2529 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2530 * of OO.ui.Process.
2531 *
2532 * @param {Object} [data] Window closing data
2533 * @return {OO.ui.Process} Hold process
2534 */
2535 OO.ui.Window.prototype.getHoldProcess = function () {
2536 return new OO.ui.Process();
2537 };
2538
2539 /**
2540 * Get the ‘teardown’ process.
2541 *
2542 * The teardown process is used to teardown a window after use. During teardown,
2543 * user interactions within the window are conveyed and the window is closed, based on the `data`
2544 * argument. This method is called during the closing phase of the window’s lifecycle.
2545 *
2546 * Override this method to add additional steps to the ‘teardown’ process the parent method provides
2547 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
2548 * of OO.ui.Process.
2549 *
2550 * @param {Object} [data] Window closing data
2551 * @return {OO.ui.Process} Teardown process
2552 */
2553 OO.ui.Window.prototype.getTeardownProcess = function () {
2554 return new OO.ui.Process();
2555 };
2556
2557 /**
2558 * Set the window manager.
2559 *
2560 * This will cause the window to initialize. Calling it more than once will cause an error.
2561 *
2562 * @param {OO.ui.WindowManager} manager Manager for this window
2563 * @throws {Error} An error is thrown if the method is called more than once
2564 * @chainable
2565 */
2566 OO.ui.Window.prototype.setManager = function ( manager ) {
2567 if ( this.manager ) {
2568 throw new Error( 'Cannot set window manager, window already has a manager' );
2569 }
2570
2571 this.manager = manager;
2572 this.initialize();
2573
2574 return this;
2575 };
2576
2577 /**
2578 * Set the window size by symbolic name (e.g., 'small' or 'medium')
2579 *
2580 * @param {string} size Symbolic name of size: `small`, `medium`, `large`, `larger` or
2581 * `full`
2582 * @chainable
2583 */
2584 OO.ui.Window.prototype.setSize = function ( size ) {
2585 this.size = size;
2586 this.updateSize();
2587 return this;
2588 };
2589
2590 /**
2591 * Update the window size.
2592 *
2593 * @throws {Error} An error is thrown if the window is not attached to a window manager
2594 * @chainable
2595 */
2596 OO.ui.Window.prototype.updateSize = function () {
2597 if ( !this.manager ) {
2598 throw new Error( 'Cannot update window size, must be attached to a manager' );
2599 }
2600
2601 this.manager.updateWindowSize( this );
2602
2603 return this;
2604 };
2605
2606 /**
2607 * Set window dimensions. This method is called by the {@link OO.ui.WindowManager window manager}
2608 * when the window is opening. In general, setDimensions should not be called directly.
2609 *
2610 * To set the size of the window, use the #setSize method.
2611 *
2612 * @param {Object} dim CSS dimension properties
2613 * @param {string|number} [dim.width] Width
2614 * @param {string|number} [dim.minWidth] Minimum width
2615 * @param {string|number} [dim.maxWidth] Maximum width
2616 * @param {string|number} [dim.width] Height, omit to set based on height of contents
2617 * @param {string|number} [dim.minWidth] Minimum height
2618 * @param {string|number} [dim.maxWidth] Maximum height
2619 * @chainable
2620 */
2621 OO.ui.Window.prototype.setDimensions = function ( dim ) {
2622 var height,
2623 win = this,
2624 styleObj = this.$frame[ 0 ].style;
2625
2626 // Calculate the height we need to set using the correct width
2627 if ( dim.height === undefined ) {
2628 this.withoutSizeTransitions( function () {
2629 var oldWidth = styleObj.width;
2630 win.$frame.css( 'width', dim.width || '' );
2631 height = win.getContentHeight();
2632 styleObj.width = oldWidth;
2633 } );
2634 } else {
2635 height = dim.height;
2636 }
2637
2638 this.$frame.css( {
2639 width: dim.width || '',
2640 minWidth: dim.minWidth || '',
2641 maxWidth: dim.maxWidth || '',
2642 height: height || '',
2643 minHeight: dim.minHeight || '',
2644 maxHeight: dim.maxHeight || ''
2645 } );
2646
2647 return this;
2648 };
2649
2650 /**
2651 * Initialize window contents.
2652 *
2653 * Before the window is opened for the first time, #initialize is called so that content that
2654 * persists between openings can be added to the window.
2655 *
2656 * To set up a window with new content each time the window opens, use #getSetupProcess.
2657 *
2658 * @throws {Error} An error is thrown if the window is not attached to a window manager
2659 * @chainable
2660 */
2661 OO.ui.Window.prototype.initialize = function () {
2662 if ( !this.manager ) {
2663 throw new Error( 'Cannot initialize window, must be attached to a manager' );
2664 }
2665
2666 // Properties
2667 this.$head = $( '<div>' );
2668 this.$body = $( '<div>' );
2669 this.$foot = $( '<div>' );
2670 this.$document = $( this.getElementDocument() );
2671
2672 // Events
2673 this.$element.on( 'mousedown', this.onMouseDown.bind( this ) );
2674
2675 // Initialization
2676 this.$head.addClass( 'oo-ui-window-head' );
2677 this.$body.addClass( 'oo-ui-window-body' );
2678 this.$foot.addClass( 'oo-ui-window-foot' );
2679 this.$content.append( this.$head, this.$body, this.$foot );
2680
2681 return this;
2682 };
2683
2684 /**
2685 * Called when someone tries to focus the hidden element at the end of the dialog.
2686 * Sends focus back to the start of the dialog.
2687 *
2688 * @param {jQuery.Event} event Focus event
2689 */
2690 OO.ui.Window.prototype.onFocusTrapFocused = function ( event ) {
2691 if ( this.$focusTrapBefore.is( event.target ) ) {
2692 OO.ui.findFocusable( this.$content, true ).focus();
2693 } else {
2694 // this.$content is the part of the focus cycle, and is the first focusable element
2695 this.$content.focus();
2696 }
2697 };
2698
2699 /**
2700 * Open the window.
2701 *
2702 * This method is a wrapper around a call to the window manager’s {@link OO.ui.WindowManager#openWindow openWindow}
2703 * method, which returns a promise resolved when the window is done opening.
2704 *
2705 * To customize the window each time it opens, use #getSetupProcess or #getReadyProcess.
2706 *
2707 * @param {Object} [data] Window opening data
2708 * @return {jQuery.Promise} Promise resolved with a value when the window is opened, or rejected
2709 * if the window fails to open. When the promise is resolved successfully, the first argument of the
2710 * value is a new promise, which is resolved when the window begins closing.
2711 * @throws {Error} An error is thrown if the window is not attached to a window manager
2712 */
2713 OO.ui.Window.prototype.open = function ( data ) {
2714 if ( !this.manager ) {
2715 throw new Error( 'Cannot open window, must be attached to a manager' );
2716 }
2717
2718 return this.manager.openWindow( this, data );
2719 };
2720
2721 /**
2722 * Close the window.
2723 *
2724 * This method is a wrapper around a call to the window
2725 * manager’s {@link OO.ui.WindowManager#closeWindow closeWindow} method,
2726 * which returns a closing promise resolved when the window is done closing.
2727 *
2728 * The window's #getHoldProcess and #getTeardownProcess methods are called during the closing
2729 * phase of the window’s lifecycle and can be used to specify closing behavior each time
2730 * the window closes.
2731 *
2732 * @param {Object} [data] Window closing data
2733 * @return {jQuery.Promise} Promise resolved when window is closed
2734 * @throws {Error} An error is thrown if the window is not attached to a window manager
2735 */
2736 OO.ui.Window.prototype.close = function ( data ) {
2737 if ( !this.manager ) {
2738 throw new Error( 'Cannot close window, must be attached to a manager' );
2739 }
2740
2741 return this.manager.closeWindow( this, data );
2742 };
2743
2744 /**
2745 * Setup window.
2746 *
2747 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2748 * by other systems.
2749 *
2750 * @param {Object} [data] Window opening data
2751 * @return {jQuery.Promise} Promise resolved when window is setup
2752 */
2753 OO.ui.Window.prototype.setup = function ( data ) {
2754 var win = this;
2755
2756 this.toggle( true );
2757
2758 this.focusTrapHandler = OO.ui.bind( this.onFocusTrapFocused, this );
2759 this.$focusTraps.on( 'focus', this.focusTrapHandler );
2760
2761 return this.getSetupProcess( data ).execute().then( function () {
2762 // Force redraw by asking the browser to measure the elements' widths
2763 win.$element.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2764 win.$content.addClass( 'oo-ui-window-content-setup' ).width();
2765 } );
2766 };
2767
2768 /**
2769 * Ready window.
2770 *
2771 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2772 * by other systems.
2773 *
2774 * @param {Object} [data] Window opening data
2775 * @return {jQuery.Promise} Promise resolved when window is ready
2776 */
2777 OO.ui.Window.prototype.ready = function ( data ) {
2778 var win = this;
2779
2780 this.$content.focus();
2781 return this.getReadyProcess( data ).execute().then( function () {
2782 // Force redraw by asking the browser to measure the elements' widths
2783 win.$element.addClass( 'oo-ui-window-ready' ).width();
2784 win.$content.addClass( 'oo-ui-window-content-ready' ).width();
2785 } );
2786 };
2787
2788 /**
2789 * Hold window.
2790 *
2791 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2792 * by other systems.
2793 *
2794 * @param {Object} [data] Window closing data
2795 * @return {jQuery.Promise} Promise resolved when window is held
2796 */
2797 OO.ui.Window.prototype.hold = function ( data ) {
2798 var win = this;
2799
2800 return this.getHoldProcess( data ).execute().then( function () {
2801 // Get the focused element within the window's content
2802 var $focus = win.$content.find( OO.ui.Element.static.getDocument( win.$content ).activeElement );
2803
2804 // Blur the focused element
2805 if ( $focus.length ) {
2806 $focus[ 0 ].blur();
2807 }
2808
2809 // Force redraw by asking the browser to measure the elements' widths
2810 win.$element.removeClass( 'oo-ui-window-ready' ).width();
2811 win.$content.removeClass( 'oo-ui-window-content-ready' ).width();
2812 } );
2813 };
2814
2815 /**
2816 * Teardown window.
2817 *
2818 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2819 * by other systems.
2820 *
2821 * @param {Object} [data] Window closing data
2822 * @return {jQuery.Promise} Promise resolved when window is torn down
2823 */
2824 OO.ui.Window.prototype.teardown = function ( data ) {
2825 var win = this;
2826
2827 return this.getTeardownProcess( data ).execute().then( function () {
2828 // Force redraw by asking the browser to measure the elements' widths
2829 win.$element.removeClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2830 win.$content.removeClass( 'oo-ui-window-content-setup' ).width();
2831 win.$focusTraps.off( 'focus', win.focusTrapHandler );
2832 win.toggle( false );
2833 } );
2834 };
2835
2836 /**
2837 * The Dialog class serves as the base class for the other types of dialogs.
2838 * Unless extended to include controls, the rendered dialog box is a simple window
2839 * that users can close by hitting the ‘Esc’ key. Dialog windows are used with OO.ui.WindowManager,
2840 * which opens, closes, and controls the presentation of the window. See the
2841 * [OOjs UI documentation on MediaWiki] [1] for more information.
2842 *
2843 * @example
2844 * // A simple dialog window.
2845 * function MyDialog( config ) {
2846 * MyDialog.parent.call( this, config );
2847 * }
2848 * OO.inheritClass( MyDialog, OO.ui.Dialog );
2849 * MyDialog.prototype.initialize = function () {
2850 * MyDialog.parent.prototype.initialize.call( this );
2851 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
2852 * this.content.$element.append( '<p>A simple dialog window. Press \'Esc\' to close.</p>' );
2853 * this.$body.append( this.content.$element );
2854 * };
2855 * MyDialog.prototype.getBodyHeight = function () {
2856 * return this.content.$element.outerHeight( true );
2857 * };
2858 * var myDialog = new MyDialog( {
2859 * size: 'medium'
2860 * } );
2861 * // Create and append a window manager, which opens and closes the window.
2862 * var windowManager = new OO.ui.WindowManager();
2863 * $( 'body' ).append( windowManager.$element );
2864 * windowManager.addWindows( [ myDialog ] );
2865 * // Open the window!
2866 * windowManager.openWindow( myDialog );
2867 *
2868 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Dialogs
2869 *
2870 * @abstract
2871 * @class
2872 * @extends OO.ui.Window
2873 * @mixins OO.ui.mixin.PendingElement
2874 *
2875 * @constructor
2876 * @param {Object} [config] Configuration options
2877 */
2878 OO.ui.Dialog = function OoUiDialog( config ) {
2879 // Parent constructor
2880 OO.ui.Dialog.parent.call( this, config );
2881
2882 // Mixin constructors
2883 OO.ui.mixin.PendingElement.call( this );
2884
2885 // Properties
2886 this.actions = new OO.ui.ActionSet();
2887 this.attachedActions = [];
2888 this.currentAction = null;
2889 this.onDialogKeyDownHandler = this.onDialogKeyDown.bind( this );
2890
2891 // Events
2892 this.actions.connect( this, {
2893 click: 'onActionClick',
2894 resize: 'onActionResize',
2895 change: 'onActionsChange'
2896 } );
2897
2898 // Initialization
2899 this.$element
2900 .addClass( 'oo-ui-dialog' )
2901 .attr( 'role', 'dialog' );
2902 };
2903
2904 /* Setup */
2905
2906 OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
2907 OO.mixinClass( OO.ui.Dialog, OO.ui.mixin.PendingElement );
2908
2909 /* Static Properties */
2910
2911 /**
2912 * Symbolic name of dialog.
2913 *
2914 * The dialog class must have a symbolic name in order to be registered with OO.Factory.
2915 * Please see the [OOjs UI documentation on MediaWiki] [3] for more information.
2916 *
2917 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
2918 *
2919 * @abstract
2920 * @static
2921 * @inheritable
2922 * @property {string}
2923 */
2924 OO.ui.Dialog.static.name = '';
2925
2926 /**
2927 * The dialog title.
2928 *
2929 * The title can be specified as a plaintext string, a {@link OO.ui.mixin.LabelElement Label} node, or a function
2930 * that will produce a Label node or string. The title can also be specified with data passed to the
2931 * constructor (see #getSetupProcess). In this case, the static value will be overridden.
2932 *
2933 * @abstract
2934 * @static
2935 * @inheritable
2936 * @property {jQuery|string|Function}
2937 */
2938 OO.ui.Dialog.static.title = '';
2939
2940 /**
2941 * An array of configured {@link OO.ui.ActionWidget action widgets}.
2942 *
2943 * Actions can also be specified with data passed to the constructor (see #getSetupProcess). In this case, the static
2944 * value will be overridden.
2945 *
2946 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
2947 *
2948 * @static
2949 * @inheritable
2950 * @property {Object[]}
2951 */
2952 OO.ui.Dialog.static.actions = [];
2953
2954 /**
2955 * Close the dialog when the 'Esc' key is pressed.
2956 *
2957 * @static
2958 * @abstract
2959 * @inheritable
2960 * @property {boolean}
2961 */
2962 OO.ui.Dialog.static.escapable = true;
2963
2964 /* Methods */
2965
2966 /**
2967 * Handle frame document key down events.
2968 *
2969 * @private
2970 * @param {jQuery.Event} e Key down event
2971 */
2972 OO.ui.Dialog.prototype.onDialogKeyDown = function ( e ) {
2973 if ( e.which === OO.ui.Keys.ESCAPE ) {
2974 this.executeAction( '' );
2975 e.preventDefault();
2976 e.stopPropagation();
2977 }
2978 };
2979
2980 /**
2981 * Handle action resized events.
2982 *
2983 * @private
2984 * @param {OO.ui.ActionWidget} action Action that was resized
2985 */
2986 OO.ui.Dialog.prototype.onActionResize = function () {
2987 // Override in subclass
2988 };
2989
2990 /**
2991 * Handle action click events.
2992 *
2993 * @private
2994 * @param {OO.ui.ActionWidget} action Action that was clicked
2995 */
2996 OO.ui.Dialog.prototype.onActionClick = function ( action ) {
2997 if ( !this.isPending() ) {
2998 this.executeAction( action.getAction() );
2999 }
3000 };
3001
3002 /**
3003 * Handle actions change event.
3004 *
3005 * @private
3006 */
3007 OO.ui.Dialog.prototype.onActionsChange = function () {
3008 this.detachActions();
3009 if ( !this.isClosing() ) {
3010 this.attachActions();
3011 }
3012 };
3013
3014 /**
3015 * Get the set of actions used by the dialog.
3016 *
3017 * @return {OO.ui.ActionSet}
3018 */
3019 OO.ui.Dialog.prototype.getActions = function () {
3020 return this.actions;
3021 };
3022
3023 /**
3024 * Get a process for taking action.
3025 *
3026 * When you override this method, you can create a new OO.ui.Process and return it, or add additional
3027 * accept steps to the process the parent method provides using the {@link OO.ui.Process#first 'first'}
3028 * and {@link OO.ui.Process#next 'next'} methods of OO.ui.Process.
3029 *
3030 * @param {string} [action] Symbolic name of action
3031 * @return {OO.ui.Process} Action process
3032 */
3033 OO.ui.Dialog.prototype.getActionProcess = function ( action ) {
3034 return new OO.ui.Process()
3035 .next( function () {
3036 if ( !action ) {
3037 // An empty action always closes the dialog without data, which should always be
3038 // safe and make no changes
3039 this.close();
3040 }
3041 }, this );
3042 };
3043
3044 /**
3045 * @inheritdoc
3046 *
3047 * @param {Object} [data] Dialog opening data
3048 * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use
3049 * the {@link #static-title static title}
3050 * @param {Object[]} [data.actions] List of configuration options for each
3051 * {@link OO.ui.ActionWidget action widget}, omit to use {@link #static-actions static actions}.
3052 */
3053 OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
3054 data = data || {};
3055
3056 // Parent method
3057 return OO.ui.Dialog.parent.prototype.getSetupProcess.call( this, data )
3058 .next( function () {
3059 var config = this.constructor.static,
3060 actions = data.actions !== undefined ? data.actions : config.actions;
3061
3062 this.title.setLabel(
3063 data.title !== undefined ? data.title : this.constructor.static.title
3064 );
3065 this.actions.add( this.getActionWidgets( actions ) );
3066
3067 if ( this.constructor.static.escapable ) {
3068 this.$element.on( 'keydown', this.onDialogKeyDownHandler );
3069 }
3070 }, this );
3071 };
3072
3073 /**
3074 * @inheritdoc
3075 */
3076 OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
3077 // Parent method
3078 return OO.ui.Dialog.parent.prototype.getTeardownProcess.call( this, data )
3079 .first( function () {
3080 if ( this.constructor.static.escapable ) {
3081 this.$element.off( 'keydown', this.onDialogKeyDownHandler );
3082 }
3083
3084 this.actions.clear();
3085 this.currentAction = null;
3086 }, this );
3087 };
3088
3089 /**
3090 * @inheritdoc
3091 */
3092 OO.ui.Dialog.prototype.initialize = function () {
3093 var titleId;
3094
3095 // Parent method
3096 OO.ui.Dialog.parent.prototype.initialize.call( this );
3097
3098 titleId = OO.ui.generateElementId();
3099
3100 // Properties
3101 this.title = new OO.ui.LabelWidget( {
3102 id: titleId
3103 } );
3104
3105 // Initialization
3106 this.$content.addClass( 'oo-ui-dialog-content' );
3107 this.$element.attr( 'aria-labelledby', titleId );
3108 this.setPendingElement( this.$head );
3109 };
3110
3111 /**
3112 * Get action widgets from a list of configs
3113 *
3114 * @param {Object[]} actions Action widget configs
3115 * @return {OO.ui.ActionWidget[]} Action widgets
3116 */
3117 OO.ui.Dialog.prototype.getActionWidgets = function ( actions ) {
3118 var i, len, widgets = [];
3119 for ( i = 0, len = actions.length; i < len; i++ ) {
3120 widgets.push(
3121 new OO.ui.ActionWidget( actions[ i ] )
3122 );
3123 }
3124 return widgets;
3125 };
3126
3127 /**
3128 * Attach action actions.
3129 *
3130 * @protected
3131 */
3132 OO.ui.Dialog.prototype.attachActions = function () {
3133 // Remember the list of potentially attached actions
3134 this.attachedActions = this.actions.get();
3135 };
3136
3137 /**
3138 * Detach action actions.
3139 *
3140 * @protected
3141 * @chainable
3142 */
3143 OO.ui.Dialog.prototype.detachActions = function () {
3144 var i, len;
3145
3146 // Detach all actions that may have been previously attached
3147 for ( i = 0, len = this.attachedActions.length; i < len; i++ ) {
3148 this.attachedActions[ i ].$element.detach();
3149 }
3150 this.attachedActions = [];
3151 };
3152
3153 /**
3154 * Execute an action.
3155 *
3156 * @param {string} action Symbolic name of action to execute
3157 * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
3158 */
3159 OO.ui.Dialog.prototype.executeAction = function ( action ) {
3160 this.pushPending();
3161 this.currentAction = action;
3162 return this.getActionProcess( action ).execute()
3163 .always( this.popPending.bind( this ) );
3164 };
3165
3166 /**
3167 * Window managers are used to open and close {@link OO.ui.Window windows} and control their presentation.
3168 * Managed windows are mutually exclusive. If a new window is opened while a current window is opening
3169 * or is opened, the current window will be closed and any ongoing {@link OO.ui.Process process} will be cancelled. Windows
3170 * themselves are persistent and—rather than being torn down when closed—can be repopulated with the
3171 * pertinent data and reused.
3172 *
3173 * Over the lifecycle of a window, the window manager makes available three promises: `opening`,
3174 * `opened`, and `closing`, which represent the primary stages of the cycle:
3175 *
3176 * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s
3177 * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window.
3178 *
3179 * - an `opening` event is emitted with an `opening` promise
3180 * - the #getSetupDelay method is called and the returned value is used to time a pause in execution before
3181 * the window’s {@link OO.ui.Window#getSetupProcess getSetupProcess} method is called on the
3182 * window and its result executed
3183 * - a `setup` progress notification is emitted from the `opening` promise
3184 * - the #getReadyDelay method is called the returned value is used to time a pause in execution before
3185 * the window’s {@link OO.ui.Window#getReadyProcess getReadyProcess} method is called on the
3186 * window and its result executed
3187 * - a `ready` progress notification is emitted from the `opening` promise
3188 * - the `opening` promise is resolved with an `opened` promise
3189 *
3190 * **Opened**: the window is now open.
3191 *
3192 * **Closing**: the closing stage begins when the window manager's #closeWindow or the
3193 * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins
3194 * to close the window.
3195 *
3196 * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted
3197 * - the #getHoldDelay method is called and the returned value is used to time a pause in execution before
3198 * the window's {@link OO.ui.Window#getHoldProcess getHoldProces} method is called on the
3199 * window and its result executed
3200 * - a `hold` progress notification is emitted from the `closing` promise
3201 * - the #getTeardownDelay() method is called and the returned value is used to time a pause in execution before
3202 * the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method is called on the
3203 * window and its result executed
3204 * - a `teardown` progress notification is emitted from the `closing` promise
3205 * - the `closing` promise is resolved. The window is now closed
3206 *
3207 * See the [OOjs UI documentation on MediaWiki][1] for more information.
3208 *
3209 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
3210 *
3211 * @class
3212 * @extends OO.ui.Element
3213 * @mixins OO.EventEmitter
3214 *
3215 * @constructor
3216 * @param {Object} [config] Configuration options
3217 * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
3218 * Note that window classes that are instantiated with a factory must have
3219 * a {@link OO.ui.Dialog#static-name static name} property that specifies a symbolic name.
3220 * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
3221 */
3222 OO.ui.WindowManager = function OoUiWindowManager( config ) {
3223 // Configuration initialization
3224 config = config || {};
3225
3226 // Parent constructor
3227 OO.ui.WindowManager.parent.call( this, config );
3228
3229 // Mixin constructors
3230 OO.EventEmitter.call( this );
3231
3232 // Properties
3233 this.factory = config.factory;
3234 this.modal = config.modal === undefined || !!config.modal;
3235 this.windows = {};
3236 this.opening = null;
3237 this.opened = null;
3238 this.closing = null;
3239 this.preparingToOpen = null;
3240 this.preparingToClose = null;
3241 this.currentWindow = null;
3242 this.globalEvents = false;
3243 this.$ariaHidden = null;
3244 this.onWindowResizeTimeout = null;
3245 this.onWindowResizeHandler = this.onWindowResize.bind( this );
3246 this.afterWindowResizeHandler = this.afterWindowResize.bind( this );
3247
3248 // Initialization
3249 this.$element
3250 .addClass( 'oo-ui-windowManager' )
3251 .toggleClass( 'oo-ui-windowManager-modal', this.modal );
3252 };
3253
3254 /* Setup */
3255
3256 OO.inheritClass( OO.ui.WindowManager, OO.ui.Element );
3257 OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter );
3258
3259 /* Events */
3260
3261 /**
3262 * An 'opening' event is emitted when the window begins to be opened.
3263 *
3264 * @event opening
3265 * @param {OO.ui.Window} win Window that's being opened
3266 * @param {jQuery.Promise} opening An `opening` promise resolved with a value when the window is opened successfully.
3267 * When the `opening` promise is resolved, the first argument of the value is an 'opened' promise, the second argument
3268 * is the opening data. The `opening` promise emits `setup` and `ready` notifications when those processes are complete.
3269 * @param {Object} data Window opening data
3270 */
3271
3272 /**
3273 * A 'closing' event is emitted when the window begins to be closed.
3274 *
3275 * @event closing
3276 * @param {OO.ui.Window} win Window that's being closed
3277 * @param {jQuery.Promise} closing A `closing` promise is resolved with a value when the window
3278 * is closed successfully. The promise emits `hold` and `teardown` notifications when those
3279 * processes are complete. When the `closing` promise is resolved, the first argument of its value
3280 * is the closing data.
3281 * @param {Object} data Window closing data
3282 */
3283
3284 /**
3285 * A 'resize' event is emitted when a window is resized.
3286 *
3287 * @event resize
3288 * @param {OO.ui.Window} win Window that was resized
3289 */
3290
3291 /* Static Properties */
3292
3293 /**
3294 * Map of the symbolic name of each window size and its CSS properties.
3295 *
3296 * @static
3297 * @inheritable
3298 * @property {Object}
3299 */
3300 OO.ui.WindowManager.static.sizes = {
3301 small: {
3302 width: 300
3303 },
3304 medium: {
3305 width: 500
3306 },
3307 large: {
3308 width: 700
3309 },
3310 larger: {
3311 width: 900
3312 },
3313 full: {
3314 // These can be non-numeric because they are never used in calculations
3315 width: '100%',
3316 height: '100%'
3317 }
3318 };
3319
3320 /**
3321 * Symbolic name of the default window size.
3322 *
3323 * The default size is used if the window's requested size is not recognized.
3324 *
3325 * @static
3326 * @inheritable
3327 * @property {string}
3328 */
3329 OO.ui.WindowManager.static.defaultSize = 'medium';
3330
3331 /* Methods */
3332
3333 /**
3334 * Handle window resize events.
3335 *
3336 * @private
3337 * @param {jQuery.Event} e Window resize event
3338 */
3339 OO.ui.WindowManager.prototype.onWindowResize = function () {
3340 clearTimeout( this.onWindowResizeTimeout );
3341 this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 );
3342 };
3343
3344 /**
3345 * Handle window resize events.
3346 *
3347 * @private
3348 * @param {jQuery.Event} e Window resize event
3349 */
3350 OO.ui.WindowManager.prototype.afterWindowResize = function () {
3351 if ( this.currentWindow ) {
3352 this.updateWindowSize( this.currentWindow );
3353 }
3354 };
3355
3356 /**
3357 * Check if window is opening.
3358 *
3359 * @return {boolean} Window is opening
3360 */
3361 OO.ui.WindowManager.prototype.isOpening = function ( win ) {
3362 return win === this.currentWindow && !!this.opening && this.opening.state() === 'pending';
3363 };
3364
3365 /**
3366 * Check if window is closing.
3367 *
3368 * @return {boolean} Window is closing
3369 */
3370 OO.ui.WindowManager.prototype.isClosing = function ( win ) {
3371 return win === this.currentWindow && !!this.closing && this.closing.state() === 'pending';
3372 };
3373
3374 /**
3375 * Check if window is opened.
3376 *
3377 * @return {boolean} Window is opened
3378 */
3379 OO.ui.WindowManager.prototype.isOpened = function ( win ) {
3380 return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending';
3381 };
3382
3383 /**
3384 * Check if a window is being managed.
3385 *
3386 * @param {OO.ui.Window} win Window to check
3387 * @return {boolean} Window is being managed
3388 */
3389 OO.ui.WindowManager.prototype.hasWindow = function ( win ) {
3390 var name;
3391
3392 for ( name in this.windows ) {
3393 if ( this.windows[ name ] === win ) {
3394 return true;
3395 }
3396 }
3397
3398 return false;
3399 };
3400
3401 /**
3402 * Get the number of milliseconds to wait after opening begins before executing the ‘setup’ process.
3403 *
3404 * @param {OO.ui.Window} win Window being opened
3405 * @param {Object} [data] Window opening data
3406 * @return {number} Milliseconds to wait
3407 */
3408 OO.ui.WindowManager.prototype.getSetupDelay = function () {
3409 return 0;
3410 };
3411
3412 /**
3413 * Get the number of milliseconds to wait after setup has finished before executing the ‘ready’ process.
3414 *
3415 * @param {OO.ui.Window} win Window being opened
3416 * @param {Object} [data] Window opening data
3417 * @return {number} Milliseconds to wait
3418 */
3419 OO.ui.WindowManager.prototype.getReadyDelay = function () {
3420 return 0;
3421 };
3422
3423 /**
3424 * Get the number of milliseconds to wait after closing has begun before executing the 'hold' process.
3425 *
3426 * @param {OO.ui.Window} win Window being closed
3427 * @param {Object} [data] Window closing data
3428 * @return {number} Milliseconds to wait
3429 */
3430 OO.ui.WindowManager.prototype.getHoldDelay = function () {
3431 return 0;
3432 };
3433
3434 /**
3435 * Get the number of milliseconds to wait after the ‘hold’ process has finished before
3436 * executing the ‘teardown’ process.
3437 *
3438 * @param {OO.ui.Window} win Window being closed
3439 * @param {Object} [data] Window closing data
3440 * @return {number} Milliseconds to wait
3441 */
3442 OO.ui.WindowManager.prototype.getTeardownDelay = function () {
3443 return this.modal ? 250 : 0;
3444 };
3445
3446 /**
3447 * Get a window by its symbolic name.
3448 *
3449 * If the window is not yet instantiated and its symbolic name is recognized by a factory, it will be
3450 * instantiated and added to the window manager automatically. Please see the [OOjs UI documentation on MediaWiki][3]
3451 * for more information about using factories.
3452 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
3453 *
3454 * @param {string} name Symbolic name of the window
3455 * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
3456 * @throws {Error} An error is thrown if the symbolic name is not recognized by the factory.
3457 * @throws {Error} An error is thrown if the named window is not recognized as a managed window.
3458 */
3459 OO.ui.WindowManager.prototype.getWindow = function ( name ) {
3460 var deferred = $.Deferred(),
3461 win = this.windows[ name ];
3462
3463 if ( !( win instanceof OO.ui.Window ) ) {
3464 if ( this.factory ) {
3465 if ( !this.factory.lookup( name ) ) {
3466 deferred.reject( new OO.ui.Error(
3467 'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
3468 ) );
3469 } else {
3470 win = this.factory.create( name );
3471 this.addWindows( [ win ] );
3472 deferred.resolve( win );
3473 }
3474 } else {
3475 deferred.reject( new OO.ui.Error(
3476 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
3477 ) );
3478 }
3479 } else {
3480 deferred.resolve( win );
3481 }
3482
3483 return deferred.promise();
3484 };
3485
3486 /**
3487 * Get current window.
3488 *
3489 * @return {OO.ui.Window|null} Currently opening/opened/closing window
3490 */
3491 OO.ui.WindowManager.prototype.getCurrentWindow = function () {
3492 return this.currentWindow;
3493 };
3494
3495 /**
3496 * Open a window.
3497 *
3498 * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
3499 * @param {Object} [data] Window opening data
3500 * @return {jQuery.Promise} An `opening` promise resolved when the window is done opening.
3501 * See {@link #event-opening 'opening' event} for more information about `opening` promises.
3502 * @fires opening
3503 */
3504 OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
3505 var manager = this,
3506 opening = $.Deferred();
3507
3508 // Argument handling
3509 if ( typeof win === 'string' ) {
3510 return this.getWindow( win ).then( function ( win ) {
3511 return manager.openWindow( win, data );
3512 } );
3513 }
3514
3515 // Error handling
3516 if ( !this.hasWindow( win ) ) {
3517 opening.reject( new OO.ui.Error(
3518 'Cannot open window: window is not attached to manager'
3519 ) );
3520 } else if ( this.preparingToOpen || this.opening || this.opened ) {
3521 opening.reject( new OO.ui.Error(
3522 'Cannot open window: another window is opening or open'
3523 ) );
3524 }
3525
3526 // Window opening
3527 if ( opening.state() !== 'rejected' ) {
3528 // If a window is currently closing, wait for it to complete
3529 this.preparingToOpen = $.when( this.closing );
3530 // Ensure handlers get called after preparingToOpen is set
3531 this.preparingToOpen.done( function () {
3532 if ( manager.modal ) {
3533 manager.toggleGlobalEvents( true );
3534 manager.toggleAriaIsolation( true );
3535 }
3536 manager.currentWindow = win;
3537 manager.opening = opening;
3538 manager.preparingToOpen = null;
3539 manager.emit( 'opening', win, opening, data );
3540 setTimeout( function () {
3541 win.setup( data ).then( function () {
3542 manager.updateWindowSize( win );
3543 manager.opening.notify( { state: 'setup' } );
3544 setTimeout( function () {
3545 win.ready( data ).then( function () {
3546 manager.opening.notify( { state: 'ready' } );
3547 manager.opening = null;
3548 manager.opened = $.Deferred();
3549 opening.resolve( manager.opened.promise(), data );
3550 }, function () {
3551 manager.opening = null;
3552 manager.opened = $.Deferred();
3553 opening.reject();
3554 manager.closeWindow( win );
3555 } );
3556 }, manager.getReadyDelay() );
3557 }, function () {
3558 manager.opening = null;
3559 manager.opened = $.Deferred();
3560 opening.reject();
3561 manager.closeWindow( win );
3562 } );
3563 }, manager.getSetupDelay() );
3564 } );
3565 }
3566
3567 return opening.promise();
3568 };
3569
3570 /**
3571 * Close a window.
3572 *
3573 * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
3574 * @param {Object} [data] Window closing data
3575 * @return {jQuery.Promise} A `closing` promise resolved when the window is done closing.
3576 * See {@link #event-closing 'closing' event} for more information about closing promises.
3577 * @throws {Error} An error is thrown if the window is not managed by the window manager.
3578 * @fires closing
3579 */
3580 OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
3581 var manager = this,
3582 closing = $.Deferred(),
3583 opened;
3584
3585 // Argument handling
3586 if ( typeof win === 'string' ) {
3587 win = this.windows[ win ];
3588 } else if ( !this.hasWindow( win ) ) {
3589 win = null;
3590 }
3591
3592 // Error handling
3593 if ( !win ) {
3594 closing.reject( new OO.ui.Error(
3595 'Cannot close window: window is not attached to manager'
3596 ) );
3597 } else if ( win !== this.currentWindow ) {
3598 closing.reject( new OO.ui.Error(
3599 'Cannot close window: window already closed with different data'
3600 ) );
3601 } else if ( this.preparingToClose || this.closing ) {
3602 closing.reject( new OO.ui.Error(
3603 'Cannot close window: window already closing with different data'
3604 ) );
3605 }
3606
3607 // Window closing
3608 if ( closing.state() !== 'rejected' ) {
3609 // If the window is currently opening, close it when it's done
3610 this.preparingToClose = $.when( this.opening );
3611 // Ensure handlers get called after preparingToClose is set
3612 this.preparingToClose.always( function () {
3613 manager.closing = closing;
3614 manager.preparingToClose = null;
3615 manager.emit( 'closing', win, closing, data );
3616 opened = manager.opened;
3617 manager.opened = null;
3618 opened.resolve( closing.promise(), data );
3619 setTimeout( function () {
3620 win.hold( data ).then( function () {
3621 closing.notify( { state: 'hold' } );
3622 setTimeout( function () {
3623 win.teardown( data ).then( function () {
3624 closing.notify( { state: 'teardown' } );
3625 if ( manager.modal ) {
3626 manager.toggleGlobalEvents( false );
3627 manager.toggleAriaIsolation( false );
3628 }
3629 manager.closing = null;
3630 manager.currentWindow = null;
3631 closing.resolve( data );
3632 } );
3633 }, manager.getTeardownDelay() );
3634 } );
3635 }, manager.getHoldDelay() );
3636 } );
3637 }
3638
3639 return closing.promise();
3640 };
3641
3642 /**
3643 * Add windows to the window manager.
3644 *
3645 * Windows can be added by reference, symbolic name, or explicitly defined symbolic names.
3646 * See the [OOjs ui documentation on MediaWiki] [2] for examples.
3647 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
3648 *
3649 * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows An array of window objects specified
3650 * by reference, symbolic name, or explicitly defined symbolic names.
3651 * @throws {Error} An error is thrown if a window is added by symbolic name, but has neither an
3652 * explicit nor a statically configured symbolic name.
3653 */
3654 OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
3655 var i, len, win, name, list;
3656
3657 if ( Array.isArray( windows ) ) {
3658 // Convert to map of windows by looking up symbolic names from static configuration
3659 list = {};
3660 for ( i = 0, len = windows.length; i < len; i++ ) {
3661 name = windows[ i ].constructor.static.name;
3662 if ( typeof name !== 'string' ) {
3663 throw new Error( 'Cannot add window' );
3664 }
3665 list[ name ] = windows[ i ];
3666 }
3667 } else if ( OO.isPlainObject( windows ) ) {
3668 list = windows;
3669 }
3670
3671 // Add windows
3672 for ( name in list ) {
3673 win = list[ name ];
3674 this.windows[ name ] = win.toggle( false );
3675 this.$element.append( win.$element );
3676 win.setManager( this );
3677 }
3678 };
3679
3680 /**
3681 * Remove the specified windows from the windows manager.
3682 *
3683 * Windows will be closed before they are removed. If you wish to remove all windows, you may wish to use
3684 * the #clearWindows method instead. If you no longer need the window manager and want to ensure that it no
3685 * longer listens to events, use the #destroy method.
3686 *
3687 * @param {string[]} names Symbolic names of windows to remove
3688 * @return {jQuery.Promise} Promise resolved when window is closed and removed
3689 * @throws {Error} An error is thrown if the named windows are not managed by the window manager.
3690 */
3691 OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
3692 var i, len, win, name, cleanupWindow,
3693 manager = this,
3694 promises = [],
3695 cleanup = function ( name, win ) {
3696 delete manager.windows[ name ];
3697 win.$element.detach();
3698 };
3699
3700 for ( i = 0, len = names.length; i < len; i++ ) {
3701 name = names[ i ];
3702 win = this.windows[ name ];
3703 if ( !win ) {
3704 throw new Error( 'Cannot remove window' );
3705 }
3706 cleanupWindow = cleanup.bind( null, name, win );
3707 promises.push( this.closeWindow( name ).then( cleanupWindow, cleanupWindow ) );
3708 }
3709
3710 return $.when.apply( $, promises );
3711 };
3712
3713 /**
3714 * Remove all windows from the window manager.
3715 *
3716 * Windows will be closed before they are removed. Note that the window manager, though not in use, will still
3717 * listen to events. If the window manager will not be used again, you may wish to use the #destroy method instead.
3718 * To remove just a subset of windows, use the #removeWindows method.
3719 *
3720 * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
3721 */
3722 OO.ui.WindowManager.prototype.clearWindows = function () {
3723 return this.removeWindows( Object.keys( this.windows ) );
3724 };
3725
3726 /**
3727 * Set dialog size. In general, this method should not be called directly.
3728 *
3729 * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
3730 *
3731 * @chainable
3732 */
3733 OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) {
3734 var isFullscreen;
3735
3736 // Bypass for non-current, and thus invisible, windows
3737 if ( win !== this.currentWindow ) {
3738 return;
3739 }
3740
3741 isFullscreen = win.getSize() === 'full';
3742
3743 this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', isFullscreen );
3744 this.$element.toggleClass( 'oo-ui-windowManager-floating', !isFullscreen );
3745 win.setDimensions( win.getSizeProperties() );
3746
3747 this.emit( 'resize', win );
3748
3749 return this;
3750 };
3751
3752 /**
3753 * Bind or unbind global events for scrolling.
3754 *
3755 * @private
3756 * @param {boolean} [on] Bind global events
3757 * @chainable
3758 */
3759 OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) {
3760 var scrollWidth, bodyMargin,
3761 $body = $( this.getElementDocument().body ),
3762 // We could have multiple window managers open so only modify
3763 // the body css at the bottom of the stack
3764 stackDepth = $body.data( 'windowManagerGlobalEvents' ) || 0 ;
3765
3766 on = on === undefined ? !!this.globalEvents : !!on;
3767
3768 if ( on ) {
3769 if ( !this.globalEvents ) {
3770 $( this.getElementWindow() ).on( {
3771 // Start listening for top-level window dimension changes
3772 'orientationchange resize': this.onWindowResizeHandler
3773 } );
3774 if ( stackDepth === 0 ) {
3775 scrollWidth = window.innerWidth - document.documentElement.clientWidth;
3776 bodyMargin = parseFloat( $body.css( 'margin-right' ) ) || 0;
3777 $body.css( {
3778 overflow: 'hidden',
3779 'margin-right': bodyMargin + scrollWidth
3780 } );
3781 }
3782 stackDepth++;
3783 this.globalEvents = true;
3784 }
3785 } else if ( this.globalEvents ) {
3786 $( this.getElementWindow() ).off( {
3787 // Stop listening for top-level window dimension changes
3788 'orientationchange resize': this.onWindowResizeHandler
3789 } );
3790 stackDepth--;
3791 if ( stackDepth === 0 ) {
3792 $body.css( {
3793 overflow: '',
3794 'margin-right': ''
3795 } );
3796 }
3797 this.globalEvents = false;
3798 }
3799 $body.data( 'windowManagerGlobalEvents', stackDepth );
3800
3801 return this;
3802 };
3803
3804 /**
3805 * Toggle screen reader visibility of content other than the window manager.
3806 *
3807 * @private
3808 * @param {boolean} [isolate] Make only the window manager visible to screen readers
3809 * @chainable
3810 */
3811 OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) {
3812 isolate = isolate === undefined ? !this.$ariaHidden : !!isolate;
3813
3814 if ( isolate ) {
3815 if ( !this.$ariaHidden ) {
3816 // Hide everything other than the window manager from screen readers
3817 this.$ariaHidden = $( 'body' )
3818 .children()
3819 .not( this.$element.parentsUntil( 'body' ).last() )
3820 .attr( 'aria-hidden', '' );
3821 }
3822 } else if ( this.$ariaHidden ) {
3823 // Restore screen reader visibility
3824 this.$ariaHidden.removeAttr( 'aria-hidden' );
3825 this.$ariaHidden = null;
3826 }
3827
3828 return this;
3829 };
3830
3831 /**
3832 * Destroy the window manager.
3833 *
3834 * Destroying the window manager ensures that it will no longer listen to events. If you would like to
3835 * continue using the window manager, but wish to remove all windows from it, use the #clearWindows method
3836 * instead.
3837 */
3838 OO.ui.WindowManager.prototype.destroy = function () {
3839 this.toggleGlobalEvents( false );
3840 this.toggleAriaIsolation( false );
3841 this.clearWindows();
3842 this.$element.remove();
3843 };
3844
3845 /**
3846 * Errors contain a required message (either a string or jQuery selection) that is used to describe what went wrong
3847 * in a {@link OO.ui.Process process}. The error's #recoverable and #warning configurations are used to customize the
3848 * appearance and functionality of the error interface.
3849 *
3850 * The basic error interface contains a formatted error message as well as two buttons: 'Dismiss' and 'Try again' (i.e., the error
3851 * is 'recoverable' by default). If the error is not recoverable, the 'Try again' button will not be rendered and the widget
3852 * that initiated the failed process will be disabled.
3853 *
3854 * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button, which will try the
3855 * process again.
3856 *
3857 * For an example of error interfaces, please see the [OOjs UI documentation on MediaWiki][1].
3858 *
3859 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Processes_and_errors
3860 *
3861 * @class
3862 *
3863 * @constructor
3864 * @param {string|jQuery} message Description of error
3865 * @param {Object} [config] Configuration options
3866 * @cfg {boolean} [recoverable=true] Error is recoverable.
3867 * By default, errors are recoverable, and users can try the process again.
3868 * @cfg {boolean} [warning=false] Error is a warning.
3869 * If the error is a warning, the error interface will include a
3870 * 'Dismiss' and a 'Continue' button. It is the responsibility of the developer to ensure that the warning
3871 * is not triggered a second time if the user chooses to continue.
3872 */
3873 OO.ui.Error = function OoUiError( message, config ) {
3874 // Allow passing positional parameters inside the config object
3875 if ( OO.isPlainObject( message ) && config === undefined ) {
3876 config = message;
3877 message = config.message;
3878 }
3879
3880 // Configuration initialization
3881 config = config || {};
3882
3883 // Properties
3884 this.message = message instanceof jQuery ? message : String( message );
3885 this.recoverable = config.recoverable === undefined || !!config.recoverable;
3886 this.warning = !!config.warning;
3887 };
3888
3889 /* Setup */
3890
3891 OO.initClass( OO.ui.Error );
3892
3893 /* Methods */
3894
3895 /**
3896 * Check if the error is recoverable.
3897 *
3898 * If the error is recoverable, users are able to try the process again.
3899 *
3900 * @return {boolean} Error is recoverable
3901 */
3902 OO.ui.Error.prototype.isRecoverable = function () {
3903 return this.recoverable;
3904 };
3905
3906 /**
3907 * Check if the error is a warning.
3908 *
3909 * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button.
3910 *
3911 * @return {boolean} Error is warning
3912 */
3913 OO.ui.Error.prototype.isWarning = function () {
3914 return this.warning;
3915 };
3916
3917 /**
3918 * Get error message as DOM nodes.
3919 *
3920 * @return {jQuery} Error message in DOM nodes
3921 */
3922 OO.ui.Error.prototype.getMessage = function () {
3923 return this.message instanceof jQuery ?
3924 this.message.clone() :
3925 $( '<div>' ).text( this.message ).contents();
3926 };
3927
3928 /**
3929 * Get the error message text.
3930 *
3931 * @return {string} Error message
3932 */
3933 OO.ui.Error.prototype.getMessageText = function () {
3934 return this.message instanceof jQuery ? this.message.text() : this.message;
3935 };
3936
3937 /**
3938 * Wraps an HTML snippet for use with configuration values which default
3939 * to strings. This bypasses the default html-escaping done to string
3940 * values.
3941 *
3942 * @class
3943 *
3944 * @constructor
3945 * @param {string} [content] HTML content
3946 */
3947 OO.ui.HtmlSnippet = function OoUiHtmlSnippet( content ) {
3948 // Properties
3949 this.content = content;
3950 };
3951
3952 /* Setup */
3953
3954 OO.initClass( OO.ui.HtmlSnippet );
3955
3956 /* Methods */
3957
3958 /**
3959 * Render into HTML.
3960 *
3961 * @return {string} Unchanged HTML snippet.
3962 */
3963 OO.ui.HtmlSnippet.prototype.toString = function () {
3964 return this.content;
3965 };
3966
3967 /**
3968 * A Process is a list of steps that are called in sequence. The step can be a number, a jQuery promise,
3969 * or a function:
3970 *
3971 * - **number**: the process will wait for the specified number of milliseconds before proceeding.
3972 * - **promise**: the process will continue to the next step when the promise is successfully resolved
3973 * or stop if the promise is rejected.
3974 * - **function**: the process will execute the function. The process will stop if the function returns
3975 * either a boolean `false` or a promise that is rejected; if the function returns a number, the process
3976 * will wait for that number of milliseconds before proceeding.
3977 *
3978 * If the process fails, an {@link OO.ui.Error error} is generated. Depending on how the error is
3979 * configured, users can dismiss the error and try the process again, or not. If a process is stopped,
3980 * its remaining steps will not be performed.
3981 *
3982 * @class
3983 *
3984 * @constructor
3985 * @param {number|jQuery.Promise|Function} step Number of miliseconds to wait before proceeding, promise
3986 * that must be resolved before proceeding, or a function to execute. See #createStep for more information. see #createStep for more information
3987 * @param {Object} [context=null] Execution context of the function. The context is ignored if the step is
3988 * a number or promise.
3989 * @return {Object} Step object, with `callback` and `context` properties
3990 */
3991 OO.ui.Process = function ( step, context ) {
3992 // Properties
3993 this.steps = [];
3994
3995 // Initialization
3996 if ( step !== undefined ) {
3997 this.next( step, context );
3998 }
3999 };
4000
4001 /* Setup */
4002
4003 OO.initClass( OO.ui.Process );
4004
4005 /* Methods */
4006
4007 /**
4008 * Start the process.
4009 *
4010 * @return {jQuery.Promise} Promise that is resolved when all steps have successfully completed.
4011 * If any of the steps return a promise that is rejected or a boolean false, this promise is rejected
4012 * and any remaining steps are not performed.
4013 */
4014 OO.ui.Process.prototype.execute = function () {
4015 var i, len, promise;
4016
4017 /**
4018 * Continue execution.
4019 *
4020 * @ignore
4021 * @param {Array} step A function and the context it should be called in
4022 * @return {Function} Function that continues the process
4023 */
4024 function proceed( step ) {
4025 return function () {
4026 // Execute step in the correct context
4027 var deferred,
4028 result = step.callback.call( step.context );
4029
4030 if ( result === false ) {
4031 // Use rejected promise for boolean false results
4032 return $.Deferred().reject( [] ).promise();
4033 }
4034 if ( typeof result === 'number' ) {
4035 if ( result < 0 ) {
4036 throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
4037 }
4038 // Use a delayed promise for numbers, expecting them to be in milliseconds
4039 deferred = $.Deferred();
4040 setTimeout( deferred.resolve, result );
4041 return deferred.promise();
4042 }
4043 if ( result instanceof OO.ui.Error ) {
4044 // Use rejected promise for error
4045 return $.Deferred().reject( [ result ] ).promise();
4046 }
4047 if ( Array.isArray( result ) && result.length && result[ 0 ] instanceof OO.ui.Error ) {
4048 // Use rejected promise for list of errors
4049 return $.Deferred().reject( result ).promise();
4050 }
4051 // Duck-type the object to see if it can produce a promise
4052 if ( result && $.isFunction( result.promise ) ) {
4053 // Use a promise generated from the result
4054 return result.promise();
4055 }
4056 // Use resolved promise for other results
4057 return $.Deferred().resolve().promise();
4058 };
4059 }
4060
4061 if ( this.steps.length ) {
4062 // Generate a chain reaction of promises
4063 promise = proceed( this.steps[ 0 ] )();
4064 for ( i = 1, len = this.steps.length; i < len; i++ ) {
4065 promise = promise.then( proceed( this.steps[ i ] ) );
4066 }
4067 } else {
4068 promise = $.Deferred().resolve().promise();
4069 }
4070
4071 return promise;
4072 };
4073
4074 /**
4075 * Create a process step.
4076 *
4077 * @private
4078 * @param {number|jQuery.Promise|Function} step
4079 *
4080 * - Number of milliseconds to wait before proceeding
4081 * - Promise that must be resolved before proceeding
4082 * - Function to execute
4083 * - If the function returns a boolean false the process will stop
4084 * - If the function returns a promise, the process will continue to the next
4085 * step when the promise is resolved or stop if the promise is rejected
4086 * - If the function returns a number, the process will wait for that number of
4087 * milliseconds before proceeding
4088 * @param {Object} [context=null] Execution context of the function. The context is
4089 * ignored if the step is a number or promise.
4090 * @return {Object} Step object, with `callback` and `context` properties
4091 */
4092 OO.ui.Process.prototype.createStep = function ( step, context ) {
4093 if ( typeof step === 'number' || $.isFunction( step.promise ) ) {
4094 return {
4095 callback: function () {
4096 return step;
4097 },
4098 context: null
4099 };
4100 }
4101 if ( $.isFunction( step ) ) {
4102 return {
4103 callback: step,
4104 context: context
4105 };
4106 }
4107 throw new Error( 'Cannot create process step: number, promise or function expected' );
4108 };
4109
4110 /**
4111 * Add step to the beginning of the process.
4112 *
4113 * @inheritdoc #createStep
4114 * @return {OO.ui.Process} this
4115 * @chainable
4116 */
4117 OO.ui.Process.prototype.first = function ( step, context ) {
4118 this.steps.unshift( this.createStep( step, context ) );
4119 return this;
4120 };
4121
4122 /**
4123 * Add step to the end of the process.
4124 *
4125 * @inheritdoc #createStep
4126 * @return {OO.ui.Process} this
4127 * @chainable
4128 */
4129 OO.ui.Process.prototype.next = function ( step, context ) {
4130 this.steps.push( this.createStep( step, context ) );
4131 return this;
4132 };
4133
4134 /**
4135 * A ToolFactory creates tools on demand. All tools ({@link OO.ui.Tool Tools}, {@link OO.ui.PopupTool PopupTools},
4136 * and {@link OO.ui.ToolGroupTool ToolGroupTools}) must be registered with a tool factory. Tools are
4137 * registered by their symbolic name. See {@link OO.ui.Toolbar toolbars} for an example.
4138 *
4139 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
4140 *
4141 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
4142 *
4143 * @class
4144 * @extends OO.Factory
4145 * @constructor
4146 */
4147 OO.ui.ToolFactory = function OoUiToolFactory() {
4148 // Parent constructor
4149 OO.ui.ToolFactory.parent.call( this );
4150 };
4151
4152 /* Setup */
4153
4154 OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
4155
4156 /* Methods */
4157
4158 /**
4159 * Get tools from the factory
4160 *
4161 * @param {Array|string} [include] Included tools, see #extract for format
4162 * @param {Array|string} [exclude] Excluded tools, see #extract for format
4163 * @param {Array|string} [promote] Promoted tools, see #extract for format
4164 * @param {Array|string} [demote] Demoted tools, see #extract for format
4165 * @return {string[]} List of tools
4166 */
4167 OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
4168 var i, len, included, promoted, demoted,
4169 auto = [],
4170 used = {};
4171
4172 // Collect included and not excluded tools
4173 included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
4174
4175 // Promotion
4176 promoted = this.extract( promote, used );
4177 demoted = this.extract( demote, used );
4178
4179 // Auto
4180 for ( i = 0, len = included.length; i < len; i++ ) {
4181 if ( !used[ included[ i ] ] ) {
4182 auto.push( included[ i ] );
4183 }
4184 }
4185
4186 return promoted.concat( auto ).concat( demoted );
4187 };
4188
4189 /**
4190 * Get a flat list of names from a list of names or groups.
4191 *
4192 * Normally, `collection` is an array of tool specifications. Tools can be specified in the
4193 * following ways:
4194 *
4195 * - To include an individual tool, use the symbolic name: `{ name: 'tool-name' }` or `'tool-name'`.
4196 * - To include all tools in a group, use the group name: `{ group: 'group-name' }`. (To assign the
4197 * tool to a group, use OO.ui.Tool.static.group.)
4198 *
4199 * Alternatively, to include all tools that are not yet assigned to any other toolgroup, use the
4200 * catch-all selector `'*'`.
4201 *
4202 * If `used` is passed, tool names that appear as properties in this object will be considered
4203 * already assigned, and will not be returned even if specified otherwise. The tool names extracted
4204 * by this function call will be added as new properties in the object.
4205 *
4206 * @private
4207 * @param {Array|string} collection List of tools, see above
4208 * @param {Object} [used] Object containing information about used tools, see above
4209 * @return {string[]} List of extracted tool names
4210 */
4211 OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
4212 var i, len, item, name, tool,
4213 names = [];
4214
4215 if ( collection === '*' ) {
4216 for ( name in this.registry ) {
4217 tool = this.registry[ name ];
4218 if (
4219 // Only add tools by group name when auto-add is enabled
4220 tool.static.autoAddToCatchall &&
4221 // Exclude already used tools
4222 ( !used || !used[ name ] )
4223 ) {
4224 names.push( name );
4225 if ( used ) {
4226 used[ name ] = true;
4227 }
4228 }
4229 }
4230 } else if ( Array.isArray( collection ) ) {
4231 for ( i = 0, len = collection.length; i < len; i++ ) {
4232 item = collection[ i ];
4233 // Allow plain strings as shorthand for named tools
4234 if ( typeof item === 'string' ) {
4235 item = { name: item };
4236 }
4237 if ( OO.isPlainObject( item ) ) {
4238 if ( item.group ) {
4239 for ( name in this.registry ) {
4240 tool = this.registry[ name ];
4241 if (
4242 // Include tools with matching group
4243 tool.static.group === item.group &&
4244 // Only add tools by group name when auto-add is enabled
4245 tool.static.autoAddToGroup &&
4246 // Exclude already used tools
4247 ( !used || !used[ name ] )
4248 ) {
4249 names.push( name );
4250 if ( used ) {
4251 used[ name ] = true;
4252 }
4253 }
4254 }
4255 // Include tools with matching name and exclude already used tools
4256 } else if ( item.name && ( !used || !used[ item.name ] ) ) {
4257 names.push( item.name );
4258 if ( used ) {
4259 used[ item.name ] = true;
4260 }
4261 }
4262 }
4263 }
4264 }
4265 return names;
4266 };
4267
4268 /**
4269 * ToolGroupFactories create {@link OO.ui.ToolGroup toolgroups} on demand. The toolgroup classes must
4270 * specify a symbolic name and be registered with the factory. The following classes are registered by
4271 * default:
4272 *
4273 * - {@link OO.ui.BarToolGroup BarToolGroups} (‘bar’)
4274 * - {@link OO.ui.MenuToolGroup MenuToolGroups} (‘menu’)
4275 * - {@link OO.ui.ListToolGroup ListToolGroups} (‘list’)
4276 *
4277 * See {@link OO.ui.Toolbar toolbars} for an example.
4278 *
4279 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
4280 *
4281 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
4282 * @class
4283 * @extends OO.Factory
4284 * @constructor
4285 */
4286 OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() {
4287 var i, l, defaultClasses;
4288 // Parent constructor
4289 OO.Factory.call( this );
4290
4291 defaultClasses = this.constructor.static.getDefaultClasses();
4292
4293 // Register default toolgroups
4294 for ( i = 0, l = defaultClasses.length; i < l; i++ ) {
4295 this.register( defaultClasses[ i ] );
4296 }
4297 };
4298
4299 /* Setup */
4300
4301 OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory );
4302
4303 /* Static Methods */
4304
4305 /**
4306 * Get a default set of classes to be registered on construction.
4307 *
4308 * @return {Function[]} Default classes
4309 */
4310 OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
4311 return [
4312 OO.ui.BarToolGroup,
4313 OO.ui.ListToolGroup,
4314 OO.ui.MenuToolGroup
4315 ];
4316 };
4317
4318 /**
4319 * Theme logic.
4320 *
4321 * @abstract
4322 * @class
4323 *
4324 * @constructor
4325 * @param {Object} [config] Configuration options
4326 */
4327 OO.ui.Theme = function OoUiTheme( config ) {
4328 // Configuration initialization
4329 config = config || {};
4330 };
4331
4332 /* Setup */
4333
4334 OO.initClass( OO.ui.Theme );
4335
4336 /* Methods */
4337
4338 /**
4339 * Get a list of classes to be applied to a widget.
4340 *
4341 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
4342 * otherwise state transitions will not work properly.
4343 *
4344 * @param {OO.ui.Element} element Element for which to get classes
4345 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
4346 */
4347 OO.ui.Theme.prototype.getElementClasses = function () {
4348 return { on: [], off: [] };
4349 };
4350
4351 /**
4352 * Update CSS classes provided by the theme.
4353 *
4354 * For elements with theme logic hooks, this should be called any time there's a state change.
4355 *
4356 * @param {OO.ui.Element} element Element for which to update classes
4357 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
4358 */
4359 OO.ui.Theme.prototype.updateElementClasses = function ( element ) {
4360 var $elements = $( [] ),
4361 classes = this.getElementClasses( element );
4362
4363 if ( element.$icon ) {
4364 $elements = $elements.add( element.$icon );
4365 }
4366 if ( element.$indicator ) {
4367 $elements = $elements.add( element.$indicator );
4368 }
4369
4370 $elements
4371 .removeClass( classes.off.join( ' ' ) )
4372 .addClass( classes.on.join( ' ' ) );
4373 };
4374
4375 /**
4376 * RequestManager is a mixin that manages the lifecycle of a promise-backed request for a widget, such as
4377 * the {@link OO.ui.mixin.LookupElement}.
4378 *
4379 * @class
4380 * @abstract
4381 *
4382 * @constructor
4383 */
4384 OO.ui.mixin.RequestManager = function OoUiMixinRequestManager() {
4385 this.requestCache = {};
4386 this.requestQuery = null;
4387 this.requestRequest = null;
4388 };
4389
4390 /* Setup */
4391
4392 OO.initClass( OO.ui.mixin.RequestManager );
4393
4394 /**
4395 * Get request results for the current query.
4396 *
4397 * @return {jQuery.Promise} Promise object which will be passed response data as the first argument of
4398 * the done event. If the request was aborted to make way for a subsequent request, this promise
4399 * may not be rejected, depending on what jQuery feels like doing.
4400 */
4401 OO.ui.mixin.RequestManager.prototype.getRequestData = function () {
4402 var widget = this,
4403 value = this.getRequestQuery(),
4404 deferred = $.Deferred(),
4405 ourRequest;
4406
4407 this.abortRequest();
4408 if ( Object.prototype.hasOwnProperty.call( this.requestCache, value ) ) {
4409 deferred.resolve( this.requestCache[ value ] );
4410 } else {
4411 if ( this.pushPending ) {
4412 this.pushPending();
4413 }
4414 this.requestQuery = value;
4415 ourRequest = this.requestRequest = this.getRequest();
4416 ourRequest
4417 .always( function () {
4418 // We need to pop pending even if this is an old request, otherwise
4419 // the widget will remain pending forever.
4420 // TODO: this assumes that an aborted request will fail or succeed soon after
4421 // being aborted, or at least eventually. It would be nice if we could popPending()
4422 // at abort time, but only if we knew that we hadn't already called popPending()
4423 // for that request.
4424 if ( widget.popPending ) {
4425 widget.popPending();
4426 }
4427 } )
4428 .done( function ( response ) {
4429 // If this is an old request (and aborting it somehow caused it to still succeed),
4430 // ignore its success completely
4431 if ( ourRequest === widget.requestRequest ) {
4432 widget.requestQuery = null;
4433 widget.requestRequest = null;
4434 widget.requestCache[ value ] = widget.getRequestCacheDataFromResponse( response );
4435 deferred.resolve( widget.requestCache[ value ] );
4436 }
4437 } )
4438 .fail( function () {
4439 // If this is an old request (or a request failing because it's being aborted),
4440 // ignore its failure completely
4441 if ( ourRequest === widget.requestRequest ) {
4442 widget.requestQuery = null;
4443 widget.requestRequest = null;
4444 deferred.reject();
4445 }
4446 } );
4447 }
4448 return deferred.promise();
4449 };
4450
4451 /**
4452 * Abort the currently pending request, if any.
4453 *
4454 * @private
4455 */
4456 OO.ui.mixin.RequestManager.prototype.abortRequest = function () {
4457 var oldRequest = this.requestRequest;
4458 if ( oldRequest ) {
4459 // First unset this.requestRequest to the fail handler will notice
4460 // that the request is no longer current
4461 this.requestRequest = null;
4462 this.requestQuery = null;
4463 oldRequest.abort();
4464 }
4465 };
4466
4467 /**
4468 * Get the query to be made.
4469 *
4470 * @protected
4471 * @method
4472 * @abstract
4473 * @return {string} query to be used
4474 */
4475 OO.ui.mixin.RequestManager.prototype.getRequestQuery = null;
4476
4477 /**
4478 * Get a new request object of the current query value.
4479 *
4480 * @protected
4481 * @method
4482 * @abstract
4483 * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
4484 */
4485 OO.ui.mixin.RequestManager.prototype.getRequest = null;
4486
4487 /**
4488 * Pre-process data returned by the request from #getRequest.
4489 *
4490 * The return value of this function will be cached, and any further queries for the given value
4491 * will use the cache rather than doing API requests.
4492 *
4493 * @protected
4494 * @method
4495 * @abstract
4496 * @param {Mixed} response Response from server
4497 * @return {Mixed} Cached result data
4498 */
4499 OO.ui.mixin.RequestManager.prototype.getRequestCacheDataFromResponse = null;
4500
4501 /**
4502 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
4503 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
4504 * order in which users will navigate through the focusable elements via the "tab" key.
4505 *
4506 * @example
4507 * // TabIndexedElement is mixed into the ButtonWidget class
4508 * // to provide a tabIndex property.
4509 * var button1 = new OO.ui.ButtonWidget( {
4510 * label: 'fourth',
4511 * tabIndex: 4
4512 * } );
4513 * var button2 = new OO.ui.ButtonWidget( {
4514 * label: 'second',
4515 * tabIndex: 2
4516 * } );
4517 * var button3 = new OO.ui.ButtonWidget( {
4518 * label: 'third',
4519 * tabIndex: 3
4520 * } );
4521 * var button4 = new OO.ui.ButtonWidget( {
4522 * label: 'first',
4523 * tabIndex: 1
4524 * } );
4525 * $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
4526 *
4527 * @abstract
4528 * @class
4529 *
4530 * @constructor
4531 * @param {Object} [config] Configuration options
4532 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
4533 * the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
4534 * functionality will be applied to it instead.
4535 * @cfg {number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
4536 * order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
4537 * to remove the element from the tab-navigation flow.
4538 */
4539 OO.ui.mixin.TabIndexedElement = function OoUiMixinTabIndexedElement( config ) {
4540 // Configuration initialization
4541 config = $.extend( { tabIndex: 0 }, config );
4542
4543 // Properties
4544 this.$tabIndexed = null;
4545 this.tabIndex = null;
4546
4547 // Events
4548 this.connect( this, { disable: 'onTabIndexedElementDisable' } );
4549
4550 // Initialization
4551 this.setTabIndex( config.tabIndex );
4552 this.setTabIndexedElement( config.$tabIndexed || this.$element );
4553 };
4554
4555 /* Setup */
4556
4557 OO.initClass( OO.ui.mixin.TabIndexedElement );
4558
4559 /* Methods */
4560
4561 /**
4562 * Set the element that should use the tabindex functionality.
4563 *
4564 * This method is used to retarget a tabindex mixin so that its functionality applies
4565 * to the specified element. If an element is currently using the functionality, the mixin’s
4566 * effect on that element is removed before the new element is set up.
4567 *
4568 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
4569 * @chainable
4570 */
4571 OO.ui.mixin.TabIndexedElement.prototype.setTabIndexedElement = function ( $tabIndexed ) {
4572 var tabIndex = this.tabIndex;
4573 // Remove attributes from old $tabIndexed
4574 this.setTabIndex( null );
4575 // Force update of new $tabIndexed
4576 this.$tabIndexed = $tabIndexed;
4577 this.tabIndex = tabIndex;
4578 return this.updateTabIndex();
4579 };
4580
4581 /**
4582 * Set the value of the tabindex.
4583 *
4584 * @param {number|null} tabIndex Tabindex value, or `null` for no tabindex
4585 * @chainable
4586 */
4587 OO.ui.mixin.TabIndexedElement.prototype.setTabIndex = function ( tabIndex ) {
4588 tabIndex = typeof tabIndex === 'number' ? tabIndex : null;
4589
4590 if ( this.tabIndex !== tabIndex ) {
4591 this.tabIndex = tabIndex;
4592 this.updateTabIndex();
4593 }
4594
4595 return this;
4596 };
4597
4598 /**
4599 * Update the `tabindex` attribute, in case of changes to tab index or
4600 * disabled state.
4601 *
4602 * @private
4603 * @chainable
4604 */
4605 OO.ui.mixin.TabIndexedElement.prototype.updateTabIndex = function () {
4606 if ( this.$tabIndexed ) {
4607 if ( this.tabIndex !== null ) {
4608 // Do not index over disabled elements
4609 this.$tabIndexed.attr( {
4610 tabindex: this.isDisabled() ? -1 : this.tabIndex,
4611 // Support: ChromeVox and NVDA
4612 // These do not seem to inherit aria-disabled from parent elements
4613 'aria-disabled': this.isDisabled().toString()
4614 } );
4615 } else {
4616 this.$tabIndexed.removeAttr( 'tabindex aria-disabled' );
4617 }
4618 }
4619 return this;
4620 };
4621
4622 /**
4623 * Handle disable events.
4624 *
4625 * @private
4626 * @param {boolean} disabled Element is disabled
4627 */
4628 OO.ui.mixin.TabIndexedElement.prototype.onTabIndexedElementDisable = function () {
4629 this.updateTabIndex();
4630 };
4631
4632 /**
4633 * Get the value of the tabindex.
4634 *
4635 * @return {number|null} Tabindex value
4636 */
4637 OO.ui.mixin.TabIndexedElement.prototype.getTabIndex = function () {
4638 return this.tabIndex;
4639 };
4640
4641 /**
4642 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
4643 * interface element that can be configured with access keys for accessibility.
4644 * See the [OOjs UI documentation on MediaWiki] [1] for examples.
4645 *
4646 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
4647 * @abstract
4648 * @class
4649 *
4650 * @constructor
4651 * @param {Object} [config] Configuration options
4652 * @cfg {jQuery} [$button] The button element created by the class.
4653 * If this configuration is omitted, the button element will use a generated `<a>`.
4654 * @cfg {boolean} [framed=true] Render the button with a frame
4655 */
4656 OO.ui.mixin.ButtonElement = function OoUiMixinButtonElement( config ) {
4657 // Configuration initialization
4658 config = config || {};
4659
4660 // Properties
4661 this.$button = null;
4662 this.framed = null;
4663 this.active = false;
4664 this.onMouseUpHandler = this.onMouseUp.bind( this );
4665 this.onMouseDownHandler = this.onMouseDown.bind( this );
4666 this.onKeyDownHandler = this.onKeyDown.bind( this );
4667 this.onKeyUpHandler = this.onKeyUp.bind( this );
4668 this.onClickHandler = this.onClick.bind( this );
4669 this.onKeyPressHandler = this.onKeyPress.bind( this );
4670
4671 // Initialization
4672 this.$element.addClass( 'oo-ui-buttonElement' );
4673 this.toggleFramed( config.framed === undefined || config.framed );
4674 this.setButtonElement( config.$button || $( '<a>' ) );
4675 };
4676
4677 /* Setup */
4678
4679 OO.initClass( OO.ui.mixin.ButtonElement );
4680
4681 /* Static Properties */
4682
4683 /**
4684 * Cancel mouse down events.
4685 *
4686 * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
4687 * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
4688 * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
4689 * parent widget.
4690 *
4691 * @static
4692 * @inheritable
4693 * @property {boolean}
4694 */
4695 OO.ui.mixin.ButtonElement.static.cancelButtonMouseDownEvents = true;
4696
4697 /* Events */
4698
4699 /**
4700 * A 'click' event is emitted when the button element is clicked.
4701 *
4702 * @event click
4703 */
4704
4705 /* Methods */
4706
4707 /**
4708 * Set the button element.
4709 *
4710 * This method is used to retarget a button mixin so that its functionality applies to
4711 * the specified button element instead of the one created by the class. If a button element
4712 * is already set, the method will remove the mixin’s effect on that element.
4713 *
4714 * @param {jQuery} $button Element to use as button
4715 */
4716 OO.ui.mixin.ButtonElement.prototype.setButtonElement = function ( $button ) {
4717 if ( this.$button ) {
4718 this.$button
4719 .removeClass( 'oo-ui-buttonElement-button' )
4720 .removeAttr( 'role accesskey' )
4721 .off( {
4722 mousedown: this.onMouseDownHandler,
4723 keydown: this.onKeyDownHandler,
4724 click: this.onClickHandler,
4725 keypress: this.onKeyPressHandler
4726 } );
4727 }
4728
4729 this.$button = $button
4730 .addClass( 'oo-ui-buttonElement-button' )
4731 .attr( { role: 'button' } )
4732 .on( {
4733 mousedown: this.onMouseDownHandler,
4734 keydown: this.onKeyDownHandler,
4735 click: this.onClickHandler,
4736 keypress: this.onKeyPressHandler
4737 } );
4738 };
4739
4740 /**
4741 * Handles mouse down events.
4742 *
4743 * @protected
4744 * @param {jQuery.Event} e Mouse down event
4745 */
4746 OO.ui.mixin.ButtonElement.prototype.onMouseDown = function ( e ) {
4747 if ( this.isDisabled() || e.which !== OO.ui.MouseButtons.LEFT ) {
4748 return;
4749 }
4750 this.$element.addClass( 'oo-ui-buttonElement-pressed' );
4751 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
4752 // reliably remove the pressed class
4753 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
4754 // Prevent change of focus unless specifically configured otherwise
4755 if ( this.constructor.static.cancelButtonMouseDownEvents ) {
4756 return false;
4757 }
4758 };
4759
4760 /**
4761 * Handles mouse up events.
4762 *
4763 * @protected
4764 * @param {jQuery.Event} e Mouse up event
4765 */
4766 OO.ui.mixin.ButtonElement.prototype.onMouseUp = function ( e ) {
4767 if ( this.isDisabled() || e.which !== OO.ui.MouseButtons.LEFT ) {
4768 return;
4769 }
4770 this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
4771 // Stop listening for mouseup, since we only needed this once
4772 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
4773 };
4774
4775 /**
4776 * Handles mouse click events.
4777 *
4778 * @protected
4779 * @param {jQuery.Event} e Mouse click event
4780 * @fires click
4781 */
4782 OO.ui.mixin.ButtonElement.prototype.onClick = function ( e ) {
4783 if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
4784 if ( this.emit( 'click' ) ) {
4785 return false;
4786 }
4787 }
4788 };
4789
4790 /**
4791 * Handles key down events.
4792 *
4793 * @protected
4794 * @param {jQuery.Event} e Key down event
4795 */
4796 OO.ui.mixin.ButtonElement.prototype.onKeyDown = function ( e ) {
4797 if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
4798 return;
4799 }
4800 this.$element.addClass( 'oo-ui-buttonElement-pressed' );
4801 // Run the keyup handler no matter where the key is when the button is let go, so we can
4802 // reliably remove the pressed class
4803 this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler, true );
4804 };
4805
4806 /**
4807 * Handles key up events.
4808 *
4809 * @protected
4810 * @param {jQuery.Event} e Key up event
4811 */
4812 OO.ui.mixin.ButtonElement.prototype.onKeyUp = function ( e ) {
4813 if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
4814 return;
4815 }
4816 this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
4817 // Stop listening for keyup, since we only needed this once
4818 this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler, true );
4819 };
4820
4821 /**
4822 * Handles key press events.
4823 *
4824 * @protected
4825 * @param {jQuery.Event} e Key press event
4826 * @fires click
4827 */
4828 OO.ui.mixin.ButtonElement.prototype.onKeyPress = function ( e ) {
4829 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
4830 if ( this.emit( 'click' ) ) {
4831 return false;
4832 }
4833 }
4834 };
4835
4836 /**
4837 * Check if button has a frame.
4838 *
4839 * @return {boolean} Button is framed
4840 */
4841 OO.ui.mixin.ButtonElement.prototype.isFramed = function () {
4842 return this.framed;
4843 };
4844
4845 /**
4846 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
4847 *
4848 * @param {boolean} [framed] Make button framed, omit to toggle
4849 * @chainable
4850 */
4851 OO.ui.mixin.ButtonElement.prototype.toggleFramed = function ( framed ) {
4852 framed = framed === undefined ? !this.framed : !!framed;
4853 if ( framed !== this.framed ) {
4854 this.framed = framed;
4855 this.$element
4856 .toggleClass( 'oo-ui-buttonElement-frameless', !framed )
4857 .toggleClass( 'oo-ui-buttonElement-framed', framed );
4858 this.updateThemeClasses();
4859 }
4860
4861 return this;
4862 };
4863
4864 /**
4865 * Set the button's active state.
4866 *
4867 * The active state occurs when a {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} or
4868 * a {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} is pressed. This method does nothing
4869 * for other button types.
4870 *
4871 * @param {boolean} value Make button active
4872 * @chainable
4873 */
4874 OO.ui.mixin.ButtonElement.prototype.setActive = function ( value ) {
4875 this.active = !!value;
4876 this.$element.toggleClass( 'oo-ui-buttonElement-active', this.active );
4877 return this;
4878 };
4879
4880 /**
4881 * Check if the button is active
4882 *
4883 * @return {boolean} The button is active
4884 */
4885 OO.ui.mixin.ButtonElement.prototype.isActive = function () {
4886 return this.active;
4887 };
4888
4889 /**
4890 * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
4891 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
4892 * items from the group is done through the interface the class provides.
4893 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
4894 *
4895 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
4896 *
4897 * @abstract
4898 * @class
4899 *
4900 * @constructor
4901 * @param {Object} [config] Configuration options
4902 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
4903 * is omitted, the group element will use a generated `<div>`.
4904 */
4905 OO.ui.mixin.GroupElement = function OoUiMixinGroupElement( config ) {
4906 // Configuration initialization
4907 config = config || {};
4908
4909 // Properties
4910 this.$group = null;
4911 this.items = [];
4912 this.aggregateItemEvents = {};
4913
4914 // Initialization
4915 this.setGroupElement( config.$group || $( '<div>' ) );
4916 };
4917
4918 /* Methods */
4919
4920 /**
4921 * Set the group element.
4922 *
4923 * If an element is already set, items will be moved to the new element.
4924 *
4925 * @param {jQuery} $group Element to use as group
4926 */
4927 OO.ui.mixin.GroupElement.prototype.setGroupElement = function ( $group ) {
4928 var i, len;
4929
4930 this.$group = $group;
4931 for ( i = 0, len = this.items.length; i < len; i++ ) {
4932 this.$group.append( this.items[ i ].$element );
4933 }
4934 };
4935
4936 /**
4937 * Check if a group contains no items.
4938 *
4939 * @return {boolean} Group is empty
4940 */
4941 OO.ui.mixin.GroupElement.prototype.isEmpty = function () {
4942 return !this.items.length;
4943 };
4944
4945 /**
4946 * Get all items in the group.
4947 *
4948 * The method returns an array of item references (e.g., [button1, button2, button3]) and is useful
4949 * when synchronizing groups of items, or whenever the references are required (e.g., when removing items
4950 * from a group).
4951 *
4952 * @return {OO.ui.Element[]} An array of items.
4953 */
4954 OO.ui.mixin.GroupElement.prototype.getItems = function () {
4955 return this.items.slice( 0 );
4956 };
4957
4958 /**
4959 * Get an item by its data.
4960 *
4961 * Only the first item with matching data will be returned. To return all matching items,
4962 * use the #getItemsFromData method.
4963 *
4964 * @param {Object} data Item data to search for
4965 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
4966 */
4967 OO.ui.mixin.GroupElement.prototype.getItemFromData = function ( data ) {
4968 var i, len, item,
4969 hash = OO.getHash( data );
4970
4971 for ( i = 0, len = this.items.length; i < len; i++ ) {
4972 item = this.items[ i ];
4973 if ( hash === OO.getHash( item.getData() ) ) {
4974 return item;
4975 }
4976 }
4977
4978 return null;
4979 };
4980
4981 /**
4982 * Get items by their data.
4983 *
4984 * All items with matching data will be returned. To return only the first match, use the #getItemFromData method instead.
4985 *
4986 * @param {Object} data Item data to search for
4987 * @return {OO.ui.Element[]} Items with equivalent data
4988 */
4989 OO.ui.mixin.GroupElement.prototype.getItemsFromData = function ( data ) {
4990 var i, len, item,
4991 hash = OO.getHash( data ),
4992 items = [];
4993
4994 for ( i = 0, len = this.items.length; i < len; i++ ) {
4995 item = this.items[ i ];
4996 if ( hash === OO.getHash( item.getData() ) ) {
4997 items.push( item );
4998 }
4999 }
5000
5001 return items;
5002 };
5003
5004 /**
5005 * Aggregate the events emitted by the group.
5006 *
5007 * When events are aggregated, the group will listen to all contained items for the event,
5008 * and then emit the event under a new name. The new event will contain an additional leading
5009 * parameter containing the item that emitted the original event. Other arguments emitted from
5010 * the original event are passed through.
5011 *
5012 * @param {Object.<string,string|null>} events An object keyed by the name of the event that should be
5013 * aggregated (e.g., ‘click’) and the value of the new name to use (e.g., ‘groupClick’).
5014 * A `null` value will remove aggregated events.
5015
5016 * @throws {Error} An error is thrown if aggregation already exists.
5017 */
5018 OO.ui.mixin.GroupElement.prototype.aggregate = function ( events ) {
5019 var i, len, item, add, remove, itemEvent, groupEvent;
5020
5021 for ( itemEvent in events ) {
5022 groupEvent = events[ itemEvent ];
5023
5024 // Remove existing aggregated event
5025 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
5026 // Don't allow duplicate aggregations
5027 if ( groupEvent ) {
5028 throw new Error( 'Duplicate item event aggregation for ' + itemEvent );
5029 }
5030 // Remove event aggregation from existing items
5031 for ( i = 0, len = this.items.length; i < len; i++ ) {
5032 item = this.items[ i ];
5033 if ( item.connect && item.disconnect ) {
5034 remove = {};
5035 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
5036 item.disconnect( this, remove );
5037 }
5038 }
5039 // Prevent future items from aggregating event
5040 delete this.aggregateItemEvents[ itemEvent ];
5041 }
5042
5043 // Add new aggregate event
5044 if ( groupEvent ) {
5045 // Make future items aggregate event
5046 this.aggregateItemEvents[ itemEvent ] = groupEvent;
5047 // Add event aggregation to existing items
5048 for ( i = 0, len = this.items.length; i < len; i++ ) {
5049 item = this.items[ i ];
5050 if ( item.connect && item.disconnect ) {
5051 add = {};
5052 add[ itemEvent ] = [ 'emit', groupEvent, item ];
5053 item.connect( this, add );
5054 }
5055 }
5056 }
5057 }
5058 };
5059
5060 /**
5061 * Add items to the group.
5062 *
5063 * Items will be added to the end of the group array unless the optional `index` parameter specifies
5064 * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
5065 *
5066 * @param {OO.ui.Element[]} items An array of items to add to the group
5067 * @param {number} [index] Index of the insertion point
5068 * @chainable
5069 */
5070 OO.ui.mixin.GroupElement.prototype.addItems = function ( items, index ) {
5071 var i, len, item, event, events, currentIndex,
5072 itemElements = [];
5073
5074 for ( i = 0, len = items.length; i < len; i++ ) {
5075 item = items[ i ];
5076
5077 // Check if item exists then remove it first, effectively "moving" it
5078 currentIndex = this.items.indexOf( item );
5079 if ( currentIndex >= 0 ) {
5080 this.removeItems( [ item ] );
5081 // Adjust index to compensate for removal
5082 if ( currentIndex < index ) {
5083 index--;
5084 }
5085 }
5086 // Add the item
5087 if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
5088 events = {};
5089 for ( event in this.aggregateItemEvents ) {
5090 events[ event ] = [ 'emit', this.aggregateItemEvents[ event ], item ];
5091 }
5092 item.connect( this, events );
5093 }
5094 item.setElementGroup( this );
5095 itemElements.push( item.$element.get( 0 ) );
5096 }
5097
5098 if ( index === undefined || index < 0 || index >= this.items.length ) {
5099 this.$group.append( itemElements );
5100 this.items.push.apply( this.items, items );
5101 } else if ( index === 0 ) {
5102 this.$group.prepend( itemElements );
5103 this.items.unshift.apply( this.items, items );
5104 } else {
5105 this.items[ index ].$element.before( itemElements );
5106 this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
5107 }
5108
5109 return this;
5110 };
5111
5112 /**
5113 * Remove the specified items from a group.
5114 *
5115 * Removed items are detached (not removed) from the DOM so that they may be reused.
5116 * To remove all items from a group, you may wish to use the #clearItems method instead.
5117 *
5118 * @param {OO.ui.Element[]} items An array of items to remove
5119 * @chainable
5120 */
5121 OO.ui.mixin.GroupElement.prototype.removeItems = function ( items ) {
5122 var i, len, item, index, remove, itemEvent;
5123
5124 // Remove specific items
5125 for ( i = 0, len = items.length; i < len; i++ ) {
5126 item = items[ i ];
5127 index = this.items.indexOf( item );
5128 if ( index !== -1 ) {
5129 if (
5130 item.connect && item.disconnect &&
5131 !$.isEmptyObject( this.aggregateItemEvents )
5132 ) {
5133 remove = {};
5134 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
5135 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
5136 }
5137 item.disconnect( this, remove );
5138 }
5139 item.setElementGroup( null );
5140 this.items.splice( index, 1 );
5141 item.$element.detach();
5142 }
5143 }
5144
5145 return this;
5146 };
5147
5148 /**
5149 * Clear all items from the group.
5150 *
5151 * Cleared items are detached from the DOM, not removed, so that they may be reused.
5152 * To remove only a subset of items from a group, use the #removeItems method.
5153 *
5154 * @chainable
5155 */
5156 OO.ui.mixin.GroupElement.prototype.clearItems = function () {
5157 var i, len, item, remove, itemEvent;
5158
5159 // Remove all items
5160 for ( i = 0, len = this.items.length; i < len; i++ ) {
5161 item = this.items[ i ];
5162 if (
5163 item.connect && item.disconnect &&
5164 !$.isEmptyObject( this.aggregateItemEvents )
5165 ) {
5166 remove = {};
5167 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
5168 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
5169 }
5170 item.disconnect( this, remove );
5171 }
5172 item.setElementGroup( null );
5173 item.$element.detach();
5174 }
5175
5176 this.items = [];
5177 return this;
5178 };
5179
5180 /**
5181 * DraggableElement is a mixin class used to create elements that can be clicked
5182 * and dragged by a mouse to a new position within a group. This class must be used
5183 * in conjunction with OO.ui.mixin.DraggableGroupElement, which provides a container for
5184 * the draggable elements.
5185 *
5186 * @abstract
5187 * @class
5188 *
5189 * @constructor
5190 */
5191 OO.ui.mixin.DraggableElement = function OoUiMixinDraggableElement() {
5192 // Properties
5193 this.index = null;
5194
5195 // Initialize and events
5196 this.$element
5197 .attr( 'draggable', true )
5198 .addClass( 'oo-ui-draggableElement' )
5199 .on( {
5200 dragstart: this.onDragStart.bind( this ),
5201 dragover: this.onDragOver.bind( this ),
5202 dragend: this.onDragEnd.bind( this ),
5203 drop: this.onDrop.bind( this )
5204 } );
5205 };
5206
5207 OO.initClass( OO.ui.mixin.DraggableElement );
5208
5209 /* Events */
5210
5211 /**
5212 * @event dragstart
5213 *
5214 * A dragstart event is emitted when the user clicks and begins dragging an item.
5215 * @param {OO.ui.mixin.DraggableElement} item The item the user has clicked and is dragging with the mouse.
5216 */
5217
5218 /**
5219 * @event dragend
5220 * A dragend event is emitted when the user drags an item and releases the mouse,
5221 * thus terminating the drag operation.
5222 */
5223
5224 /**
5225 * @event drop
5226 * A drop event is emitted when the user drags an item and then releases the mouse button
5227 * over a valid target.
5228 */
5229
5230 /* Static Properties */
5231
5232 /**
5233 * @inheritdoc OO.ui.mixin.ButtonElement
5234 */
5235 OO.ui.mixin.DraggableElement.static.cancelButtonMouseDownEvents = false;
5236
5237 /* Methods */
5238
5239 /**
5240 * Respond to dragstart event.
5241 *
5242 * @private
5243 * @param {jQuery.Event} event jQuery event
5244 * @fires dragstart
5245 */
5246 OO.ui.mixin.DraggableElement.prototype.onDragStart = function ( e ) {
5247 var dataTransfer = e.originalEvent.dataTransfer;
5248 // Define drop effect
5249 dataTransfer.dropEffect = 'none';
5250 dataTransfer.effectAllowed = 'move';
5251 // Support: Firefox
5252 // We must set up a dataTransfer data property or Firefox seems to
5253 // ignore the fact the element is draggable.
5254 try {
5255 dataTransfer.setData( 'application-x/OOjs-UI-draggable', this.getIndex() );
5256 } catch ( err ) {
5257 // The above is only for Firefox. Move on if it fails.
5258 }
5259 // Add dragging class
5260 this.$element.addClass( 'oo-ui-draggableElement-dragging' );
5261 // Emit event
5262 this.emit( 'dragstart', this );
5263 return true;
5264 };
5265
5266 /**
5267 * Respond to dragend event.
5268 *
5269 * @private
5270 * @fires dragend
5271 */
5272 OO.ui.mixin.DraggableElement.prototype.onDragEnd = function () {
5273 this.$element.removeClass( 'oo-ui-draggableElement-dragging' );
5274 this.emit( 'dragend' );
5275 };
5276
5277 /**
5278 * Handle drop event.
5279 *
5280 * @private
5281 * @param {jQuery.Event} event jQuery event
5282 * @fires drop
5283 */
5284 OO.ui.mixin.DraggableElement.prototype.onDrop = function ( e ) {
5285 e.preventDefault();
5286 this.emit( 'drop', e );
5287 };
5288
5289 /**
5290 * In order for drag/drop to work, the dragover event must
5291 * return false and stop propogation.
5292 *
5293 * @private
5294 */
5295 OO.ui.mixin.DraggableElement.prototype.onDragOver = function ( e ) {
5296 e.preventDefault();
5297 };
5298
5299 /**
5300 * Set item index.
5301 * Store it in the DOM so we can access from the widget drag event
5302 *
5303 * @private
5304 * @param {number} Item index
5305 */
5306 OO.ui.mixin.DraggableElement.prototype.setIndex = function ( index ) {
5307 if ( this.index !== index ) {
5308 this.index = index;
5309 this.$element.data( 'index', index );
5310 }
5311 };
5312
5313 /**
5314 * Get item index
5315 *
5316 * @private
5317 * @return {number} Item index
5318 */
5319 OO.ui.mixin.DraggableElement.prototype.getIndex = function () {
5320 return this.index;
5321 };
5322
5323 /**
5324 * DraggableGroupElement is a mixin class used to create a group element to
5325 * contain draggable elements, which are items that can be clicked and dragged by a mouse.
5326 * The class is used with OO.ui.mixin.DraggableElement.
5327 *
5328 * @abstract
5329 * @class
5330 * @mixins OO.ui.mixin.GroupElement
5331 *
5332 * @constructor
5333 * @param {Object} [config] Configuration options
5334 * @cfg {string} [orientation] Item orientation: 'horizontal' or 'vertical'. The orientation
5335 * should match the layout of the items. Items displayed in a single row
5336 * or in several rows should use horizontal orientation. The vertical orientation should only be
5337 * used when the items are displayed in a single column. Defaults to 'vertical'
5338 */
5339 OO.ui.mixin.DraggableGroupElement = function OoUiMixinDraggableGroupElement( config ) {
5340 // Configuration initialization
5341 config = config || {};
5342
5343 // Parent constructor
5344 OO.ui.mixin.GroupElement.call( this, config );
5345
5346 // Properties
5347 this.orientation = config.orientation || 'vertical';
5348 this.dragItem = null;
5349 this.itemDragOver = null;
5350 this.itemKeys = {};
5351 this.sideInsertion = '';
5352
5353 // Events
5354 this.aggregate( {
5355 dragstart: 'itemDragStart',
5356 dragend: 'itemDragEnd',
5357 drop: 'itemDrop'
5358 } );
5359 this.connect( this, {
5360 itemDragStart: 'onItemDragStart',
5361 itemDrop: 'onItemDrop',
5362 itemDragEnd: 'onItemDragEnd'
5363 } );
5364 this.$element.on( {
5365 dragover: this.onDragOver.bind( this ),
5366 dragleave: this.onDragLeave.bind( this )
5367 } );
5368
5369 // Initialize
5370 if ( Array.isArray( config.items ) ) {
5371 this.addItems( config.items );
5372 }
5373 this.$placeholder = $( '<div>' )
5374 .addClass( 'oo-ui-draggableGroupElement-placeholder' );
5375 this.$element
5376 .addClass( 'oo-ui-draggableGroupElement' )
5377 .append( this.$status )
5378 .toggleClass( 'oo-ui-draggableGroupElement-horizontal', this.orientation === 'horizontal' )
5379 .prepend( this.$placeholder );
5380 };
5381
5382 /* Setup */
5383 OO.mixinClass( OO.ui.mixin.DraggableGroupElement, OO.ui.mixin.GroupElement );
5384
5385 /* Events */
5386
5387 /**
5388 * A 'reorder' event is emitted when the order of items in the group changes.
5389 *
5390 * @event reorder
5391 * @param {OO.ui.mixin.DraggableElement} item Reordered item
5392 * @param {number} [newIndex] New index for the item
5393 */
5394
5395 /* Methods */
5396
5397 /**
5398 * Respond to item drag start event
5399 *
5400 * @private
5401 * @param {OO.ui.mixin.DraggableElement} item Dragged item
5402 */
5403 OO.ui.mixin.DraggableGroupElement.prototype.onItemDragStart = function ( item ) {
5404 var i, len;
5405
5406 // Map the index of each object
5407 for ( i = 0, len = this.items.length; i < len; i++ ) {
5408 this.items[ i ].setIndex( i );
5409 }
5410
5411 if ( this.orientation === 'horizontal' ) {
5412 // Set the height of the indicator
5413 this.$placeholder.css( {
5414 height: item.$element.outerHeight(),
5415 width: 2
5416 } );
5417 } else {
5418 // Set the width of the indicator
5419 this.$placeholder.css( {
5420 height: 2,
5421 width: item.$element.outerWidth()
5422 } );
5423 }
5424 this.setDragItem( item );
5425 };
5426
5427 /**
5428 * Respond to item drag end event
5429 *
5430 * @private
5431 */
5432 OO.ui.mixin.DraggableGroupElement.prototype.onItemDragEnd = function () {
5433 this.unsetDragItem();
5434 return false;
5435 };
5436
5437 /**
5438 * Handle drop event and switch the order of the items accordingly
5439 *
5440 * @private
5441 * @param {OO.ui.mixin.DraggableElement} item Dropped item
5442 * @fires reorder
5443 */
5444 OO.ui.mixin.DraggableGroupElement.prototype.onItemDrop = function ( item ) {
5445 var toIndex = item.getIndex();
5446 // Check if the dropped item is from the current group
5447 // TODO: Figure out a way to configure a list of legally droppable
5448 // elements even if they are not yet in the list
5449 if ( this.getDragItem() ) {
5450 // If the insertion point is 'after', the insertion index
5451 // is shifted to the right (or to the left in RTL, hence 'after')
5452 if ( this.sideInsertion === 'after' ) {
5453 toIndex++;
5454 }
5455 // Emit change event
5456 this.emit( 'reorder', this.getDragItem(), toIndex );
5457 }
5458 this.unsetDragItem();
5459 // Return false to prevent propogation
5460 return false;
5461 };
5462
5463 /**
5464 * Handle dragleave event.
5465 *
5466 * @private
5467 */
5468 OO.ui.mixin.DraggableGroupElement.prototype.onDragLeave = function () {
5469 // This means the item was dragged outside the widget
5470 this.$placeholder
5471 .css( 'left', 0 )
5472 .addClass( 'oo-ui-element-hidden' );
5473 };
5474
5475 /**
5476 * Respond to dragover event
5477 *
5478 * @private
5479 * @param {jQuery.Event} event Event details
5480 */
5481 OO.ui.mixin.DraggableGroupElement.prototype.onDragOver = function ( e ) {
5482 var dragOverObj, $optionWidget, itemOffset, itemMidpoint, itemBoundingRect,
5483 itemSize, cssOutput, dragPosition, itemIndex, itemPosition,
5484 clientX = e.originalEvent.clientX,
5485 clientY = e.originalEvent.clientY;
5486
5487 // Get the OptionWidget item we are dragging over
5488 dragOverObj = this.getElementDocument().elementFromPoint( clientX, clientY );
5489 $optionWidget = $( dragOverObj ).closest( '.oo-ui-draggableElement' );
5490 if ( $optionWidget[ 0 ] ) {
5491 itemOffset = $optionWidget.offset();
5492 itemBoundingRect = $optionWidget[ 0 ].getBoundingClientRect();
5493 itemPosition = $optionWidget.position();
5494 itemIndex = $optionWidget.data( 'index' );
5495 }
5496
5497 if (
5498 itemOffset &&
5499 this.isDragging() &&
5500 itemIndex !== this.getDragItem().getIndex()
5501 ) {
5502 if ( this.orientation === 'horizontal' ) {
5503 // Calculate where the mouse is relative to the item width
5504 itemSize = itemBoundingRect.width;
5505 itemMidpoint = itemBoundingRect.left + itemSize / 2;
5506 dragPosition = clientX;
5507 // Which side of the item we hover over will dictate
5508 // where the placeholder will appear, on the left or
5509 // on the right
5510 cssOutput = {
5511 left: dragPosition < itemMidpoint ? itemPosition.left : itemPosition.left + itemSize,
5512 top: itemPosition.top
5513 };
5514 } else {
5515 // Calculate where the mouse is relative to the item height
5516 itemSize = itemBoundingRect.height;
5517 itemMidpoint = itemBoundingRect.top + itemSize / 2;
5518 dragPosition = clientY;
5519 // Which side of the item we hover over will dictate
5520 // where the placeholder will appear, on the top or
5521 // on the bottom
5522 cssOutput = {
5523 top: dragPosition < itemMidpoint ? itemPosition.top : itemPosition.top + itemSize,
5524 left: itemPosition.left
5525 };
5526 }
5527 // Store whether we are before or after an item to rearrange
5528 // For horizontal layout, we need to account for RTL, as this is flipped
5529 if ( this.orientation === 'horizontal' && this.$element.css( 'direction' ) === 'rtl' ) {
5530 this.sideInsertion = dragPosition < itemMidpoint ? 'after' : 'before';
5531 } else {
5532 this.sideInsertion = dragPosition < itemMidpoint ? 'before' : 'after';
5533 }
5534 // Add drop indicator between objects
5535 this.$placeholder
5536 .css( cssOutput )
5537 .removeClass( 'oo-ui-element-hidden' );
5538 } else {
5539 // This means the item was dragged outside the widget
5540 this.$placeholder
5541 .css( 'left', 0 )
5542 .addClass( 'oo-ui-element-hidden' );
5543 }
5544 // Prevent default
5545 e.preventDefault();
5546 };
5547
5548 /**
5549 * Set a dragged item
5550 *
5551 * @param {OO.ui.mixin.DraggableElement} item Dragged item
5552 */
5553 OO.ui.mixin.DraggableGroupElement.prototype.setDragItem = function ( item ) {
5554 this.dragItem = item;
5555 };
5556
5557 /**
5558 * Unset the current dragged item
5559 */
5560 OO.ui.mixin.DraggableGroupElement.prototype.unsetDragItem = function () {
5561 this.dragItem = null;
5562 this.itemDragOver = null;
5563 this.$placeholder.addClass( 'oo-ui-element-hidden' );
5564 this.sideInsertion = '';
5565 };
5566
5567 /**
5568 * Get the item that is currently being dragged.
5569 *
5570 * @return {OO.ui.mixin.DraggableElement|null} The currently dragged item, or `null` if no item is being dragged
5571 */
5572 OO.ui.mixin.DraggableGroupElement.prototype.getDragItem = function () {
5573 return this.dragItem;
5574 };
5575
5576 /**
5577 * Check if an item in the group is currently being dragged.
5578 *
5579 * @return {Boolean} Item is being dragged
5580 */
5581 OO.ui.mixin.DraggableGroupElement.prototype.isDragging = function () {
5582 return this.getDragItem() !== null;
5583 };
5584
5585 /**
5586 * IconElement is often mixed into other classes to generate an icon.
5587 * Icons are graphics, about the size of normal text. They are used to aid the user
5588 * in locating a control or to convey information in a space-efficient way. See the
5589 * [OOjs UI documentation on MediaWiki] [1] for a list of icons
5590 * included in the library.
5591 *
5592 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
5593 *
5594 * @abstract
5595 * @class
5596 *
5597 * @constructor
5598 * @param {Object} [config] Configuration options
5599 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
5600 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
5601 * the icon element be set to an existing icon instead of the one generated by this class, set a
5602 * value using a jQuery selection. For example:
5603 *
5604 * // Use a <div> tag instead of a <span>
5605 * $icon: $("<div>")
5606 * // Use an existing icon element instead of the one generated by the class
5607 * $icon: this.$element
5608 * // Use an icon element from a child widget
5609 * $icon: this.childwidget.$element
5610 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
5611 * symbolic names. A map is used for i18n purposes and contains a `default` icon
5612 * name and additional names keyed by language code. The `default` name is used when no icon is keyed
5613 * by the user's language.
5614 *
5615 * Example of an i18n map:
5616 *
5617 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
5618 * See the [OOjs UI documentation on MediaWiki] [2] for a list of icons included in the library.
5619 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
5620 * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
5621 * text. The icon title is displayed when users move the mouse over the icon.
5622 */
5623 OO.ui.mixin.IconElement = function OoUiMixinIconElement( config ) {
5624 // Configuration initialization
5625 config = config || {};
5626
5627 // Properties
5628 this.$icon = null;
5629 this.icon = null;
5630 this.iconTitle = null;
5631
5632 // Initialization
5633 this.setIcon( config.icon || this.constructor.static.icon );
5634 this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle );
5635 this.setIconElement( config.$icon || $( '<span>' ) );
5636 };
5637
5638 /* Setup */
5639
5640 OO.initClass( OO.ui.mixin.IconElement );
5641
5642 /* Static Properties */
5643
5644 /**
5645 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
5646 * for i18n purposes and contains a `default` icon name and additional names keyed by
5647 * language code. The `default` name is used when no icon is keyed by the user's language.
5648 *
5649 * Example of an i18n map:
5650 *
5651 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
5652 *
5653 * Note: the static property will be overridden if the #icon configuration is used.
5654 *
5655 * @static
5656 * @inheritable
5657 * @property {Object|string}
5658 */
5659 OO.ui.mixin.IconElement.static.icon = null;
5660
5661 /**
5662 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
5663 * function that returns title text, or `null` for no title.
5664 *
5665 * The static property will be overridden if the #iconTitle configuration is used.
5666 *
5667 * @static
5668 * @inheritable
5669 * @property {string|Function|null}
5670 */
5671 OO.ui.mixin.IconElement.static.iconTitle = null;
5672
5673 /* Methods */
5674
5675 /**
5676 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
5677 * applies to the specified icon element instead of the one created by the class. If an icon
5678 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
5679 * and mixin methods will no longer affect the element.
5680 *
5681 * @param {jQuery} $icon Element to use as icon
5682 */
5683 OO.ui.mixin.IconElement.prototype.setIconElement = function ( $icon ) {
5684 if ( this.$icon ) {
5685 this.$icon
5686 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon )
5687 .removeAttr( 'title' );
5688 }
5689
5690 this.$icon = $icon
5691 .addClass( 'oo-ui-iconElement-icon' )
5692 .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon );
5693 if ( this.iconTitle !== null ) {
5694 this.$icon.attr( 'title', this.iconTitle );
5695 }
5696
5697 this.updateThemeClasses();
5698 };
5699
5700 /**
5701 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
5702 * The icon parameter can also be set to a map of icon names. See the #icon config setting
5703 * for an example.
5704 *
5705 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
5706 * by language code, or `null` to remove the icon.
5707 * @chainable
5708 */
5709 OO.ui.mixin.IconElement.prototype.setIcon = function ( icon ) {
5710 icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
5711 icon = typeof icon === 'string' && icon.trim().length ? icon.trim() : null;
5712
5713 if ( this.icon !== icon ) {
5714 if ( this.$icon ) {
5715 if ( this.icon !== null ) {
5716 this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
5717 }
5718 if ( icon !== null ) {
5719 this.$icon.addClass( 'oo-ui-icon-' + icon );
5720 }
5721 }
5722 this.icon = icon;
5723 }
5724
5725 this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon );
5726 this.updateThemeClasses();
5727
5728 return this;
5729 };
5730
5731 /**
5732 * Set the icon title. Use `null` to remove the title.
5733 *
5734 * @param {string|Function|null} iconTitle A text string used as the icon title,
5735 * a function that returns title text, or `null` for no title.
5736 * @chainable
5737 */
5738 OO.ui.mixin.IconElement.prototype.setIconTitle = function ( iconTitle ) {
5739 iconTitle = typeof iconTitle === 'function' ||
5740 ( typeof iconTitle === 'string' && iconTitle.length ) ?
5741 OO.ui.resolveMsg( iconTitle ) : null;
5742
5743 if ( this.iconTitle !== iconTitle ) {
5744 this.iconTitle = iconTitle;
5745 if ( this.$icon ) {
5746 if ( this.iconTitle !== null ) {
5747 this.$icon.attr( 'title', iconTitle );
5748 } else {
5749 this.$icon.removeAttr( 'title' );
5750 }
5751 }
5752 }
5753
5754 return this;
5755 };
5756
5757 /**
5758 * Get the symbolic name of the icon.
5759 *
5760 * @return {string} Icon name
5761 */
5762 OO.ui.mixin.IconElement.prototype.getIcon = function () {
5763 return this.icon;
5764 };
5765
5766 /**
5767 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
5768 *
5769 * @return {string} Icon title text
5770 */
5771 OO.ui.mixin.IconElement.prototype.getIconTitle = function () {
5772 return this.iconTitle;
5773 };
5774
5775 /**
5776 * IndicatorElement is often mixed into other classes to generate an indicator.
5777 * Indicators are small graphics that are generally used in two ways:
5778 *
5779 * - To draw attention to the status of an item. For example, an indicator might be
5780 * used to show that an item in a list has errors that need to be resolved.
5781 * - To clarify the function of a control that acts in an exceptional way (a button
5782 * that opens a menu instead of performing an action directly, for example).
5783 *
5784 * For a list of indicators included in the library, please see the
5785 * [OOjs UI documentation on MediaWiki] [1].
5786 *
5787 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
5788 *
5789 * @abstract
5790 * @class
5791 *
5792 * @constructor
5793 * @param {Object} [config] Configuration options
5794 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
5795 * configuration is omitted, the indicator element will use a generated `<span>`.
5796 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
5797 * See the [OOjs UI documentation on MediaWiki][2] for a list of indicators included
5798 * in the library.
5799 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
5800 * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
5801 * or a function that returns title text. The indicator title is displayed when users move
5802 * the mouse over the indicator.
5803 */
5804 OO.ui.mixin.IndicatorElement = function OoUiMixinIndicatorElement( config ) {
5805 // Configuration initialization
5806 config = config || {};
5807
5808 // Properties
5809 this.$indicator = null;
5810 this.indicator = null;
5811 this.indicatorTitle = null;
5812
5813 // Initialization
5814 this.setIndicator( config.indicator || this.constructor.static.indicator );
5815 this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle );
5816 this.setIndicatorElement( config.$indicator || $( '<span>' ) );
5817 };
5818
5819 /* Setup */
5820
5821 OO.initClass( OO.ui.mixin.IndicatorElement );
5822
5823 /* Static Properties */
5824
5825 /**
5826 * Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
5827 * The static property will be overridden if the #indicator configuration is used.
5828 *
5829 * @static
5830 * @inheritable
5831 * @property {string|null}
5832 */
5833 OO.ui.mixin.IndicatorElement.static.indicator = null;
5834
5835 /**
5836 * A text string used as the indicator title, a function that returns title text, or `null`
5837 * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
5838 *
5839 * @static
5840 * @inheritable
5841 * @property {string|Function|null}
5842 */
5843 OO.ui.mixin.IndicatorElement.static.indicatorTitle = null;
5844
5845 /* Methods */
5846
5847 /**
5848 * Set the indicator element.
5849 *
5850 * If an element is already set, it will be cleaned up before setting up the new element.
5851 *
5852 * @param {jQuery} $indicator Element to use as indicator
5853 */
5854 OO.ui.mixin.IndicatorElement.prototype.setIndicatorElement = function ( $indicator ) {
5855 if ( this.$indicator ) {
5856 this.$indicator
5857 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator )
5858 .removeAttr( 'title' );
5859 }
5860
5861 this.$indicator = $indicator
5862 .addClass( 'oo-ui-indicatorElement-indicator' )
5863 .toggleClass( 'oo-ui-indicator-' + this.indicator, !!this.indicator );
5864 if ( this.indicatorTitle !== null ) {
5865 this.$indicator.attr( 'title', this.indicatorTitle );
5866 }
5867
5868 this.updateThemeClasses();
5869 };
5870
5871 /**
5872 * Set the indicator by its symbolic name: ‘alert’, ‘down’, ‘next’, ‘previous’, ‘required’, ‘up’. Use `null` to remove the indicator.
5873 *
5874 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
5875 * @chainable
5876 */
5877 OO.ui.mixin.IndicatorElement.prototype.setIndicator = function ( indicator ) {
5878 indicator = typeof indicator === 'string' && indicator.length ? indicator.trim() : null;
5879
5880 if ( this.indicator !== indicator ) {
5881 if ( this.$indicator ) {
5882 if ( this.indicator !== null ) {
5883 this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
5884 }
5885 if ( indicator !== null ) {
5886 this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
5887 }
5888 }
5889 this.indicator = indicator;
5890 }
5891
5892 this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator );
5893 this.updateThemeClasses();
5894
5895 return this;
5896 };
5897
5898 /**
5899 * Set the indicator title.
5900 *
5901 * The title is displayed when a user moves the mouse over the indicator.
5902 *
5903 * @param {string|Function|null} indicator Indicator title text, a function that returns text, or
5904 * `null` for no indicator title
5905 * @chainable
5906 */
5907 OO.ui.mixin.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
5908 indicatorTitle = typeof indicatorTitle === 'function' ||
5909 ( typeof indicatorTitle === 'string' && indicatorTitle.length ) ?
5910 OO.ui.resolveMsg( indicatorTitle ) : null;
5911
5912 if ( this.indicatorTitle !== indicatorTitle ) {
5913 this.indicatorTitle = indicatorTitle;
5914 if ( this.$indicator ) {
5915 if ( this.indicatorTitle !== null ) {
5916 this.$indicator.attr( 'title', indicatorTitle );
5917 } else {
5918 this.$indicator.removeAttr( 'title' );
5919 }
5920 }
5921 }
5922
5923 return this;
5924 };
5925
5926 /**
5927 * Get the symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
5928 *
5929 * @return {string} Symbolic name of indicator
5930 */
5931 OO.ui.mixin.IndicatorElement.prototype.getIndicator = function () {
5932 return this.indicator;
5933 };
5934
5935 /**
5936 * Get the indicator title.
5937 *
5938 * The title is displayed when a user moves the mouse over the indicator.
5939 *
5940 * @return {string} Indicator title text
5941 */
5942 OO.ui.mixin.IndicatorElement.prototype.getIndicatorTitle = function () {
5943 return this.indicatorTitle;
5944 };
5945
5946 /**
5947 * LabelElement is often mixed into other classes to generate a label, which
5948 * helps identify the function of an interface element.
5949 * See the [OOjs UI documentation on MediaWiki] [1] for more information.
5950 *
5951 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
5952 *
5953 * @abstract
5954 * @class
5955 *
5956 * @constructor
5957 * @param {Object} [config] Configuration options
5958 * @cfg {jQuery} [$label] The label element created by the class. If this
5959 * configuration is omitted, the label element will use a generated `<span>`.
5960 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified
5961 * as a plaintext string, a jQuery selection of elements, or a function that will produce a string
5962 * in the future. See the [OOjs UI documentation on MediaWiki] [2] for examples.
5963 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
5964 * @cfg {boolean} [autoFitLabel=true] Fit the label to the width of the parent element.
5965 * The label will be truncated to fit if necessary.
5966 */
5967 OO.ui.mixin.LabelElement = function OoUiMixinLabelElement( config ) {
5968 // Configuration initialization
5969 config = config || {};
5970
5971 // Properties
5972 this.$label = null;
5973 this.label = null;
5974 this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel;
5975
5976 // Initialization
5977 this.setLabel( config.label || this.constructor.static.label );
5978 this.setLabelElement( config.$label || $( '<span>' ) );
5979 };
5980
5981 /* Setup */
5982
5983 OO.initClass( OO.ui.mixin.LabelElement );
5984
5985 /* Events */
5986
5987 /**
5988 * @event labelChange
5989 * @param {string} value
5990 */
5991
5992 /* Static Properties */
5993
5994 /**
5995 * The label text. The label can be specified as a plaintext string, a function that will
5996 * produce a string in the future, or `null` for no label. The static value will
5997 * be overridden if a label is specified with the #label config option.
5998 *
5999 * @static
6000 * @inheritable
6001 * @property {string|Function|null}
6002 */
6003 OO.ui.mixin.LabelElement.static.label = null;
6004
6005 /* Methods */
6006
6007 /**
6008 * Set the label element.
6009 *
6010 * If an element is already set, it will be cleaned up before setting up the new element.
6011 *
6012 * @param {jQuery} $label Element to use as label
6013 */
6014 OO.ui.mixin.LabelElement.prototype.setLabelElement = function ( $label ) {
6015 if ( this.$label ) {
6016 this.$label.removeClass( 'oo-ui-labelElement-label' ).empty();
6017 }
6018
6019 this.$label = $label.addClass( 'oo-ui-labelElement-label' );
6020 this.setLabelContent( this.label );
6021 };
6022
6023 /**
6024 * Set the label.
6025 *
6026 * An empty string will result in the label being hidden. A string containing only whitespace will
6027 * be converted to a single `&nbsp;`.
6028 *
6029 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
6030 * text; or null for no label
6031 * @chainable
6032 */
6033 OO.ui.mixin.LabelElement.prototype.setLabel = function ( label ) {
6034 label = typeof label === 'function' ? OO.ui.resolveMsg( label ) : label;
6035 label = ( ( typeof label === 'string' && label.length ) || label instanceof jQuery || label instanceof OO.ui.HtmlSnippet ) ? label : null;
6036
6037 this.$element.toggleClass( 'oo-ui-labelElement', !!label );
6038
6039 if ( this.label !== label ) {
6040 if ( this.$label ) {
6041 this.setLabelContent( label );
6042 }
6043 this.label = label;
6044 this.emit( 'labelChange' );
6045 }
6046
6047 return this;
6048 };
6049
6050 /**
6051 * Get the label.
6052 *
6053 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
6054 * text; or null for no label
6055 */
6056 OO.ui.mixin.LabelElement.prototype.getLabel = function () {
6057 return this.label;
6058 };
6059
6060 /**
6061 * Fit the label.
6062 *
6063 * @chainable
6064 */
6065 OO.ui.mixin.LabelElement.prototype.fitLabel = function () {
6066 if ( this.$label && this.$label.autoEllipsis && this.autoFitLabel ) {
6067 this.$label.autoEllipsis( { hasSpan: false, tooltip: true } );
6068 }
6069
6070 return this;
6071 };
6072
6073 /**
6074 * Set the content of the label.
6075 *
6076 * Do not call this method until after the label element has been set by #setLabelElement.
6077 *
6078 * @private
6079 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
6080 * text; or null for no label
6081 */
6082 OO.ui.mixin.LabelElement.prototype.setLabelContent = function ( label ) {
6083 if ( typeof label === 'string' ) {
6084 if ( label.match( /^\s*$/ ) ) {
6085 // Convert whitespace only string to a single non-breaking space
6086 this.$label.html( '&nbsp;' );
6087 } else {
6088 this.$label.text( label );
6089 }
6090 } else if ( label instanceof OO.ui.HtmlSnippet ) {
6091 this.$label.html( label.toString() );
6092 } else if ( label instanceof jQuery ) {
6093 this.$label.empty().append( label );
6094 } else {
6095 this.$label.empty();
6096 }
6097 };
6098
6099 /**
6100 * LookupElement is a mixin that creates a {@link OO.ui.FloatingMenuSelectWidget menu} of suggested values for
6101 * a {@link OO.ui.TextInputWidget text input widget}. Suggested values are based on the characters the user types
6102 * into the text input field and, in general, the menu is only displayed when the user types. If a suggested value is chosen
6103 * from the lookup menu, that value becomes the value of the input field.
6104 *
6105 * Note that a new menu of suggested items is displayed when a value is chosen from the lookup menu. If this is
6106 * not the desired behavior, disable lookup menus with the #setLookupsDisabled method, then set the value, then
6107 * re-enable lookups.
6108 *
6109 * See the [OOjs UI demos][1] for an example.
6110 *
6111 * [1]: https://tools.wmflabs.org/oojs-ui/oojs-ui/demos/index.html#widgets-apex-vector-ltr
6112 *
6113 * @class
6114 * @abstract
6115 *
6116 * @constructor
6117 * @param {Object} [config] Configuration options
6118 * @cfg {jQuery} [$overlay] Overlay for the lookup menu; defaults to relative positioning
6119 * @cfg {jQuery} [$container=this.$element] The container element. The lookup menu is rendered beneath the specified element.
6120 * @cfg {boolean} [allowSuggestionsWhenEmpty=false] Request and display a lookup menu when the text input is empty.
6121 * By default, the lookup menu is not generated and displayed until the user begins to type.
6122 * @cfg {boolean} [highlightFirst=true] Whether the first lookup result should be highlighted (so, that the user can
6123 * take it over into the input with simply pressing return) automatically or not.
6124 */
6125 OO.ui.mixin.LookupElement = function OoUiMixinLookupElement( config ) {
6126 // Configuration initialization
6127 config = $.extend( { highlightFirst: true }, config );
6128
6129 // Mixin constructors
6130 OO.ui.mixin.RequestManager.call( this, config );
6131
6132 // Properties
6133 this.$overlay = config.$overlay || this.$element;
6134 this.lookupMenu = new OO.ui.FloatingMenuSelectWidget( {
6135 widget: this,
6136 input: this,
6137 $container: config.$container || this.$element
6138 } );
6139
6140 this.allowSuggestionsWhenEmpty = config.allowSuggestionsWhenEmpty || false;
6141
6142 this.lookupsDisabled = false;
6143 this.lookupInputFocused = false;
6144 this.lookupHighlightFirstItem = config.highlightFirst;
6145
6146 // Events
6147 this.$input.on( {
6148 focus: this.onLookupInputFocus.bind( this ),
6149 blur: this.onLookupInputBlur.bind( this ),
6150 mousedown: this.onLookupInputMouseDown.bind( this )
6151 } );
6152 this.connect( this, { change: 'onLookupInputChange' } );
6153 this.lookupMenu.connect( this, {
6154 toggle: 'onLookupMenuToggle',
6155 choose: 'onLookupMenuItemChoose'
6156 } );
6157
6158 // Initialization
6159 this.$element.addClass( 'oo-ui-lookupElement' );
6160 this.lookupMenu.$element.addClass( 'oo-ui-lookupElement-menu' );
6161 this.$overlay.append( this.lookupMenu.$element );
6162 };
6163
6164 /* Setup */
6165
6166 OO.mixinClass( OO.ui.mixin.LookupElement, OO.ui.mixin.RequestManager );
6167
6168 /* Methods */
6169
6170 /**
6171 * Handle input focus event.
6172 *
6173 * @protected
6174 * @param {jQuery.Event} e Input focus event
6175 */
6176 OO.ui.mixin.LookupElement.prototype.onLookupInputFocus = function () {
6177 this.lookupInputFocused = true;
6178 this.populateLookupMenu();
6179 };
6180
6181 /**
6182 * Handle input blur event.
6183 *
6184 * @protected
6185 * @param {jQuery.Event} e Input blur event
6186 */
6187 OO.ui.mixin.LookupElement.prototype.onLookupInputBlur = function () {
6188 this.closeLookupMenu();
6189 this.lookupInputFocused = false;
6190 };
6191
6192 /**
6193 * Handle input mouse down event.
6194 *
6195 * @protected
6196 * @param {jQuery.Event} e Input mouse down event
6197 */
6198 OO.ui.mixin.LookupElement.prototype.onLookupInputMouseDown = function () {
6199 // Only open the menu if the input was already focused.
6200 // This way we allow the user to open the menu again after closing it with Esc
6201 // by clicking in the input. Opening (and populating) the menu when initially
6202 // clicking into the input is handled by the focus handler.
6203 if ( this.lookupInputFocused && !this.lookupMenu.isVisible() ) {
6204 this.populateLookupMenu();
6205 }
6206 };
6207
6208 /**
6209 * Handle input change event.
6210 *
6211 * @protected
6212 * @param {string} value New input value
6213 */
6214 OO.ui.mixin.LookupElement.prototype.onLookupInputChange = function () {
6215 if ( this.lookupInputFocused ) {
6216 this.populateLookupMenu();
6217 }
6218 };
6219
6220 /**
6221 * Handle the lookup menu being shown/hidden.
6222 *
6223 * @protected
6224 * @param {boolean} visible Whether the lookup menu is now visible.
6225 */
6226 OO.ui.mixin.LookupElement.prototype.onLookupMenuToggle = function ( visible ) {
6227 if ( !visible ) {
6228 // When the menu is hidden, abort any active request and clear the menu.
6229 // This has to be done here in addition to closeLookupMenu(), because
6230 // MenuSelectWidget will close itself when the user presses Esc.
6231 this.abortLookupRequest();
6232 this.lookupMenu.clearItems();
6233 }
6234 };
6235
6236 /**
6237 * Handle menu item 'choose' event, updating the text input value to the value of the clicked item.
6238 *
6239 * @protected
6240 * @param {OO.ui.MenuOptionWidget} item Selected item
6241 */
6242 OO.ui.mixin.LookupElement.prototype.onLookupMenuItemChoose = function ( item ) {
6243 this.setValue( item.getData() );
6244 };
6245
6246 /**
6247 * Get lookup menu.
6248 *
6249 * @private
6250 * @return {OO.ui.FloatingMenuSelectWidget}
6251 */
6252 OO.ui.mixin.LookupElement.prototype.getLookupMenu = function () {
6253 return this.lookupMenu;
6254 };
6255
6256 /**
6257 * Disable or re-enable lookups.
6258 *
6259 * When lookups are disabled, calls to #populateLookupMenu will be ignored.
6260 *
6261 * @param {boolean} disabled Disable lookups
6262 */
6263 OO.ui.mixin.LookupElement.prototype.setLookupsDisabled = function ( disabled ) {
6264 this.lookupsDisabled = !!disabled;
6265 };
6266
6267 /**
6268 * Open the menu. If there are no entries in the menu, this does nothing.
6269 *
6270 * @private
6271 * @chainable
6272 */
6273 OO.ui.mixin.LookupElement.prototype.openLookupMenu = function () {
6274 if ( !this.lookupMenu.isEmpty() ) {
6275 this.lookupMenu.toggle( true );
6276 }
6277 return this;
6278 };
6279
6280 /**
6281 * Close the menu, empty it, and abort any pending request.
6282 *
6283 * @private
6284 * @chainable
6285 */
6286 OO.ui.mixin.LookupElement.prototype.closeLookupMenu = function () {
6287 this.lookupMenu.toggle( false );
6288 this.abortLookupRequest();
6289 this.lookupMenu.clearItems();
6290 return this;
6291 };
6292
6293 /**
6294 * Request menu items based on the input's current value, and when they arrive,
6295 * populate the menu with these items and show the menu.
6296 *
6297 * If lookups have been disabled with #setLookupsDisabled, this function does nothing.
6298 *
6299 * @private
6300 * @chainable
6301 */
6302 OO.ui.mixin.LookupElement.prototype.populateLookupMenu = function () {
6303 var widget = this,
6304 value = this.getValue();
6305
6306 if ( this.lookupsDisabled || this.isReadOnly() ) {
6307 return;
6308 }
6309
6310 // If the input is empty, clear the menu, unless suggestions when empty are allowed.
6311 if ( !this.allowSuggestionsWhenEmpty && value === '' ) {
6312 this.closeLookupMenu();
6313 // Skip population if there is already a request pending for the current value
6314 } else if ( value !== this.lookupQuery ) {
6315 this.getLookupMenuItems()
6316 .done( function ( items ) {
6317 widget.lookupMenu.clearItems();
6318 if ( items.length ) {
6319 widget.lookupMenu
6320 .addItems( items )
6321 .toggle( true );
6322 widget.initializeLookupMenuSelection();
6323 } else {
6324 widget.lookupMenu.toggle( false );
6325 }
6326 } )
6327 .fail( function () {
6328 widget.lookupMenu.clearItems();
6329 } );
6330 }
6331
6332 return this;
6333 };
6334
6335 /**
6336 * Highlight the first selectable item in the menu, if configured.
6337 *
6338 * @private
6339 * @chainable
6340 */
6341 OO.ui.mixin.LookupElement.prototype.initializeLookupMenuSelection = function () {
6342 if ( this.lookupHighlightFirstItem && !this.lookupMenu.getSelectedItem() ) {
6343 this.lookupMenu.highlightItem( this.lookupMenu.getFirstSelectableItem() );
6344 }
6345 };
6346
6347 /**
6348 * Get lookup menu items for the current query.
6349 *
6350 * @private
6351 * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument of
6352 * the done event. If the request was aborted to make way for a subsequent request, this promise
6353 * will not be rejected: it will remain pending forever.
6354 */
6355 OO.ui.mixin.LookupElement.prototype.getLookupMenuItems = function () {
6356 return this.getRequestData().then( function ( data ) {
6357 return this.getLookupMenuOptionsFromData( data );
6358 }.bind( this ) );
6359 };
6360
6361 /**
6362 * Abort the currently pending lookup request, if any.
6363 *
6364 * @private
6365 */
6366 OO.ui.mixin.LookupElement.prototype.abortLookupRequest = function () {
6367 this.abortRequest();
6368 };
6369
6370 /**
6371 * Get a new request object of the current lookup query value.
6372 *
6373 * @protected
6374 * @method
6375 * @abstract
6376 * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
6377 */
6378 OO.ui.mixin.LookupElement.prototype.getLookupRequest = null;
6379
6380 /**
6381 * Pre-process data returned by the request from #getLookupRequest.
6382 *
6383 * The return value of this function will be cached, and any further queries for the given value
6384 * will use the cache rather than doing API requests.
6385 *
6386 * @protected
6387 * @method
6388 * @abstract
6389 * @param {Mixed} response Response from server
6390 * @return {Mixed} Cached result data
6391 */
6392 OO.ui.mixin.LookupElement.prototype.getLookupCacheDataFromResponse = null;
6393
6394 /**
6395 * Get a list of menu option widgets from the (possibly cached) data returned by
6396 * #getLookupCacheDataFromResponse.
6397 *
6398 * @protected
6399 * @method
6400 * @abstract
6401 * @param {Mixed} data Cached result data, usually an array
6402 * @return {OO.ui.MenuOptionWidget[]} Menu items
6403 */
6404 OO.ui.mixin.LookupElement.prototype.getLookupMenuOptionsFromData = null;
6405
6406 /**
6407 * Set the read-only state of the widget.
6408 *
6409 * This will also disable/enable the lookups functionality.
6410 *
6411 * @param {boolean} readOnly Make input read-only
6412 * @chainable
6413 */
6414 OO.ui.mixin.LookupElement.prototype.setReadOnly = function ( readOnly ) {
6415 // Parent method
6416 // Note: Calling #setReadOnly this way assumes this is mixed into an OO.ui.TextInputWidget
6417 OO.ui.TextInputWidget.prototype.setReadOnly.call( this, readOnly );
6418
6419 // During construction, #setReadOnly is called before the OO.ui.mixin.LookupElement constructor
6420 if ( this.isReadOnly() && this.lookupMenu ) {
6421 this.closeLookupMenu();
6422 }
6423
6424 return this;
6425 };
6426
6427 /**
6428 * @inheritdoc OO.ui.mixin.RequestManager
6429 */
6430 OO.ui.mixin.LookupElement.prototype.getRequestQuery = function () {
6431 return this.getValue();
6432 };
6433
6434 /**
6435 * @inheritdoc OO.ui.mixin.RequestManager
6436 */
6437 OO.ui.mixin.LookupElement.prototype.getRequest = function () {
6438 return this.getLookupRequest();
6439 };
6440
6441 /**
6442 * @inheritdoc OO.ui.mixin.RequestManager
6443 */
6444 OO.ui.mixin.LookupElement.prototype.getRequestCacheDataFromResponse = function ( response ) {
6445 return this.getLookupCacheDataFromResponse( response );
6446 };
6447
6448 /**
6449 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
6450 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
6451 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
6452 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
6453 *
6454 * @abstract
6455 * @class
6456 *
6457 * @constructor
6458 * @param {Object} [config] Configuration options
6459 * @cfg {Object} [popup] Configuration to pass to popup
6460 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
6461 */
6462 OO.ui.mixin.PopupElement = function OoUiMixinPopupElement( config ) {
6463 // Configuration initialization
6464 config = config || {};
6465
6466 // Properties
6467 this.popup = new OO.ui.PopupWidget( $.extend(
6468 { autoClose: true },
6469 config.popup,
6470 { $autoCloseIgnore: this.$element }
6471 ) );
6472 };
6473
6474 /* Methods */
6475
6476 /**
6477 * Get popup.
6478 *
6479 * @return {OO.ui.PopupWidget} Popup widget
6480 */
6481 OO.ui.mixin.PopupElement.prototype.getPopup = function () {
6482 return this.popup;
6483 };
6484
6485 /**
6486 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
6487 * additional functionality to an element created by another class. The class provides
6488 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
6489 * which are used to customize the look and feel of a widget to better describe its
6490 * importance and functionality.
6491 *
6492 * The library currently contains the following styling flags for general use:
6493 *
6494 * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
6495 * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
6496 * - **constructive**: Constructive styling is applied to convey that the widget will create something.
6497 *
6498 * The flags affect the appearance of the buttons:
6499 *
6500 * @example
6501 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
6502 * var button1 = new OO.ui.ButtonWidget( {
6503 * label: 'Constructive',
6504 * flags: 'constructive'
6505 * } );
6506 * var button2 = new OO.ui.ButtonWidget( {
6507 * label: 'Destructive',
6508 * flags: 'destructive'
6509 * } );
6510 * var button3 = new OO.ui.ButtonWidget( {
6511 * label: 'Progressive',
6512 * flags: 'progressive'
6513 * } );
6514 * $( 'body' ).append( button1.$element, button2.$element, button3.$element );
6515 *
6516 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
6517 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
6518 *
6519 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
6520 *
6521 * @abstract
6522 * @class
6523 *
6524 * @constructor
6525 * @param {Object} [config] Configuration options
6526 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'constructive' or 'primary') to apply.
6527 * Please see the [OOjs UI documentation on MediaWiki] [2] for more information about available flags.
6528 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
6529 * @cfg {jQuery} [$flagged] The flagged element. By default,
6530 * the flagged functionality is applied to the element created by the class ($element).
6531 * If a different element is specified, the flagged functionality will be applied to it instead.
6532 */
6533 OO.ui.mixin.FlaggedElement = function OoUiMixinFlaggedElement( config ) {
6534 // Configuration initialization
6535 config = config || {};
6536
6537 // Properties
6538 this.flags = {};
6539 this.$flagged = null;
6540
6541 // Initialization
6542 this.setFlags( config.flags );
6543 this.setFlaggedElement( config.$flagged || this.$element );
6544 };
6545
6546 /* Events */
6547
6548 /**
6549 * @event flag
6550 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
6551 * parameter contains the name of each modified flag and indicates whether it was
6552 * added or removed.
6553 *
6554 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
6555 * that the flag was added, `false` that the flag was removed.
6556 */
6557
6558 /* Methods */
6559
6560 /**
6561 * Set the flagged element.
6562 *
6563 * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
6564 * If an element is already set, the method will remove the mixin’s effect on that element.
6565 *
6566 * @param {jQuery} $flagged Element that should be flagged
6567 */
6568 OO.ui.mixin.FlaggedElement.prototype.setFlaggedElement = function ( $flagged ) {
6569 var classNames = Object.keys( this.flags ).map( function ( flag ) {
6570 return 'oo-ui-flaggedElement-' + flag;
6571 } ).join( ' ' );
6572
6573 if ( this.$flagged ) {
6574 this.$flagged.removeClass( classNames );
6575 }
6576
6577 this.$flagged = $flagged.addClass( classNames );
6578 };
6579
6580 /**
6581 * Check if the specified flag is set.
6582 *
6583 * @param {string} flag Name of flag
6584 * @return {boolean} The flag is set
6585 */
6586 OO.ui.mixin.FlaggedElement.prototype.hasFlag = function ( flag ) {
6587 // This may be called before the constructor, thus before this.flags is set
6588 return this.flags && ( flag in this.flags );
6589 };
6590
6591 /**
6592 * Get the names of all flags set.
6593 *
6594 * @return {string[]} Flag names
6595 */
6596 OO.ui.mixin.FlaggedElement.prototype.getFlags = function () {
6597 // This may be called before the constructor, thus before this.flags is set
6598 return Object.keys( this.flags || {} );
6599 };
6600
6601 /**
6602 * Clear all flags.
6603 *
6604 * @chainable
6605 * @fires flag
6606 */
6607 OO.ui.mixin.FlaggedElement.prototype.clearFlags = function () {
6608 var flag, className,
6609 changes = {},
6610 remove = [],
6611 classPrefix = 'oo-ui-flaggedElement-';
6612
6613 for ( flag in this.flags ) {
6614 className = classPrefix + flag;
6615 changes[ flag ] = false;
6616 delete this.flags[ flag ];
6617 remove.push( className );
6618 }
6619
6620 if ( this.$flagged ) {
6621 this.$flagged.removeClass( remove.join( ' ' ) );
6622 }
6623
6624 this.updateThemeClasses();
6625 this.emit( 'flag', changes );
6626
6627 return this;
6628 };
6629
6630 /**
6631 * Add one or more flags.
6632 *
6633 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
6634 * or an object keyed by flag name with a boolean value that indicates whether the flag should
6635 * be added (`true`) or removed (`false`).
6636 * @chainable
6637 * @fires flag
6638 */
6639 OO.ui.mixin.FlaggedElement.prototype.setFlags = function ( flags ) {
6640 var i, len, flag, className,
6641 changes = {},
6642 add = [],
6643 remove = [],
6644 classPrefix = 'oo-ui-flaggedElement-';
6645
6646 if ( typeof flags === 'string' ) {
6647 className = classPrefix + flags;
6648 // Set
6649 if ( !this.flags[ flags ] ) {
6650 this.flags[ flags ] = true;
6651 add.push( className );
6652 }
6653 } else if ( Array.isArray( flags ) ) {
6654 for ( i = 0, len = flags.length; i < len; i++ ) {
6655 flag = flags[ i ];
6656 className = classPrefix + flag;
6657 // Set
6658 if ( !this.flags[ flag ] ) {
6659 changes[ flag ] = true;
6660 this.flags[ flag ] = true;
6661 add.push( className );
6662 }
6663 }
6664 } else if ( OO.isPlainObject( flags ) ) {
6665 for ( flag in flags ) {
6666 className = classPrefix + flag;
6667 if ( flags[ flag ] ) {
6668 // Set
6669 if ( !this.flags[ flag ] ) {
6670 changes[ flag ] = true;
6671 this.flags[ flag ] = true;
6672 add.push( className );
6673 }
6674 } else {
6675 // Remove
6676 if ( this.flags[ flag ] ) {
6677 changes[ flag ] = false;
6678 delete this.flags[ flag ];
6679 remove.push( className );
6680 }
6681 }
6682 }
6683 }
6684
6685 if ( this.$flagged ) {
6686 this.$flagged
6687 .addClass( add.join( ' ' ) )
6688 .removeClass( remove.join( ' ' ) );
6689 }
6690
6691 this.updateThemeClasses();
6692 this.emit( 'flag', changes );
6693
6694 return this;
6695 };
6696
6697 /**
6698 * TitledElement is mixed into other classes to provide a `title` attribute.
6699 * Titles are rendered by the browser and are made visible when the user moves
6700 * the mouse over the element. Titles are not visible on touch devices.
6701 *
6702 * @example
6703 * // TitledElement provides a 'title' attribute to the
6704 * // ButtonWidget class
6705 * var button = new OO.ui.ButtonWidget( {
6706 * label: 'Button with Title',
6707 * title: 'I am a button'
6708 * } );
6709 * $( 'body' ).append( button.$element );
6710 *
6711 * @abstract
6712 * @class
6713 *
6714 * @constructor
6715 * @param {Object} [config] Configuration options
6716 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
6717 * If this config is omitted, the title functionality is applied to $element, the
6718 * element created by the class.
6719 * @cfg {string|Function} [title] The title text or a function that returns text. If
6720 * this config is omitted, the value of the {@link #static-title static title} property is used.
6721 */
6722 OO.ui.mixin.TitledElement = function OoUiMixinTitledElement( config ) {
6723 // Configuration initialization
6724 config = config || {};
6725
6726 // Properties
6727 this.$titled = null;
6728 this.title = null;
6729
6730 // Initialization
6731 this.setTitle( config.title || this.constructor.static.title );
6732 this.setTitledElement( config.$titled || this.$element );
6733 };
6734
6735 /* Setup */
6736
6737 OO.initClass( OO.ui.mixin.TitledElement );
6738
6739 /* Static Properties */
6740
6741 /**
6742 * The title text, a function that returns text, or `null` for no title. The value of the static property
6743 * is overridden if the #title config option is used.
6744 *
6745 * @static
6746 * @inheritable
6747 * @property {string|Function|null}
6748 */
6749 OO.ui.mixin.TitledElement.static.title = null;
6750
6751 /* Methods */
6752
6753 /**
6754 * Set the titled element.
6755 *
6756 * This method is used to retarget a titledElement mixin so that its functionality applies to the specified element.
6757 * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
6758 *
6759 * @param {jQuery} $titled Element that should use the 'titled' functionality
6760 */
6761 OO.ui.mixin.TitledElement.prototype.setTitledElement = function ( $titled ) {
6762 if ( this.$titled ) {
6763 this.$titled.removeAttr( 'title' );
6764 }
6765
6766 this.$titled = $titled;
6767 if ( this.title ) {
6768 this.$titled.attr( 'title', this.title );
6769 }
6770 };
6771
6772 /**
6773 * Set title.
6774 *
6775 * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
6776 * @chainable
6777 */
6778 OO.ui.mixin.TitledElement.prototype.setTitle = function ( title ) {
6779 title = typeof title === 'function' ? OO.ui.resolveMsg( title ) : title;
6780 title = ( typeof title === 'string' && title.length ) ? title : null;
6781
6782 if ( this.title !== title ) {
6783 if ( this.$titled ) {
6784 if ( title !== null ) {
6785 this.$titled.attr( 'title', title );
6786 } else {
6787 this.$titled.removeAttr( 'title' );
6788 }
6789 }
6790 this.title = title;
6791 }
6792
6793 return this;
6794 };
6795
6796 /**
6797 * Get title.
6798 *
6799 * @return {string} Title string
6800 */
6801 OO.ui.mixin.TitledElement.prototype.getTitle = function () {
6802 return this.title;
6803 };
6804
6805 /**
6806 * Element that can be automatically clipped to visible boundaries.
6807 *
6808 * Whenever the element's natural height changes, you have to call
6809 * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
6810 * clipping correctly.
6811 *
6812 * The dimensions of #$clippableContainer will be compared to the boundaries of the
6813 * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
6814 * then #$clippable will be given a fixed reduced height and/or width and will be made
6815 * scrollable. By default, #$clippable and #$clippableContainer are the same element,
6816 * but you can build a static footer by setting #$clippableContainer to an element that contains
6817 * #$clippable and the footer.
6818 *
6819 * @abstract
6820 * @class
6821 *
6822 * @constructor
6823 * @param {Object} [config] Configuration options
6824 * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
6825 * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
6826 * omit to use #$clippable
6827 */
6828 OO.ui.mixin.ClippableElement = function OoUiMixinClippableElement( config ) {
6829 // Configuration initialization
6830 config = config || {};
6831
6832 // Properties
6833 this.$clippable = null;
6834 this.$clippableContainer = null;
6835 this.clipping = false;
6836 this.clippedHorizontally = false;
6837 this.clippedVertically = false;
6838 this.$clippableScrollableContainer = null;
6839 this.$clippableScroller = null;
6840 this.$clippableWindow = null;
6841 this.idealWidth = null;
6842 this.idealHeight = null;
6843 this.onClippableScrollHandler = this.clip.bind( this );
6844 this.onClippableWindowResizeHandler = this.clip.bind( this );
6845
6846 // Initialization
6847 if ( config.$clippableContainer ) {
6848 this.setClippableContainer( config.$clippableContainer );
6849 }
6850 this.setClippableElement( config.$clippable || this.$element );
6851 };
6852
6853 /* Methods */
6854
6855 /**
6856 * Set clippable element.
6857 *
6858 * If an element is already set, it will be cleaned up before setting up the new element.
6859 *
6860 * @param {jQuery} $clippable Element to make clippable
6861 */
6862 OO.ui.mixin.ClippableElement.prototype.setClippableElement = function ( $clippable ) {
6863 if ( this.$clippable ) {
6864 this.$clippable.removeClass( 'oo-ui-clippableElement-clippable' );
6865 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
6866 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
6867 }
6868
6869 this.$clippable = $clippable.addClass( 'oo-ui-clippableElement-clippable' );
6870 this.clip();
6871 };
6872
6873 /**
6874 * Set clippable container.
6875 *
6876 * This is the container that will be measured when deciding whether to clip. When clipping,
6877 * #$clippable will be resized in order to keep the clippable container fully visible.
6878 *
6879 * If the clippable container is unset, #$clippable will be used.
6880 *
6881 * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
6882 */
6883 OO.ui.mixin.ClippableElement.prototype.setClippableContainer = function ( $clippableContainer ) {
6884 this.$clippableContainer = $clippableContainer;
6885 if ( this.$clippable ) {
6886 this.clip();
6887 }
6888 };
6889
6890 /**
6891 * Toggle clipping.
6892 *
6893 * Do not turn clipping on until after the element is attached to the DOM and visible.
6894 *
6895 * @param {boolean} [clipping] Enable clipping, omit to toggle
6896 * @chainable
6897 */
6898 OO.ui.mixin.ClippableElement.prototype.toggleClipping = function ( clipping ) {
6899 clipping = clipping === undefined ? !this.clipping : !!clipping;
6900
6901 if ( this.clipping !== clipping ) {
6902 this.clipping = clipping;
6903 if ( clipping ) {
6904 this.$clippableScrollableContainer = $( this.getClosestScrollableElementContainer() );
6905 // If the clippable container is the root, we have to listen to scroll events and check
6906 // jQuery.scrollTop on the window because of browser inconsistencies
6907 this.$clippableScroller = this.$clippableScrollableContainer.is( 'html, body' ) ?
6908 $( OO.ui.Element.static.getWindow( this.$clippableScrollableContainer ) ) :
6909 this.$clippableScrollableContainer;
6910 this.$clippableScroller.on( 'scroll', this.onClippableScrollHandler );
6911 this.$clippableWindow = $( this.getElementWindow() )
6912 .on( 'resize', this.onClippableWindowResizeHandler );
6913 // Initial clip after visible
6914 this.clip();
6915 } else {
6916 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
6917 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
6918
6919 this.$clippableScrollableContainer = null;
6920 this.$clippableScroller.off( 'scroll', this.onClippableScrollHandler );
6921 this.$clippableScroller = null;
6922 this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
6923 this.$clippableWindow = null;
6924 }
6925 }
6926
6927 return this;
6928 };
6929
6930 /**
6931 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
6932 *
6933 * @return {boolean} Element will be clipped to the visible area
6934 */
6935 OO.ui.mixin.ClippableElement.prototype.isClipping = function () {
6936 return this.clipping;
6937 };
6938
6939 /**
6940 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
6941 *
6942 * @return {boolean} Part of the element is being clipped
6943 */
6944 OO.ui.mixin.ClippableElement.prototype.isClipped = function () {
6945 return this.clippedHorizontally || this.clippedVertically;
6946 };
6947
6948 /**
6949 * Check if the right of the element is being clipped by the nearest scrollable container.
6950 *
6951 * @return {boolean} Part of the element is being clipped
6952 */
6953 OO.ui.mixin.ClippableElement.prototype.isClippedHorizontally = function () {
6954 return this.clippedHorizontally;
6955 };
6956
6957 /**
6958 * Check if the bottom of the element is being clipped by the nearest scrollable container.
6959 *
6960 * @return {boolean} Part of the element is being clipped
6961 */
6962 OO.ui.mixin.ClippableElement.prototype.isClippedVertically = function () {
6963 return this.clippedVertically;
6964 };
6965
6966 /**
6967 * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
6968 *
6969 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
6970 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
6971 */
6972 OO.ui.mixin.ClippableElement.prototype.setIdealSize = function ( width, height ) {
6973 this.idealWidth = width;
6974 this.idealHeight = height;
6975
6976 if ( !this.clipping ) {
6977 // Update dimensions
6978 this.$clippable.css( { width: width, height: height } );
6979 }
6980 // While clipping, idealWidth and idealHeight are not considered
6981 };
6982
6983 /**
6984 * Clip element to visible boundaries and allow scrolling when needed. Call this method when
6985 * the element's natural height changes.
6986 *
6987 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
6988 * overlapped by, the visible area of the nearest scrollable container.
6989 *
6990 * @chainable
6991 */
6992 OO.ui.mixin.ClippableElement.prototype.clip = function () {
6993 var $container, extraHeight, extraWidth, ccOffset,
6994 $scrollableContainer, scOffset, scHeight, scWidth,
6995 ccWidth, scrollerIsWindow, scrollTop, scrollLeft,
6996 desiredWidth, desiredHeight, allotedWidth, allotedHeight,
6997 naturalWidth, naturalHeight, clipWidth, clipHeight,
6998 buffer = 7; // Chosen by fair dice roll
6999
7000 if ( !this.clipping ) {
7001 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below will fail
7002 return this;
7003 }
7004
7005 $container = this.$clippableContainer || this.$clippable;
7006 extraHeight = $container.outerHeight() - this.$clippable.outerHeight();
7007 extraWidth = $container.outerWidth() - this.$clippable.outerWidth();
7008 ccOffset = $container.offset();
7009 $scrollableContainer = this.$clippableScrollableContainer.is( 'html, body' ) ?
7010 this.$clippableWindow : this.$clippableScrollableContainer;
7011 scOffset = $scrollableContainer.offset() || { top: 0, left: 0 };
7012 scHeight = $scrollableContainer.innerHeight() - buffer;
7013 scWidth = $scrollableContainer.innerWidth() - buffer;
7014 ccWidth = $container.outerWidth() + buffer;
7015 scrollerIsWindow = this.$clippableScroller[ 0 ] === this.$clippableWindow[ 0 ];
7016 scrollTop = scrollerIsWindow ? this.$clippableScroller.scrollTop() : 0;
7017 scrollLeft = scrollerIsWindow ? this.$clippableScroller.scrollLeft() : 0;
7018 desiredWidth = ccOffset.left < 0 ?
7019 ccWidth + ccOffset.left :
7020 ( scOffset.left + scrollLeft + scWidth ) - ccOffset.left;
7021 desiredHeight = ( scOffset.top + scrollTop + scHeight ) - ccOffset.top;
7022 allotedWidth = Math.ceil( desiredWidth - extraWidth );
7023 allotedHeight = Math.ceil( desiredHeight - extraHeight );
7024 naturalWidth = this.$clippable.prop( 'scrollWidth' );
7025 naturalHeight = this.$clippable.prop( 'scrollHeight' );
7026 clipWidth = allotedWidth < naturalWidth;
7027 clipHeight = allotedHeight < naturalHeight;
7028
7029 if ( clipWidth ) {
7030 this.$clippable.css( { overflowX: 'scroll', width: Math.max( 0, allotedWidth ) } );
7031 } else {
7032 this.$clippable.css( { width: this.idealWidth ? this.idealWidth - extraWidth : '', overflowX: '' } );
7033 }
7034 if ( clipHeight ) {
7035 this.$clippable.css( { overflowY: 'scroll', height: Math.max( 0, allotedHeight ) } );
7036 } else {
7037 this.$clippable.css( { height: this.idealHeight ? this.idealHeight - extraHeight : '', overflowY: '' } );
7038 }
7039
7040 // If we stopped clipping in at least one of the dimensions
7041 if ( ( this.clippedHorizontally && !clipWidth ) || ( this.clippedVertically && !clipHeight ) ) {
7042 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
7043 }
7044
7045 this.clippedHorizontally = clipWidth;
7046 this.clippedVertically = clipHeight;
7047
7048 return this;
7049 };
7050
7051 /**
7052 * Element that will stick under a specified container, even when it is inserted elsewhere in the
7053 * document (for example, in a OO.ui.Window's $overlay).
7054 *
7055 * The elements's position is automatically calculated and maintained when window is resized or the
7056 * page is scrolled. If you reposition the container manually, you have to call #position to make
7057 * sure the element is still placed correctly.
7058 *
7059 * As positioning is only possible when both the element and the container are attached to the DOM
7060 * and visible, it's only done after you call #togglePositioning. You might want to do this inside
7061 * the #toggle method to display a floating popup, for example.
7062 *
7063 * @abstract
7064 * @class
7065 *
7066 * @constructor
7067 * @param {Object} [config] Configuration options
7068 * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
7069 * @cfg {jQuery} [$floatableContainer] Node to position below
7070 */
7071 OO.ui.mixin.FloatableElement = function OoUiMixinFloatableElement( config ) {
7072 // Configuration initialization
7073 config = config || {};
7074
7075 // Properties
7076 this.$floatable = null;
7077 this.$floatableContainer = null;
7078 this.$floatableWindow = null;
7079 this.$floatableClosestScrollable = null;
7080 this.onFloatableScrollHandler = this.position.bind( this );
7081 this.onFloatableWindowResizeHandler = this.position.bind( this );
7082
7083 // Initialization
7084 this.setFloatableContainer( config.$floatableContainer );
7085 this.setFloatableElement( config.$floatable || this.$element );
7086 };
7087
7088 /* Methods */
7089
7090 /**
7091 * Set floatable element.
7092 *
7093 * If an element is already set, it will be cleaned up before setting up the new element.
7094 *
7095 * @param {jQuery} $floatable Element to make floatable
7096 */
7097 OO.ui.mixin.FloatableElement.prototype.setFloatableElement = function ( $floatable ) {
7098 if ( this.$floatable ) {
7099 this.$floatable.removeClass( 'oo-ui-floatableElement-floatable' );
7100 this.$floatable.css( { left: '', top: '' } );
7101 }
7102
7103 this.$floatable = $floatable.addClass( 'oo-ui-floatableElement-floatable' );
7104 this.position();
7105 };
7106
7107 /**
7108 * Set floatable container.
7109 *
7110 * The element will be always positioned under the specified container.
7111 *
7112 * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
7113 */
7114 OO.ui.mixin.FloatableElement.prototype.setFloatableContainer = function ( $floatableContainer ) {
7115 this.$floatableContainer = $floatableContainer;
7116 if ( this.$floatable ) {
7117 this.position();
7118 }
7119 };
7120
7121 /**
7122 * Toggle positioning.
7123 *
7124 * Do not turn positioning on until after the element is attached to the DOM and visible.
7125 *
7126 * @param {boolean} [positioning] Enable positioning, omit to toggle
7127 * @chainable
7128 */
7129 OO.ui.mixin.FloatableElement.prototype.togglePositioning = function ( positioning ) {
7130 var closestScrollableOfContainer, closestScrollableOfFloatable;
7131
7132 positioning = positioning === undefined ? !this.positioning : !!positioning;
7133
7134 if ( this.positioning !== positioning ) {
7135 this.positioning = positioning;
7136
7137 closestScrollableOfContainer = OO.ui.Element.static.getClosestScrollableContainer( this.$floatableContainer[ 0 ] );
7138 closestScrollableOfFloatable = OO.ui.Element.static.getClosestScrollableContainer( this.$floatable[ 0 ] );
7139 if ( closestScrollableOfContainer !== closestScrollableOfFloatable ) {
7140 // If the scrollable is the root, we have to listen to scroll events
7141 // on the window because of browser inconsistencies (or do we? someone should verify this)
7142 if ( $( closestScrollableOfContainer ).is( 'html, body' ) ) {
7143 closestScrollableOfContainer = OO.ui.Element.static.getWindow( closestScrollableOfContainer );
7144 }
7145 }
7146
7147 if ( positioning ) {
7148 this.$floatableWindow = $( this.getElementWindow() );
7149 this.$floatableWindow.on( 'resize', this.onFloatableWindowResizeHandler );
7150
7151 if ( closestScrollableOfContainer !== closestScrollableOfFloatable ) {
7152 this.$floatableClosestScrollable = $( closestScrollableOfContainer );
7153 this.$floatableClosestScrollable.on( 'scroll', this.onFloatableScrollHandler );
7154 }
7155
7156 // Initial position after visible
7157 this.position();
7158 } else {
7159 if ( this.$floatableWindow ) {
7160 this.$floatableWindow.off( 'resize', this.onFloatableWindowResizeHandler );
7161 this.$floatableWindow = null;
7162 }
7163
7164 if ( this.$floatableClosestScrollable ) {
7165 this.$floatableClosestScrollable.off( 'scroll', this.onFloatableScrollHandler );
7166 this.$floatableClosestScrollable = null;
7167 }
7168
7169 this.$floatable.css( { left: '', top: '' } );
7170 }
7171 }
7172
7173 return this;
7174 };
7175
7176 /**
7177 * Position the floatable below its container.
7178 *
7179 * This should only be done when both of them are attached to the DOM and visible.
7180 *
7181 * @chainable
7182 */
7183 OO.ui.mixin.FloatableElement.prototype.position = function () {
7184 var pos;
7185
7186 if ( !this.positioning ) {
7187 return this;
7188 }
7189
7190 pos = OO.ui.Element.static.getRelativePosition( this.$floatableContainer, this.$floatable.offsetParent() );
7191
7192 // Position under container
7193 pos.top += this.$floatableContainer.height();
7194 this.$floatable.css( pos );
7195
7196 // We updated the position, so re-evaluate the clipping state.
7197 // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
7198 // will not notice the need to update itself.)
7199 // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
7200 // it not listen to the right events in the right places?
7201 if ( this.clip ) {
7202 this.clip();
7203 }
7204
7205 return this;
7206 };
7207
7208 /**
7209 * AccessKeyedElement is mixed into other classes to provide an `accesskey` attribute.
7210 * Accesskeys allow an user to go to a specific element by using
7211 * a shortcut combination of a browser specific keys + the key
7212 * set to the field.
7213 *
7214 * @example
7215 * // AccessKeyedElement provides an 'accesskey' attribute to the
7216 * // ButtonWidget class
7217 * var button = new OO.ui.ButtonWidget( {
7218 * label: 'Button with Accesskey',
7219 * accessKey: 'k'
7220 * } );
7221 * $( 'body' ).append( button.$element );
7222 *
7223 * @abstract
7224 * @class
7225 *
7226 * @constructor
7227 * @param {Object} [config] Configuration options
7228 * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
7229 * If this config is omitted, the accesskey functionality is applied to $element, the
7230 * element created by the class.
7231 * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
7232 * this config is omitted, no accesskey will be added.
7233 */
7234 OO.ui.mixin.AccessKeyedElement = function OoUiMixinAccessKeyedElement( config ) {
7235 // Configuration initialization
7236 config = config || {};
7237
7238 // Properties
7239 this.$accessKeyed = null;
7240 this.accessKey = null;
7241
7242 // Initialization
7243 this.setAccessKey( config.accessKey || null );
7244 this.setAccessKeyedElement( config.$accessKeyed || this.$element );
7245 };
7246
7247 /* Setup */
7248
7249 OO.initClass( OO.ui.mixin.AccessKeyedElement );
7250
7251 /* Static Properties */
7252
7253 /**
7254 * The access key, a function that returns a key, or `null` for no accesskey.
7255 *
7256 * @static
7257 * @inheritable
7258 * @property {string|Function|null}
7259 */
7260 OO.ui.mixin.AccessKeyedElement.static.accessKey = null;
7261
7262 /* Methods */
7263
7264 /**
7265 * Set the accesskeyed element.
7266 *
7267 * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to the specified element.
7268 * If an element is already set, the mixin's effect on that element is removed before the new element is set up.
7269 *
7270 * @param {jQuery} $accessKeyed Element that should use the 'accesskeyes' functionality
7271 */
7272 OO.ui.mixin.AccessKeyedElement.prototype.setAccessKeyedElement = function ( $accessKeyed ) {
7273 if ( this.$accessKeyed ) {
7274 this.$accessKeyed.removeAttr( 'accesskey' );
7275 }
7276
7277 this.$accessKeyed = $accessKeyed;
7278 if ( this.accessKey ) {
7279 this.$accessKeyed.attr( 'accesskey', this.accessKey );
7280 }
7281 };
7282
7283 /**
7284 * Set accesskey.
7285 *
7286 * @param {string|Function|null} accesskey Key, a function that returns a key, or `null` for no accesskey
7287 * @chainable
7288 */
7289 OO.ui.mixin.AccessKeyedElement.prototype.setAccessKey = function ( accessKey ) {
7290 accessKey = typeof accessKey === 'string' ? OO.ui.resolveMsg( accessKey ) : null;
7291
7292 if ( this.accessKey !== accessKey ) {
7293 if ( this.$accessKeyed ) {
7294 if ( accessKey !== null ) {
7295 this.$accessKeyed.attr( 'accesskey', accessKey );
7296 } else {
7297 this.$accessKeyed.removeAttr( 'accesskey' );
7298 }
7299 }
7300 this.accessKey = accessKey;
7301 }
7302
7303 return this;
7304 };
7305
7306 /**
7307 * Get accesskey.
7308 *
7309 * @return {string} accessKey string
7310 */
7311 OO.ui.mixin.AccessKeyedElement.prototype.getAccessKey = function () {
7312 return this.accessKey;
7313 };
7314
7315 /**
7316 * Tools, together with {@link OO.ui.ToolGroup toolgroups}, constitute {@link OO.ui.Toolbar toolbars}.
7317 * Each tool is configured with a static name, title, and icon and is customized with the command to carry
7318 * out when the tool is selected. Tools must also be registered with a {@link OO.ui.ToolFactory tool factory},
7319 * which creates the tools on demand.
7320 *
7321 * Every Tool subclass must implement two methods:
7322 *
7323 * - {@link #onUpdateState}
7324 * - {@link #onSelect}
7325 *
7326 * Tools are added to toolgroups ({@link OO.ui.ListToolGroup ListToolGroup},
7327 * {@link OO.ui.BarToolGroup BarToolGroup}, or {@link OO.ui.MenuToolGroup MenuToolGroup}), which determine how
7328 * the tool is displayed in the toolbar. See {@link OO.ui.Toolbar toolbars} for an example.
7329 *
7330 * For more information, please see the [OOjs UI documentation on MediaWiki][1].
7331 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
7332 *
7333 * @abstract
7334 * @class
7335 * @extends OO.ui.Widget
7336 * @mixins OO.ui.mixin.IconElement
7337 * @mixins OO.ui.mixin.FlaggedElement
7338 * @mixins OO.ui.mixin.TabIndexedElement
7339 *
7340 * @constructor
7341 * @param {OO.ui.ToolGroup} toolGroup
7342 * @param {Object} [config] Configuration options
7343 * @cfg {string|Function} [title] Title text or a function that returns text. If this config is omitted, the value of
7344 * the {@link #static-title static title} property is used.
7345 *
7346 * The title is used in different ways depending on the type of toolgroup that contains the tool. The
7347 * 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
7348 * part of a {@link OO.ui.ListToolGroup list} or {@link OO.ui.MenuToolGroup menu} toolgroup.
7349 *
7350 * For bar toolgroups, a description of the accelerator key is appended to the title if an accelerator key
7351 * is associated with an action by the same name as the tool and accelerator functionality has been added to the application.
7352 * To add accelerator key functionality, you must subclass OO.ui.Toolbar and override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method.
7353 */
7354 OO.ui.Tool = function OoUiTool( toolGroup, config ) {
7355 // Allow passing positional parameters inside the config object
7356 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
7357 config = toolGroup;
7358 toolGroup = config.toolGroup;
7359 }
7360
7361 // Configuration initialization
7362 config = config || {};
7363
7364 // Parent constructor
7365 OO.ui.Tool.parent.call( this, config );
7366
7367 // Properties
7368 this.toolGroup = toolGroup;
7369 this.toolbar = this.toolGroup.getToolbar();
7370 this.active = false;
7371 this.$title = $( '<span>' );
7372 this.$accel = $( '<span>' );
7373 this.$link = $( '<a>' );
7374 this.title = null;
7375
7376 // Mixin constructors
7377 OO.ui.mixin.IconElement.call( this, config );
7378 OO.ui.mixin.FlaggedElement.call( this, config );
7379 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$link } ) );
7380
7381 // Events
7382 this.toolbar.connect( this, { updateState: 'onUpdateState' } );
7383
7384 // Initialization
7385 this.$title.addClass( 'oo-ui-tool-title' );
7386 this.$accel
7387 .addClass( 'oo-ui-tool-accel' )
7388 .prop( {
7389 // This may need to be changed if the key names are ever localized,
7390 // but for now they are essentially written in English
7391 dir: 'ltr',
7392 lang: 'en'
7393 } );
7394 this.$link
7395 .addClass( 'oo-ui-tool-link' )
7396 .append( this.$icon, this.$title, this.$accel )
7397 .attr( 'role', 'button' );
7398 this.$element
7399 .data( 'oo-ui-tool', this )
7400 .addClass(
7401 'oo-ui-tool ' + 'oo-ui-tool-name-' +
7402 this.constructor.static.name.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' )
7403 )
7404 .toggleClass( 'oo-ui-tool-with-label', this.constructor.static.displayBothIconAndLabel )
7405 .append( this.$link );
7406 this.setTitle( config.title || this.constructor.static.title );
7407 };
7408
7409 /* Setup */
7410
7411 OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
7412 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.IconElement );
7413 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.FlaggedElement );
7414 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.TabIndexedElement );
7415
7416 /* Static Properties */
7417
7418 /**
7419 * @static
7420 * @inheritdoc
7421 */
7422 OO.ui.Tool.static.tagName = 'span';
7423
7424 /**
7425 * Symbolic name of tool.
7426 *
7427 * The symbolic name is used internally to register the tool with a {@link OO.ui.ToolFactory ToolFactory}. It can
7428 * also be used when adding tools to toolgroups.
7429 *
7430 * @abstract
7431 * @static
7432 * @inheritable
7433 * @property {string}
7434 */
7435 OO.ui.Tool.static.name = '';
7436
7437 /**
7438 * Symbolic name of the group.
7439 *
7440 * The group name is used to associate tools with each other so that they can be selected later by
7441 * a {@link OO.ui.ToolGroup toolgroup}.
7442 *
7443 * @abstract
7444 * @static
7445 * @inheritable
7446 * @property {string}
7447 */
7448 OO.ui.Tool.static.group = '';
7449
7450 /**
7451 * 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.
7452 *
7453 * @abstract
7454 * @static
7455 * @inheritable
7456 * @property {string|Function}
7457 */
7458 OO.ui.Tool.static.title = '';
7459
7460 /**
7461 * Display both icon and label when the tool is used in a {@link OO.ui.BarToolGroup bar} toolgroup.
7462 * Normally only the icon is displayed, or only the label if no icon is given.
7463 *
7464 * @static
7465 * @inheritable
7466 * @property {boolean}
7467 */
7468 OO.ui.Tool.static.displayBothIconAndLabel = false;
7469
7470 /**
7471 * Add tool to catch-all groups automatically.
7472 *
7473 * A catch-all group, which contains all tools that do not currently belong to a toolgroup,
7474 * can be included in a toolgroup using the wildcard selector, an asterisk (*).
7475 *
7476 * @static
7477 * @inheritable
7478 * @property {boolean}
7479 */
7480 OO.ui.Tool.static.autoAddToCatchall = true;
7481
7482 /**
7483 * Add tool to named groups automatically.
7484 *
7485 * By default, tools that are configured with a static ‘group’ property are added
7486 * to that group and will be selected when the symbolic name of the group is specified (e.g., when
7487 * toolgroups include tools by group name).
7488 *
7489 * @static
7490 * @property {boolean}
7491 * @inheritable
7492 */
7493 OO.ui.Tool.static.autoAddToGroup = true;
7494
7495 /**
7496 * Check if this tool is compatible with given data.
7497 *
7498 * This is a stub that can be overridden to provide support for filtering tools based on an
7499 * arbitrary piece of information (e.g., where the cursor is in a document). The implementation
7500 * must also call this method so that the compatibility check can be performed.
7501 *
7502 * @static
7503 * @inheritable
7504 * @param {Mixed} data Data to check
7505 * @return {boolean} Tool can be used with data
7506 */
7507 OO.ui.Tool.static.isCompatibleWith = function () {
7508 return false;
7509 };
7510
7511 /* Methods */
7512
7513 /**
7514 * Handle the toolbar state being updated. This method is called when the
7515 * {@link OO.ui.Toolbar#event-updateState 'updateState' event} is emitted on the
7516 * {@link OO.ui.Toolbar Toolbar} that uses this tool, and should set the state of this tool
7517 * depending on application state (usually by calling #setDisabled to enable or disable the tool,
7518 * or #setActive to mark is as currently in-use or not).
7519 *
7520 * This is an abstract method that must be overridden in a concrete subclass.
7521 *
7522 * @method
7523 * @protected
7524 * @abstract
7525 */
7526 OO.ui.Tool.prototype.onUpdateState = null;
7527
7528 /**
7529 * Handle the tool being selected. This method is called when the user triggers this tool,
7530 * usually by clicking on its label/icon.
7531 *
7532 * This is an abstract method that must be overridden in a concrete subclass.
7533 *
7534 * @method
7535 * @protected
7536 * @abstract
7537 */
7538 OO.ui.Tool.prototype.onSelect = null;
7539
7540 /**
7541 * Check if the tool is active.
7542 *
7543 * Tools become active when their #onSelect or #onUpdateState handlers change them to appear pressed
7544 * with the #setActive method. Additional CSS is applied to the tool to reflect the active state.
7545 *
7546 * @return {boolean} Tool is active
7547 */
7548 OO.ui.Tool.prototype.isActive = function () {
7549 return this.active;
7550 };
7551
7552 /**
7553 * Make the tool appear active or inactive.
7554 *
7555 * This method should be called within #onSelect or #onUpdateState event handlers to make the tool
7556 * appear pressed or not.
7557 *
7558 * @param {boolean} state Make tool appear active
7559 */
7560 OO.ui.Tool.prototype.setActive = function ( state ) {
7561 this.active = !!state;
7562 if ( this.active ) {
7563 this.$element.addClass( 'oo-ui-tool-active' );
7564 } else {
7565 this.$element.removeClass( 'oo-ui-tool-active' );
7566 }
7567 };
7568
7569 /**
7570 * Set the tool #title.
7571 *
7572 * @param {string|Function} title Title text or a function that returns text
7573 * @chainable
7574 */
7575 OO.ui.Tool.prototype.setTitle = function ( title ) {
7576 this.title = OO.ui.resolveMsg( title );
7577 this.updateTitle();
7578 return this;
7579 };
7580
7581 /**
7582 * Get the tool #title.
7583 *
7584 * @return {string} Title text
7585 */
7586 OO.ui.Tool.prototype.getTitle = function () {
7587 return this.title;
7588 };
7589
7590 /**
7591 * Get the tool's symbolic name.
7592 *
7593 * @return {string} Symbolic name of tool
7594 */
7595 OO.ui.Tool.prototype.getName = function () {
7596 return this.constructor.static.name;
7597 };
7598
7599 /**
7600 * Update the title.
7601 */
7602 OO.ui.Tool.prototype.updateTitle = function () {
7603 var titleTooltips = this.toolGroup.constructor.static.titleTooltips,
7604 accelTooltips = this.toolGroup.constructor.static.accelTooltips,
7605 accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
7606 tooltipParts = [];
7607
7608 this.$title.text( this.title );
7609 this.$accel.text( accel );
7610
7611 if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
7612 tooltipParts.push( this.title );
7613 }
7614 if ( accelTooltips && typeof accel === 'string' && accel.length ) {
7615 tooltipParts.push( accel );
7616 }
7617 if ( tooltipParts.length ) {
7618 this.$link.attr( 'title', tooltipParts.join( ' ' ) );
7619 } else {
7620 this.$link.removeAttr( 'title' );
7621 }
7622 };
7623
7624 /**
7625 * Destroy tool.
7626 *
7627 * Destroying the tool removes all event handlers and the tool’s DOM elements.
7628 * Call this method whenever you are done using a tool.
7629 */
7630 OO.ui.Tool.prototype.destroy = function () {
7631 this.toolbar.disconnect( this );
7632 this.$element.remove();
7633 };
7634
7635 /**
7636 * Toolbars are complex interface components that permit users to easily access a variety
7637 * of {@link OO.ui.Tool tools} (e.g., formatting commands) and actions, which are additional commands that are
7638 * part of the toolbar, but not configured as tools.
7639 *
7640 * Individual tools are customized and then registered with a {@link OO.ui.ToolFactory tool factory}, which creates
7641 * the tools on demand. Each tool has a symbolic name (used when registering the tool), a title (e.g., ‘Insert
7642 * image’), and an icon.
7643 *
7644 * Individual tools are organized in {@link OO.ui.ToolGroup toolgroups}, which can be {@link OO.ui.MenuToolGroup menus}
7645 * of tools, {@link OO.ui.ListToolGroup lists} of tools, or a single {@link OO.ui.BarToolGroup bar} of tools.
7646 * The arrangement and order of the toolgroups is customized when the toolbar is set up. Tools can be presented in
7647 * any order, but each can only appear once in the toolbar.
7648 *
7649 * The toolbar can be synchronized with the state of the external "application", like a text
7650 * editor's editing area, marking tools as active/inactive (e.g. a 'bold' tool would be shown as
7651 * active when the text cursor was inside bolded text) or enabled/disabled (e.g. a table caption
7652 * tool would be disabled while the user is not editing a table). A state change is signalled by
7653 * emitting the {@link #event-updateState 'updateState' event}, which calls Tools'
7654 * {@link OO.ui.Tool#onUpdateState onUpdateState method}.
7655 *
7656 * The following is an example of a basic toolbar.
7657 *
7658 * @example
7659 * // Example of a toolbar
7660 * // Create the toolbar
7661 * var toolFactory = new OO.ui.ToolFactory();
7662 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
7663 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
7664 *
7665 * // We will be placing status text in this element when tools are used
7666 * var $area = $( '<p>' ).text( 'Toolbar example' );
7667 *
7668 * // Define the tools that we're going to place in our toolbar
7669 *
7670 * // Create a class inheriting from OO.ui.Tool
7671 * function SearchTool() {
7672 * SearchTool.parent.apply( this, arguments );
7673 * }
7674 * OO.inheritClass( SearchTool, OO.ui.Tool );
7675 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
7676 * // of 'icon' and 'title' (displayed icon and text).
7677 * SearchTool.static.name = 'search';
7678 * SearchTool.static.icon = 'search';
7679 * SearchTool.static.title = 'Search...';
7680 * // Defines the action that will happen when this tool is selected (clicked).
7681 * SearchTool.prototype.onSelect = function () {
7682 * $area.text( 'Search tool clicked!' );
7683 * // Never display this tool as "active" (selected).
7684 * this.setActive( false );
7685 * };
7686 * SearchTool.prototype.onUpdateState = function () {};
7687 * // Make this tool available in our toolFactory and thus our toolbar
7688 * toolFactory.register( SearchTool );
7689 *
7690 * // Register two more tools, nothing interesting here
7691 * function SettingsTool() {
7692 * SettingsTool.parent.apply( this, arguments );
7693 * }
7694 * OO.inheritClass( SettingsTool, OO.ui.Tool );
7695 * SettingsTool.static.name = 'settings';
7696 * SettingsTool.static.icon = 'settings';
7697 * SettingsTool.static.title = 'Change settings';
7698 * SettingsTool.prototype.onSelect = function () {
7699 * $area.text( 'Settings tool clicked!' );
7700 * this.setActive( false );
7701 * };
7702 * SettingsTool.prototype.onUpdateState = function () {};
7703 * toolFactory.register( SettingsTool );
7704 *
7705 * // Register two more tools, nothing interesting here
7706 * function StuffTool() {
7707 * StuffTool.parent.apply( this, arguments );
7708 * }
7709 * OO.inheritClass( StuffTool, OO.ui.Tool );
7710 * StuffTool.static.name = 'stuff';
7711 * StuffTool.static.icon = 'ellipsis';
7712 * StuffTool.static.title = 'More stuff';
7713 * StuffTool.prototype.onSelect = function () {
7714 * $area.text( 'More stuff tool clicked!' );
7715 * this.setActive( false );
7716 * };
7717 * StuffTool.prototype.onUpdateState = function () {};
7718 * toolFactory.register( StuffTool );
7719 *
7720 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
7721 * // little popup window (a PopupWidget).
7722 * function HelpTool( toolGroup, config ) {
7723 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
7724 * padded: true,
7725 * label: 'Help',
7726 * head: true
7727 * } }, config ) );
7728 * this.popup.$body.append( '<p>I am helpful!</p>' );
7729 * }
7730 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
7731 * HelpTool.static.name = 'help';
7732 * HelpTool.static.icon = 'help';
7733 * HelpTool.static.title = 'Help';
7734 * toolFactory.register( HelpTool );
7735 *
7736 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
7737 * // used once (but not all defined tools must be used).
7738 * toolbar.setup( [
7739 * {
7740 * // 'bar' tool groups display tools' icons only, side-by-side.
7741 * type: 'bar',
7742 * include: [ 'search', 'help' ]
7743 * },
7744 * {
7745 * // 'list' tool groups display both the titles and icons, in a dropdown list.
7746 * type: 'list',
7747 * indicator: 'down',
7748 * label: 'More',
7749 * include: [ 'settings', 'stuff' ]
7750 * }
7751 * // Note how the tools themselves are toolgroup-agnostic - the same tool can be displayed
7752 * // either in a 'list' or a 'bar'. There is a 'menu' tool group too, not showcased here,
7753 * // since it's more complicated to use. (See the next example snippet on this page.)
7754 * ] );
7755 *
7756 * // Create some UI around the toolbar and place it in the document
7757 * var frame = new OO.ui.PanelLayout( {
7758 * expanded: false,
7759 * framed: true
7760 * } );
7761 * var contentFrame = new OO.ui.PanelLayout( {
7762 * expanded: false,
7763 * padded: true
7764 * } );
7765 * frame.$element.append(
7766 * toolbar.$element,
7767 * contentFrame.$element.append( $area )
7768 * );
7769 * $( 'body' ).append( frame.$element );
7770 *
7771 * // Here is where the toolbar is actually built. This must be done after inserting it into the
7772 * // document.
7773 * toolbar.initialize();
7774 * toolbar.emit( 'updateState' );
7775 *
7776 * The following example extends the previous one to illustrate 'menu' toolgroups and the usage of
7777 * {@link #event-updateState 'updateState' event}.
7778 *
7779 * @example
7780 * // Create the toolbar
7781 * var toolFactory = new OO.ui.ToolFactory();
7782 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
7783 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
7784 *
7785 * // We will be placing status text in this element when tools are used
7786 * var $area = $( '<p>' ).text( 'Toolbar example' );
7787 *
7788 * // Define the tools that we're going to place in our toolbar
7789 *
7790 * // Create a class inheriting from OO.ui.Tool
7791 * function SearchTool() {
7792 * SearchTool.parent.apply( this, arguments );
7793 * }
7794 * OO.inheritClass( SearchTool, OO.ui.Tool );
7795 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
7796 * // of 'icon' and 'title' (displayed icon and text).
7797 * SearchTool.static.name = 'search';
7798 * SearchTool.static.icon = 'search';
7799 * SearchTool.static.title = 'Search...';
7800 * // Defines the action that will happen when this tool is selected (clicked).
7801 * SearchTool.prototype.onSelect = function () {
7802 * $area.text( 'Search tool clicked!' );
7803 * // Never display this tool as "active" (selected).
7804 * this.setActive( false );
7805 * };
7806 * SearchTool.prototype.onUpdateState = function () {};
7807 * // Make this tool available in our toolFactory and thus our toolbar
7808 * toolFactory.register( SearchTool );
7809 *
7810 * // Register two more tools, nothing interesting here
7811 * function SettingsTool() {
7812 * SettingsTool.parent.apply( this, arguments );
7813 * this.reallyActive = false;
7814 * }
7815 * OO.inheritClass( SettingsTool, OO.ui.Tool );
7816 * SettingsTool.static.name = 'settings';
7817 * SettingsTool.static.icon = 'settings';
7818 * SettingsTool.static.title = 'Change settings';
7819 * SettingsTool.prototype.onSelect = function () {
7820 * $area.text( 'Settings tool clicked!' );
7821 * // Toggle the active state on each click
7822 * this.reallyActive = !this.reallyActive;
7823 * this.setActive( this.reallyActive );
7824 * // To update the menu label
7825 * this.toolbar.emit( 'updateState' );
7826 * };
7827 * SettingsTool.prototype.onUpdateState = function () {};
7828 * toolFactory.register( SettingsTool );
7829 *
7830 * // Register two more tools, nothing interesting here
7831 * function StuffTool() {
7832 * StuffTool.parent.apply( this, arguments );
7833 * this.reallyActive = false;
7834 * }
7835 * OO.inheritClass( StuffTool, OO.ui.Tool );
7836 * StuffTool.static.name = 'stuff';
7837 * StuffTool.static.icon = 'ellipsis';
7838 * StuffTool.static.title = 'More stuff';
7839 * StuffTool.prototype.onSelect = function () {
7840 * $area.text( 'More stuff tool clicked!' );
7841 * // Toggle the active state on each click
7842 * this.reallyActive = !this.reallyActive;
7843 * this.setActive( this.reallyActive );
7844 * // To update the menu label
7845 * this.toolbar.emit( 'updateState' );
7846 * };
7847 * StuffTool.prototype.onUpdateState = function () {};
7848 * toolFactory.register( StuffTool );
7849 *
7850 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
7851 * // little popup window (a PopupWidget). 'onUpdateState' is also already implemented.
7852 * function HelpTool( toolGroup, config ) {
7853 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
7854 * padded: true,
7855 * label: 'Help',
7856 * head: true
7857 * } }, config ) );
7858 * this.popup.$body.append( '<p>I am helpful!</p>' );
7859 * }
7860 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
7861 * HelpTool.static.name = 'help';
7862 * HelpTool.static.icon = 'help';
7863 * HelpTool.static.title = 'Help';
7864 * toolFactory.register( HelpTool );
7865 *
7866 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
7867 * // used once (but not all defined tools must be used).
7868 * toolbar.setup( [
7869 * {
7870 * // 'bar' tool groups display tools' icons only, side-by-side.
7871 * type: 'bar',
7872 * include: [ 'search', 'help' ]
7873 * },
7874 * {
7875 * // 'menu' tool groups display both the titles and icons, in a dropdown menu.
7876 * // Menu label indicates which items are selected.
7877 * type: 'menu',
7878 * indicator: 'down',
7879 * include: [ 'settings', 'stuff' ]
7880 * }
7881 * ] );
7882 *
7883 * // Create some UI around the toolbar and place it in the document
7884 * var frame = new OO.ui.PanelLayout( {
7885 * expanded: false,
7886 * framed: true
7887 * } );
7888 * var contentFrame = new OO.ui.PanelLayout( {
7889 * expanded: false,
7890 * padded: true
7891 * } );
7892 * frame.$element.append(
7893 * toolbar.$element,
7894 * contentFrame.$element.append( $area )
7895 * );
7896 * $( 'body' ).append( frame.$element );
7897 *
7898 * // Here is where the toolbar is actually built. This must be done after inserting it into the
7899 * // document.
7900 * toolbar.initialize();
7901 * toolbar.emit( 'updateState' );
7902 *
7903 * @class
7904 * @extends OO.ui.Element
7905 * @mixins OO.EventEmitter
7906 * @mixins OO.ui.mixin.GroupElement
7907 *
7908 * @constructor
7909 * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
7910 * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating toolgroups
7911 * @param {Object} [config] Configuration options
7912 * @cfg {boolean} [actions] Add an actions section to the toolbar. Actions are commands that are included
7913 * in the toolbar, but are not configured as tools. By default, actions are displayed on the right side of
7914 * the toolbar.
7915 * @cfg {boolean} [shadow] Add a shadow below the toolbar.
7916 */
7917 OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
7918 // Allow passing positional parameters inside the config object
7919 if ( OO.isPlainObject( toolFactory ) && config === undefined ) {
7920 config = toolFactory;
7921 toolFactory = config.toolFactory;
7922 toolGroupFactory = config.toolGroupFactory;
7923 }
7924
7925 // Configuration initialization
7926 config = config || {};
7927
7928 // Parent constructor
7929 OO.ui.Toolbar.parent.call( this, config );
7930
7931 // Mixin constructors
7932 OO.EventEmitter.call( this );
7933 OO.ui.mixin.GroupElement.call( this, config );
7934
7935 // Properties
7936 this.toolFactory = toolFactory;
7937 this.toolGroupFactory = toolGroupFactory;
7938 this.groups = [];
7939 this.tools = {};
7940 this.$bar = $( '<div>' );
7941 this.$actions = $( '<div>' );
7942 this.initialized = false;
7943 this.onWindowResizeHandler = this.onWindowResize.bind( this );
7944
7945 // Events
7946 this.$element
7947 .add( this.$bar ).add( this.$group ).add( this.$actions )
7948 .on( 'mousedown keydown', this.onPointerDown.bind( this ) );
7949
7950 // Initialization
7951 this.$group.addClass( 'oo-ui-toolbar-tools' );
7952 if ( config.actions ) {
7953 this.$bar.append( this.$actions.addClass( 'oo-ui-toolbar-actions' ) );
7954 }
7955 this.$bar
7956 .addClass( 'oo-ui-toolbar-bar' )
7957 .append( this.$group, '<div style="clear:both"></div>' );
7958 if ( config.shadow ) {
7959 this.$bar.append( '<div class="oo-ui-toolbar-shadow"></div>' );
7960 }
7961 this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar );
7962 };
7963
7964 /* Setup */
7965
7966 OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
7967 OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
7968 OO.mixinClass( OO.ui.Toolbar, OO.ui.mixin.GroupElement );
7969
7970 /* Events */
7971
7972 /**
7973 * @event updateState
7974 *
7975 * An 'updateState' event must be emitted on the Toolbar (by calling `toolbar.emit( 'updateState' )`)
7976 * every time the state of the application using the toolbar changes, and an update to the state of
7977 * tools is required.
7978 *
7979 * @param {Mixed...} data Application-defined parameters
7980 */
7981
7982 /* Methods */
7983
7984 /**
7985 * Get the tool factory.
7986 *
7987 * @return {OO.ui.ToolFactory} Tool factory
7988 */
7989 OO.ui.Toolbar.prototype.getToolFactory = function () {
7990 return this.toolFactory;
7991 };
7992
7993 /**
7994 * Get the toolgroup factory.
7995 *
7996 * @return {OO.Factory} Toolgroup factory
7997 */
7998 OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
7999 return this.toolGroupFactory;
8000 };
8001
8002 /**
8003 * Handles mouse down events.
8004 *
8005 * @private
8006 * @param {jQuery.Event} e Mouse down event
8007 */
8008 OO.ui.Toolbar.prototype.onPointerDown = function ( e ) {
8009 var $closestWidgetToEvent = $( e.target ).closest( '.oo-ui-widget' ),
8010 $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
8011 if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[ 0 ] === $closestWidgetToToolbar[ 0 ] ) {
8012 return false;
8013 }
8014 };
8015
8016 /**
8017 * Handle window resize event.
8018 *
8019 * @private
8020 * @param {jQuery.Event} e Window resize event
8021 */
8022 OO.ui.Toolbar.prototype.onWindowResize = function () {
8023 this.$element.toggleClass(
8024 'oo-ui-toolbar-narrow',
8025 this.$bar.width() <= this.narrowThreshold
8026 );
8027 };
8028
8029 /**
8030 * Sets up handles and preloads required information for the toolbar to work.
8031 * This must be called after it is attached to a visible document and before doing anything else.
8032 */
8033 OO.ui.Toolbar.prototype.initialize = function () {
8034 if ( !this.initialized ) {
8035 this.initialized = true;
8036 this.narrowThreshold = this.$group.width() + this.$actions.width();
8037 $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
8038 this.onWindowResize();
8039 }
8040 };
8041
8042 /**
8043 * Set up the toolbar.
8044 *
8045 * The toolbar is set up with a list of toolgroup configurations that specify the type of
8046 * toolgroup ({@link OO.ui.BarToolGroup bar}, {@link OO.ui.MenuToolGroup menu}, or {@link OO.ui.ListToolGroup list})
8047 * to add and which tools to include, exclude, promote, or demote within that toolgroup. Please
8048 * see {@link OO.ui.ToolGroup toolgroups} for more information about including tools in toolgroups.
8049 *
8050 * @param {Object.<string,Array>} groups List of toolgroup configurations
8051 * @param {Array|string} [groups.include] Tools to include in the toolgroup
8052 * @param {Array|string} [groups.exclude] Tools to exclude from the toolgroup
8053 * @param {Array|string} [groups.promote] Tools to promote to the beginning of the toolgroup
8054 * @param {Array|string} [groups.demote] Tools to demote to the end of the toolgroup
8055 */
8056 OO.ui.Toolbar.prototype.setup = function ( groups ) {
8057 var i, len, type, group,
8058 items = [],
8059 defaultType = 'bar';
8060
8061 // Cleanup previous groups
8062 this.reset();
8063
8064 // Build out new groups
8065 for ( i = 0, len = groups.length; i < len; i++ ) {
8066 group = groups[ i ];
8067 if ( group.include === '*' ) {
8068 // Apply defaults to catch-all groups
8069 if ( group.type === undefined ) {
8070 group.type = 'list';
8071 }
8072 if ( group.label === undefined ) {
8073 group.label = OO.ui.msg( 'ooui-toolbar-more' );
8074 }
8075 }
8076 // Check type has been registered
8077 type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType;
8078 items.push(
8079 this.getToolGroupFactory().create( type, this, group )
8080 );
8081 }
8082 this.addItems( items );
8083 };
8084
8085 /**
8086 * Remove all tools and toolgroups from the toolbar.
8087 */
8088 OO.ui.Toolbar.prototype.reset = function () {
8089 var i, len;
8090
8091 this.groups = [];
8092 this.tools = {};
8093 for ( i = 0, len = this.items.length; i < len; i++ ) {
8094 this.items[ i ].destroy();
8095 }
8096 this.clearItems();
8097 };
8098
8099 /**
8100 * Destroy the toolbar.
8101 *
8102 * Destroying the toolbar removes all event handlers and DOM elements that constitute the toolbar. Call
8103 * this method whenever you are done using a toolbar.
8104 */
8105 OO.ui.Toolbar.prototype.destroy = function () {
8106 $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
8107 this.reset();
8108 this.$element.remove();
8109 };
8110
8111 /**
8112 * Check if the tool is available.
8113 *
8114 * Available tools are ones that have not yet been added to the toolbar.
8115 *
8116 * @param {string} name Symbolic name of tool
8117 * @return {boolean} Tool is available
8118 */
8119 OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
8120 return !this.tools[ name ];
8121 };
8122
8123 /**
8124 * Prevent tool from being used again.
8125 *
8126 * @param {OO.ui.Tool} tool Tool to reserve
8127 */
8128 OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
8129 this.tools[ tool.getName() ] = tool;
8130 };
8131
8132 /**
8133 * Allow tool to be used again.
8134 *
8135 * @param {OO.ui.Tool} tool Tool to release
8136 */
8137 OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
8138 delete this.tools[ tool.getName() ];
8139 };
8140
8141 /**
8142 * Get accelerator label for tool.
8143 *
8144 * The OOjs UI library does not contain an accelerator system, but this is the hook for one. To
8145 * use an accelerator system, subclass the toolbar and override this method, which is meant to return a label
8146 * that describes the accelerator keys for the tool passed (by symbolic name) to the method.
8147 *
8148 * @param {string} name Symbolic name of tool
8149 * @return {string|undefined} Tool accelerator label if available
8150 */
8151 OO.ui.Toolbar.prototype.getToolAccelerator = function () {
8152 return undefined;
8153 };
8154
8155 /**
8156 * ToolGroups are collections of {@link OO.ui.Tool tools} that are used in a {@link OO.ui.Toolbar toolbar}.
8157 * The type of toolgroup ({@link OO.ui.ListToolGroup list}, {@link OO.ui.BarToolGroup bar}, or {@link OO.ui.MenuToolGroup menu})
8158 * to which a tool belongs determines how the tool is arranged and displayed in the toolbar. Toolgroups
8159 * themselves are created on demand with a {@link OO.ui.ToolGroupFactory toolgroup factory}.
8160 *
8161 * Toolgroups can contain individual tools, groups of tools, or all available tools, as specified
8162 * using the `include` config option. See OO.ui.ToolFactory#extract on documentation of the format.
8163 * The options `exclude`, `promote`, and `demote` support the same formats.
8164 *
8165 * See {@link OO.ui.Toolbar toolbars} for a full example. For more information about toolbars in general,
8166 * please see the [OOjs UI documentation on MediaWiki][1].
8167 *
8168 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
8169 *
8170 * @abstract
8171 * @class
8172 * @extends OO.ui.Widget
8173 * @mixins OO.ui.mixin.GroupElement
8174 *
8175 * @constructor
8176 * @param {OO.ui.Toolbar} toolbar
8177 * @param {Object} [config] Configuration options
8178 * @cfg {Array|string} [include] List of tools to include in the toolgroup, see above.
8179 * @cfg {Array|string} [exclude] List of tools to exclude from the toolgroup, see above.
8180 * @cfg {Array|string} [promote] List of tools to promote to the beginning of the toolgroup, see above.
8181 * @cfg {Array|string} [demote] List of tools to demote to the end of the toolgroup, see above.
8182 * This setting is particularly useful when tools have been added to the toolgroup
8183 * en masse (e.g., via the catch-all selector).
8184 */
8185 OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
8186 // Allow passing positional parameters inside the config object
8187 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
8188 config = toolbar;
8189 toolbar = config.toolbar;
8190 }
8191
8192 // Configuration initialization
8193 config = config || {};
8194
8195 // Parent constructor
8196 OO.ui.ToolGroup.parent.call( this, config );
8197
8198 // Mixin constructors
8199 OO.ui.mixin.GroupElement.call( this, config );
8200
8201 // Properties
8202 this.toolbar = toolbar;
8203 this.tools = {};
8204 this.pressed = null;
8205 this.autoDisabled = false;
8206 this.include = config.include || [];
8207 this.exclude = config.exclude || [];
8208 this.promote = config.promote || [];
8209 this.demote = config.demote || [];
8210 this.onCapturedMouseKeyUpHandler = this.onCapturedMouseKeyUp.bind( this );
8211
8212 // Events
8213 this.$element.on( {
8214 mousedown: this.onMouseKeyDown.bind( this ),
8215 mouseup: this.onMouseKeyUp.bind( this ),
8216 keydown: this.onMouseKeyDown.bind( this ),
8217 keyup: this.onMouseKeyUp.bind( this ),
8218 focus: this.onMouseOverFocus.bind( this ),
8219 blur: this.onMouseOutBlur.bind( this ),
8220 mouseover: this.onMouseOverFocus.bind( this ),
8221 mouseout: this.onMouseOutBlur.bind( this )
8222 } );
8223 this.toolbar.getToolFactory().connect( this, { register: 'onToolFactoryRegister' } );
8224 this.aggregate( { disable: 'itemDisable' } );
8225 this.connect( this, { itemDisable: 'updateDisabled' } );
8226
8227 // Initialization
8228 this.$group.addClass( 'oo-ui-toolGroup-tools' );
8229 this.$element
8230 .addClass( 'oo-ui-toolGroup' )
8231 .append( this.$group );
8232 this.populate();
8233 };
8234
8235 /* Setup */
8236
8237 OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget );
8238 OO.mixinClass( OO.ui.ToolGroup, OO.ui.mixin.GroupElement );
8239
8240 /* Events */
8241
8242 /**
8243 * @event update
8244 */
8245
8246 /* Static Properties */
8247
8248 /**
8249 * Show labels in tooltips.
8250 *
8251 * @static
8252 * @inheritable
8253 * @property {boolean}
8254 */
8255 OO.ui.ToolGroup.static.titleTooltips = false;
8256
8257 /**
8258 * Show acceleration labels in tooltips.
8259 *
8260 * Note: The OOjs UI library does not include an accelerator system, but does contain
8261 * a hook for one. To use an accelerator system, subclass the {@link OO.ui.Toolbar toolbar} and
8262 * override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method, which is
8263 * meant to return a label that describes the accelerator keys for a given tool (e.g., 'Ctrl + M').
8264 *
8265 * @static
8266 * @inheritable
8267 * @property {boolean}
8268 */
8269 OO.ui.ToolGroup.static.accelTooltips = false;
8270
8271 /**
8272 * Automatically disable the toolgroup when all tools are disabled
8273 *
8274 * @static
8275 * @inheritable
8276 * @property {boolean}
8277 */
8278 OO.ui.ToolGroup.static.autoDisable = true;
8279
8280 /* Methods */
8281
8282 /**
8283 * @inheritdoc
8284 */
8285 OO.ui.ToolGroup.prototype.isDisabled = function () {
8286 return this.autoDisabled || OO.ui.ToolGroup.parent.prototype.isDisabled.apply( this, arguments );
8287 };
8288
8289 /**
8290 * @inheritdoc
8291 */
8292 OO.ui.ToolGroup.prototype.updateDisabled = function () {
8293 var i, item, allDisabled = true;
8294
8295 if ( this.constructor.static.autoDisable ) {
8296 for ( i = this.items.length - 1; i >= 0; i-- ) {
8297 item = this.items[ i ];
8298 if ( !item.isDisabled() ) {
8299 allDisabled = false;
8300 break;
8301 }
8302 }
8303 this.autoDisabled = allDisabled;
8304 }
8305 OO.ui.ToolGroup.parent.prototype.updateDisabled.apply( this, arguments );
8306 };
8307
8308 /**
8309 * Handle mouse down and key down events.
8310 *
8311 * @protected
8312 * @param {jQuery.Event} e Mouse down or key down event
8313 */
8314 OO.ui.ToolGroup.prototype.onMouseKeyDown = function ( e ) {
8315 if (
8316 !this.isDisabled() &&
8317 ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
8318 ) {
8319 this.pressed = this.getTargetTool( e );
8320 if ( this.pressed ) {
8321 this.pressed.setActive( true );
8322 this.getElementDocument().addEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler, true );
8323 this.getElementDocument().addEventListener( 'keyup', this.onCapturedMouseKeyUpHandler, true );
8324 }
8325 return false;
8326 }
8327 };
8328
8329 /**
8330 * Handle captured mouse up and key up events.
8331 *
8332 * @protected
8333 * @param {Event} e Mouse up or key up event
8334 */
8335 OO.ui.ToolGroup.prototype.onCapturedMouseKeyUp = function ( e ) {
8336 this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler, true );
8337 this.getElementDocument().removeEventListener( 'keyup', this.onCapturedMouseKeyUpHandler, true );
8338 // onMouseKeyUp may be called a second time, depending on where the mouse is when the button is
8339 // released, but since `this.pressed` will no longer be true, the second call will be ignored.
8340 this.onMouseKeyUp( e );
8341 };
8342
8343 /**
8344 * Handle mouse up and key up events.
8345 *
8346 * @protected
8347 * @param {jQuery.Event} e Mouse up or key up event
8348 */
8349 OO.ui.ToolGroup.prototype.onMouseKeyUp = function ( e ) {
8350 var tool = this.getTargetTool( e );
8351
8352 if (
8353 !this.isDisabled() && this.pressed && this.pressed === tool &&
8354 ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
8355 ) {
8356 this.pressed.onSelect();
8357 this.pressed = null;
8358 return false;
8359 }
8360
8361 this.pressed = null;
8362 };
8363
8364 /**
8365 * Handle mouse over and focus events.
8366 *
8367 * @protected
8368 * @param {jQuery.Event} e Mouse over or focus event
8369 */
8370 OO.ui.ToolGroup.prototype.onMouseOverFocus = function ( e ) {
8371 var tool = this.getTargetTool( e );
8372
8373 if ( this.pressed && this.pressed === tool ) {
8374 this.pressed.setActive( true );
8375 }
8376 };
8377
8378 /**
8379 * Handle mouse out and blur events.
8380 *
8381 * @protected
8382 * @param {jQuery.Event} e Mouse out or blur event
8383 */
8384 OO.ui.ToolGroup.prototype.onMouseOutBlur = function ( e ) {
8385 var tool = this.getTargetTool( e );
8386
8387 if ( this.pressed && this.pressed === tool ) {
8388 this.pressed.setActive( false );
8389 }
8390 };
8391
8392 /**
8393 * Get the closest tool to a jQuery.Event.
8394 *
8395 * Only tool links are considered, which prevents other elements in the tool such as popups from
8396 * triggering tool group interactions.
8397 *
8398 * @private
8399 * @param {jQuery.Event} e
8400 * @return {OO.ui.Tool|null} Tool, `null` if none was found
8401 */
8402 OO.ui.ToolGroup.prototype.getTargetTool = function ( e ) {
8403 var tool,
8404 $item = $( e.target ).closest( '.oo-ui-tool-link' );
8405
8406 if ( $item.length ) {
8407 tool = $item.parent().data( 'oo-ui-tool' );
8408 }
8409
8410 return tool && !tool.isDisabled() ? tool : null;
8411 };
8412
8413 /**
8414 * Handle tool registry register events.
8415 *
8416 * If a tool is registered after the group is created, we must repopulate the list to account for:
8417 *
8418 * - a tool being added that may be included
8419 * - a tool already included being overridden
8420 *
8421 * @protected
8422 * @param {string} name Symbolic name of tool
8423 */
8424 OO.ui.ToolGroup.prototype.onToolFactoryRegister = function () {
8425 this.populate();
8426 };
8427
8428 /**
8429 * Get the toolbar that contains the toolgroup.
8430 *
8431 * @return {OO.ui.Toolbar} Toolbar that contains the toolgroup
8432 */
8433 OO.ui.ToolGroup.prototype.getToolbar = function () {
8434 return this.toolbar;
8435 };
8436
8437 /**
8438 * Add and remove tools based on configuration.
8439 */
8440 OO.ui.ToolGroup.prototype.populate = function () {
8441 var i, len, name, tool,
8442 toolFactory = this.toolbar.getToolFactory(),
8443 names = {},
8444 add = [],
8445 remove = [],
8446 list = this.toolbar.getToolFactory().getTools(
8447 this.include, this.exclude, this.promote, this.demote
8448 );
8449
8450 // Build a list of needed tools
8451 for ( i = 0, len = list.length; i < len; i++ ) {
8452 name = list[ i ];
8453 if (
8454 // Tool exists
8455 toolFactory.lookup( name ) &&
8456 // Tool is available or is already in this group
8457 ( this.toolbar.isToolAvailable( name ) || this.tools[ name ] )
8458 ) {
8459 // Hack to prevent infinite recursion via ToolGroupTool. We need to reserve the tool before
8460 // creating it, but we can't call reserveTool() yet because we haven't created the tool.
8461 this.toolbar.tools[ name ] = true;
8462 tool = this.tools[ name ];
8463 if ( !tool ) {
8464 // Auto-initialize tools on first use
8465 this.tools[ name ] = tool = toolFactory.create( name, this );
8466 tool.updateTitle();
8467 }
8468 this.toolbar.reserveTool( tool );
8469 add.push( tool );
8470 names[ name ] = true;
8471 }
8472 }
8473 // Remove tools that are no longer needed
8474 for ( name in this.tools ) {
8475 if ( !names[ name ] ) {
8476 this.tools[ name ].destroy();
8477 this.toolbar.releaseTool( this.tools[ name ] );
8478 remove.push( this.tools[ name ] );
8479 delete this.tools[ name ];
8480 }
8481 }
8482 if ( remove.length ) {
8483 this.removeItems( remove );
8484 }
8485 // Update emptiness state
8486 if ( add.length ) {
8487 this.$element.removeClass( 'oo-ui-toolGroup-empty' );
8488 } else {
8489 this.$element.addClass( 'oo-ui-toolGroup-empty' );
8490 }
8491 // Re-add tools (moving existing ones to new locations)
8492 this.addItems( add );
8493 // Disabled state may depend on items
8494 this.updateDisabled();
8495 };
8496
8497 /**
8498 * Destroy toolgroup.
8499 */
8500 OO.ui.ToolGroup.prototype.destroy = function () {
8501 var name;
8502
8503 this.clearItems();
8504 this.toolbar.getToolFactory().disconnect( this );
8505 for ( name in this.tools ) {
8506 this.toolbar.releaseTool( this.tools[ name ] );
8507 this.tools[ name ].disconnect( this ).destroy();
8508 delete this.tools[ name ];
8509 }
8510 this.$element.remove();
8511 };
8512
8513 /**
8514 * MessageDialogs display a confirmation or alert message. By default, the rendered dialog box
8515 * consists of a header that contains the dialog title, a body with the message, and a footer that
8516 * contains any {@link OO.ui.ActionWidget action widgets}. The MessageDialog class is the only type
8517 * of {@link OO.ui.Dialog dialog} that is usually instantiated directly.
8518 *
8519 * There are two basic types of message dialogs, confirmation and alert:
8520 *
8521 * - **confirmation**: the dialog title describes what a progressive action will do and the message provides
8522 * more details about the consequences.
8523 * - **alert**: the dialog title describes which event occurred and the message provides more information
8524 * about why the event occurred.
8525 *
8526 * The MessageDialog class specifies two actions: ‘accept’, the primary
8527 * action (e.g., ‘ok’) and ‘reject,’ the safe action (e.g., ‘cancel’). Both will close the window,
8528 * passing along the selected action.
8529 *
8530 * For more information and examples, please see the [OOjs UI documentation on MediaWiki][1].
8531 *
8532 * @example
8533 * // Example: Creating and opening a message dialog window.
8534 * var messageDialog = new OO.ui.MessageDialog();
8535 *
8536 * // Create and append a window manager.
8537 * var windowManager = new OO.ui.WindowManager();
8538 * $( 'body' ).append( windowManager.$element );
8539 * windowManager.addWindows( [ messageDialog ] );
8540 * // Open the window.
8541 * windowManager.openWindow( messageDialog, {
8542 * title: 'Basic message dialog',
8543 * message: 'This is the message'
8544 * } );
8545 *
8546 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Message_Dialogs
8547 *
8548 * @class
8549 * @extends OO.ui.Dialog
8550 *
8551 * @constructor
8552 * @param {Object} [config] Configuration options
8553 */
8554 OO.ui.MessageDialog = function OoUiMessageDialog( config ) {
8555 // Parent constructor
8556 OO.ui.MessageDialog.parent.call( this, config );
8557
8558 // Properties
8559 this.verticalActionLayout = null;
8560
8561 // Initialization
8562 this.$element.addClass( 'oo-ui-messageDialog' );
8563 };
8564
8565 /* Setup */
8566
8567 OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog );
8568
8569 /* Static Properties */
8570
8571 OO.ui.MessageDialog.static.name = 'message';
8572
8573 OO.ui.MessageDialog.static.size = 'small';
8574
8575 OO.ui.MessageDialog.static.verbose = false;
8576
8577 /**
8578 * Dialog title.
8579 *
8580 * The title of a confirmation dialog describes what a progressive action will do. The
8581 * title of an alert dialog describes which event occurred.
8582 *
8583 * @static
8584 * @inheritable
8585 * @property {jQuery|string|Function|null}
8586 */
8587 OO.ui.MessageDialog.static.title = null;
8588
8589 /**
8590 * The message displayed in the dialog body.
8591 *
8592 * A confirmation message describes the consequences of a progressive action. An alert
8593 * message describes why an event occurred.
8594 *
8595 * @static
8596 * @inheritable
8597 * @property {jQuery|string|Function|null}
8598 */
8599 OO.ui.MessageDialog.static.message = null;
8600
8601 // Note that OO.ui.alert() and OO.ui.confirm() rely on these.
8602 OO.ui.MessageDialog.static.actions = [
8603 { action: 'accept', label: OO.ui.deferMsg( 'ooui-dialog-message-accept' ), flags: 'primary' },
8604 { action: 'reject', label: OO.ui.deferMsg( 'ooui-dialog-message-reject' ), flags: 'safe' }
8605 ];
8606
8607 /* Methods */
8608
8609 /**
8610 * @inheritdoc
8611 */
8612 OO.ui.MessageDialog.prototype.setManager = function ( manager ) {
8613 OO.ui.MessageDialog.parent.prototype.setManager.call( this, manager );
8614
8615 // Events
8616 this.manager.connect( this, {
8617 resize: 'onResize'
8618 } );
8619
8620 return this;
8621 };
8622
8623 /**
8624 * @inheritdoc
8625 */
8626 OO.ui.MessageDialog.prototype.onActionResize = function ( action ) {
8627 this.fitActions();
8628 return OO.ui.MessageDialog.parent.prototype.onActionResize.call( this, action );
8629 };
8630
8631 /**
8632 * Handle window resized events.
8633 *
8634 * @private
8635 */
8636 OO.ui.MessageDialog.prototype.onResize = function () {
8637 var dialog = this;
8638 dialog.fitActions();
8639 // Wait for CSS transition to finish and do it again :(
8640 setTimeout( function () {
8641 dialog.fitActions();
8642 }, 300 );
8643 };
8644
8645 /**
8646 * Toggle action layout between vertical and horizontal.
8647 *
8648 * @private
8649 * @param {boolean} [value] Layout actions vertically, omit to toggle
8650 * @chainable
8651 */
8652 OO.ui.MessageDialog.prototype.toggleVerticalActionLayout = function ( value ) {
8653 value = value === undefined ? !this.verticalActionLayout : !!value;
8654
8655 if ( value !== this.verticalActionLayout ) {
8656 this.verticalActionLayout = value;
8657 this.$actions
8658 .toggleClass( 'oo-ui-messageDialog-actions-vertical', value )
8659 .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value );
8660 }
8661
8662 return this;
8663 };
8664
8665 /**
8666 * @inheritdoc
8667 */
8668 OO.ui.MessageDialog.prototype.getActionProcess = function ( action ) {
8669 if ( action ) {
8670 return new OO.ui.Process( function () {
8671 this.close( { action: action } );
8672 }, this );
8673 }
8674 return OO.ui.MessageDialog.parent.prototype.getActionProcess.call( this, action );
8675 };
8676
8677 /**
8678 * @inheritdoc
8679 *
8680 * @param {Object} [data] Dialog opening data
8681 * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
8682 * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
8683 * @param {boolean} [data.verbose] Message is verbose and should be styled as a long message
8684 * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each
8685 * action item
8686 */
8687 OO.ui.MessageDialog.prototype.getSetupProcess = function ( data ) {
8688 data = data || {};
8689
8690 // Parent method
8691 return OO.ui.MessageDialog.parent.prototype.getSetupProcess.call( this, data )
8692 .next( function () {
8693 this.title.setLabel(
8694 data.title !== undefined ? data.title : this.constructor.static.title
8695 );
8696 this.message.setLabel(
8697 data.message !== undefined ? data.message : this.constructor.static.message
8698 );
8699 this.message.$element.toggleClass(
8700 'oo-ui-messageDialog-message-verbose',
8701 data.verbose !== undefined ? data.verbose : this.constructor.static.verbose
8702 );
8703 }, this );
8704 };
8705
8706 /**
8707 * @inheritdoc
8708 */
8709 OO.ui.MessageDialog.prototype.getReadyProcess = function ( data ) {
8710 data = data || {};
8711
8712 // Parent method
8713 return OO.ui.MessageDialog.parent.prototype.getReadyProcess.call( this, data )
8714 .next( function () {
8715 // Focus the primary action button
8716 var actions = this.actions.get();
8717 actions = actions.filter( function ( action ) {
8718 return action.getFlags().indexOf( 'primary' ) > -1;
8719 } );
8720 if ( actions.length > 0 ) {
8721 actions[ 0 ].$button.focus();
8722 }
8723 }, this );
8724 };
8725
8726 /**
8727 * @inheritdoc
8728 */
8729 OO.ui.MessageDialog.prototype.getBodyHeight = function () {
8730 var bodyHeight, oldOverflow,
8731 $scrollable = this.container.$element;
8732
8733 oldOverflow = $scrollable[ 0 ].style.overflow;
8734 $scrollable[ 0 ].style.overflow = 'hidden';
8735
8736 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
8737
8738 bodyHeight = this.text.$element.outerHeight( true );
8739 $scrollable[ 0 ].style.overflow = oldOverflow;
8740
8741 return bodyHeight;
8742 };
8743
8744 /**
8745 * @inheritdoc
8746 */
8747 OO.ui.MessageDialog.prototype.setDimensions = function ( dim ) {
8748 var $scrollable = this.container.$element;
8749 OO.ui.MessageDialog.parent.prototype.setDimensions.call( this, dim );
8750
8751 // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
8752 // Need to do it after transition completes (250ms), add 50ms just in case.
8753 setTimeout( function () {
8754 var oldOverflow = $scrollable[ 0 ].style.overflow;
8755 $scrollable[ 0 ].style.overflow = 'hidden';
8756
8757 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
8758
8759 $scrollable[ 0 ].style.overflow = oldOverflow;
8760 }, 300 );
8761
8762 return this;
8763 };
8764
8765 /**
8766 * @inheritdoc
8767 */
8768 OO.ui.MessageDialog.prototype.initialize = function () {
8769 // Parent method
8770 OO.ui.MessageDialog.parent.prototype.initialize.call( this );
8771
8772 // Properties
8773 this.$actions = $( '<div>' );
8774 this.container = new OO.ui.PanelLayout( {
8775 scrollable: true, classes: [ 'oo-ui-messageDialog-container' ]
8776 } );
8777 this.text = new OO.ui.PanelLayout( {
8778 padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ]
8779 } );
8780 this.message = new OO.ui.LabelWidget( {
8781 classes: [ 'oo-ui-messageDialog-message' ]
8782 } );
8783
8784 // Initialization
8785 this.title.$element.addClass( 'oo-ui-messageDialog-title' );
8786 this.$content.addClass( 'oo-ui-messageDialog-content' );
8787 this.container.$element.append( this.text.$element );
8788 this.text.$element.append( this.title.$element, this.message.$element );
8789 this.$body.append( this.container.$element );
8790 this.$actions.addClass( 'oo-ui-messageDialog-actions' );
8791 this.$foot.append( this.$actions );
8792 };
8793
8794 /**
8795 * @inheritdoc
8796 */
8797 OO.ui.MessageDialog.prototype.attachActions = function () {
8798 var i, len, other, special, others;
8799
8800 // Parent method
8801 OO.ui.MessageDialog.parent.prototype.attachActions.call( this );
8802
8803 special = this.actions.getSpecial();
8804 others = this.actions.getOthers();
8805
8806 if ( special.safe ) {
8807 this.$actions.append( special.safe.$element );
8808 special.safe.toggleFramed( false );
8809 }
8810 if ( others.length ) {
8811 for ( i = 0, len = others.length; i < len; i++ ) {
8812 other = others[ i ];
8813 this.$actions.append( other.$element );
8814 other.toggleFramed( false );
8815 }
8816 }
8817 if ( special.primary ) {
8818 this.$actions.append( special.primary.$element );
8819 special.primary.toggleFramed( false );
8820 }
8821
8822 if ( !this.isOpening() ) {
8823 // If the dialog is currently opening, this will be called automatically soon.
8824 // This also calls #fitActions.
8825 this.updateSize();
8826 }
8827 };
8828
8829 /**
8830 * Fit action actions into columns or rows.
8831 *
8832 * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
8833 *
8834 * @private
8835 */
8836 OO.ui.MessageDialog.prototype.fitActions = function () {
8837 var i, len, action,
8838 previous = this.verticalActionLayout,
8839 actions = this.actions.get();
8840
8841 // Detect clipping
8842 this.toggleVerticalActionLayout( false );
8843 for ( i = 0, len = actions.length; i < len; i++ ) {
8844 action = actions[ i ];
8845 if ( action.$element.innerWidth() < action.$label.outerWidth( true ) ) {
8846 this.toggleVerticalActionLayout( true );
8847 break;
8848 }
8849 }
8850
8851 // Move the body out of the way of the foot
8852 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
8853
8854 if ( this.verticalActionLayout !== previous ) {
8855 // We changed the layout, window height might need to be updated.
8856 this.updateSize();
8857 }
8858 };
8859
8860 /**
8861 * ProcessDialog windows encapsulate a {@link OO.ui.Process process} and all of the code necessary
8862 * to complete it. If the process terminates with an error, a customizable {@link OO.ui.Error error
8863 * interface} alerts users to the trouble, permitting the user to dismiss the error and try again when
8864 * relevant. The ProcessDialog class is always extended and customized with the actions and content
8865 * required for each process.
8866 *
8867 * The process dialog box consists of a header that visually represents the ‘working’ state of long
8868 * processes with an animation. The header contains the dialog title as well as
8869 * two {@link OO.ui.ActionWidget action widgets}: a ‘safe’ action on the left (e.g., ‘Cancel’) and
8870 * a ‘primary’ action on the right (e.g., ‘Done’).
8871 *
8872 * Like other windows, the process dialog is managed by a {@link OO.ui.WindowManager window manager}.
8873 * Please see the [OOjs UI documentation on MediaWiki][1] for more information and examples.
8874 *
8875 * @example
8876 * // Example: Creating and opening a process dialog window.
8877 * function MyProcessDialog( config ) {
8878 * MyProcessDialog.parent.call( this, config );
8879 * }
8880 * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
8881 *
8882 * MyProcessDialog.static.title = 'Process dialog';
8883 * MyProcessDialog.static.actions = [
8884 * { action: 'save', label: 'Done', flags: 'primary' },
8885 * { label: 'Cancel', flags: 'safe' }
8886 * ];
8887 *
8888 * MyProcessDialog.prototype.initialize = function () {
8889 * MyProcessDialog.parent.prototype.initialize.apply( this, arguments );
8890 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
8891 * 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>' );
8892 * this.$body.append( this.content.$element );
8893 * };
8894 * MyProcessDialog.prototype.getActionProcess = function ( action ) {
8895 * var dialog = this;
8896 * if ( action ) {
8897 * return new OO.ui.Process( function () {
8898 * dialog.close( { action: action } );
8899 * } );
8900 * }
8901 * return MyProcessDialog.parent.prototype.getActionProcess.call( this, action );
8902 * };
8903 *
8904 * var windowManager = new OO.ui.WindowManager();
8905 * $( 'body' ).append( windowManager.$element );
8906 *
8907 * var dialog = new MyProcessDialog();
8908 * windowManager.addWindows( [ dialog ] );
8909 * windowManager.openWindow( dialog );
8910 *
8911 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
8912 *
8913 * @abstract
8914 * @class
8915 * @extends OO.ui.Dialog
8916 *
8917 * @constructor
8918 * @param {Object} [config] Configuration options
8919 */
8920 OO.ui.ProcessDialog = function OoUiProcessDialog( config ) {
8921 // Parent constructor
8922 OO.ui.ProcessDialog.parent.call( this, config );
8923
8924 // Properties
8925 this.fitOnOpen = false;
8926
8927 // Initialization
8928 this.$element.addClass( 'oo-ui-processDialog' );
8929 };
8930
8931 /* Setup */
8932
8933 OO.inheritClass( OO.ui.ProcessDialog, OO.ui.Dialog );
8934
8935 /* Methods */
8936
8937 /**
8938 * Handle dismiss button click events.
8939 *
8940 * Hides errors.
8941 *
8942 * @private
8943 */
8944 OO.ui.ProcessDialog.prototype.onDismissErrorButtonClick = function () {
8945 this.hideErrors();
8946 };
8947
8948 /**
8949 * Handle retry button click events.
8950 *
8951 * Hides errors and then tries again.
8952 *
8953 * @private
8954 */
8955 OO.ui.ProcessDialog.prototype.onRetryButtonClick = function () {
8956 this.hideErrors();
8957 this.executeAction( this.currentAction );
8958 };
8959
8960 /**
8961 * @inheritdoc
8962 */
8963 OO.ui.ProcessDialog.prototype.onActionResize = function ( action ) {
8964 if ( this.actions.isSpecial( action ) ) {
8965 this.fitLabel();
8966 }
8967 return OO.ui.ProcessDialog.parent.prototype.onActionResize.call( this, action );
8968 };
8969
8970 /**
8971 * @inheritdoc
8972 */
8973 OO.ui.ProcessDialog.prototype.initialize = function () {
8974 // Parent method
8975 OO.ui.ProcessDialog.parent.prototype.initialize.call( this );
8976
8977 // Properties
8978 this.$navigation = $( '<div>' );
8979 this.$location = $( '<div>' );
8980 this.$safeActions = $( '<div>' );
8981 this.$primaryActions = $( '<div>' );
8982 this.$otherActions = $( '<div>' );
8983 this.dismissButton = new OO.ui.ButtonWidget( {
8984 label: OO.ui.msg( 'ooui-dialog-process-dismiss' )
8985 } );
8986 this.retryButton = new OO.ui.ButtonWidget();
8987 this.$errors = $( '<div>' );
8988 this.$errorsTitle = $( '<div>' );
8989
8990 // Events
8991 this.dismissButton.connect( this, { click: 'onDismissErrorButtonClick' } );
8992 this.retryButton.connect( this, { click: 'onRetryButtonClick' } );
8993
8994 // Initialization
8995 this.title.$element.addClass( 'oo-ui-processDialog-title' );
8996 this.$location
8997 .append( this.title.$element )
8998 .addClass( 'oo-ui-processDialog-location' );
8999 this.$safeActions.addClass( 'oo-ui-processDialog-actions-safe' );
9000 this.$primaryActions.addClass( 'oo-ui-processDialog-actions-primary' );
9001 this.$otherActions.addClass( 'oo-ui-processDialog-actions-other' );
9002 this.$errorsTitle
9003 .addClass( 'oo-ui-processDialog-errors-title' )
9004 .text( OO.ui.msg( 'ooui-dialog-process-error' ) );
9005 this.$errors
9006 .addClass( 'oo-ui-processDialog-errors oo-ui-element-hidden' )
9007 .append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element );
9008 this.$content
9009 .addClass( 'oo-ui-processDialog-content' )
9010 .append( this.$errors );
9011 this.$navigation
9012 .addClass( 'oo-ui-processDialog-navigation' )
9013 .append( this.$safeActions, this.$location, this.$primaryActions );
9014 this.$head.append( this.$navigation );
9015 this.$foot.append( this.$otherActions );
9016 };
9017
9018 /**
9019 * @inheritdoc
9020 */
9021 OO.ui.ProcessDialog.prototype.getActionWidgets = function ( actions ) {
9022 var i, len, widgets = [];
9023 for ( i = 0, len = actions.length; i < len; i++ ) {
9024 widgets.push(
9025 new OO.ui.ActionWidget( $.extend( { framed: true }, actions[ i ] ) )
9026 );
9027 }
9028 return widgets;
9029 };
9030
9031 /**
9032 * @inheritdoc
9033 */
9034 OO.ui.ProcessDialog.prototype.attachActions = function () {
9035 var i, len, other, special, others;
9036
9037 // Parent method
9038 OO.ui.ProcessDialog.parent.prototype.attachActions.call( this );
9039
9040 special = this.actions.getSpecial();
9041 others = this.actions.getOthers();
9042 if ( special.primary ) {
9043 this.$primaryActions.append( special.primary.$element );
9044 }
9045 for ( i = 0, len = others.length; i < len; i++ ) {
9046 other = others[ i ];
9047 this.$otherActions.append( other.$element );
9048 }
9049 if ( special.safe ) {
9050 this.$safeActions.append( special.safe.$element );
9051 }
9052
9053 this.fitLabel();
9054 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
9055 };
9056
9057 /**
9058 * @inheritdoc
9059 */
9060 OO.ui.ProcessDialog.prototype.executeAction = function ( action ) {
9061 var process = this;
9062 return OO.ui.ProcessDialog.parent.prototype.executeAction.call( this, action )
9063 .fail( function ( errors ) {
9064 process.showErrors( errors || [] );
9065 } );
9066 };
9067
9068 /**
9069 * @inheritdoc
9070 */
9071 OO.ui.ProcessDialog.prototype.setDimensions = function () {
9072 // Parent method
9073 OO.ui.ProcessDialog.parent.prototype.setDimensions.apply( this, arguments );
9074
9075 this.fitLabel();
9076 };
9077
9078 /**
9079 * Fit label between actions.
9080 *
9081 * @private
9082 * @chainable
9083 */
9084 OO.ui.ProcessDialog.prototype.fitLabel = function () {
9085 var safeWidth, primaryWidth, biggerWidth, labelWidth, navigationWidth, leftWidth, rightWidth,
9086 size = this.getSizeProperties();
9087
9088 if ( typeof size.width !== 'number' ) {
9089 if ( this.isOpened() ) {
9090 navigationWidth = this.$head.width() - 20;
9091 } else if ( this.isOpening() ) {
9092 if ( !this.fitOnOpen ) {
9093 // Size is relative and the dialog isn't open yet, so wait.
9094 this.manager.opening.done( this.fitLabel.bind( this ) );
9095 this.fitOnOpen = true;
9096 }
9097 return;
9098 } else {
9099 return;
9100 }
9101 } else {
9102 navigationWidth = size.width - 20;
9103 }
9104
9105 safeWidth = this.$safeActions.is( ':visible' ) ? this.$safeActions.width() : 0;
9106 primaryWidth = this.$primaryActions.is( ':visible' ) ? this.$primaryActions.width() : 0;
9107 biggerWidth = Math.max( safeWidth, primaryWidth );
9108
9109 labelWidth = this.title.$element.width();
9110
9111 if ( 2 * biggerWidth + labelWidth < navigationWidth ) {
9112 // We have enough space to center the label
9113 leftWidth = rightWidth = biggerWidth;
9114 } else {
9115 // Let's hope we at least have enough space not to overlap, because we can't wrap the label…
9116 if ( this.getDir() === 'ltr' ) {
9117 leftWidth = safeWidth;
9118 rightWidth = primaryWidth;
9119 } else {
9120 leftWidth = primaryWidth;
9121 rightWidth = safeWidth;
9122 }
9123 }
9124
9125 this.$location.css( { paddingLeft: leftWidth, paddingRight: rightWidth } );
9126
9127 return this;
9128 };
9129
9130 /**
9131 * Handle errors that occurred during accept or reject processes.
9132 *
9133 * @private
9134 * @param {OO.ui.Error[]|OO.ui.Error} errors Errors to be handled
9135 */
9136 OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
9137 var i, len, $item, actions,
9138 items = [],
9139 abilities = {},
9140 recoverable = true,
9141 warning = false;
9142
9143 if ( errors instanceof OO.ui.Error ) {
9144 errors = [ errors ];
9145 }
9146
9147 for ( i = 0, len = errors.length; i < len; i++ ) {
9148 if ( !errors[ i ].isRecoverable() ) {
9149 recoverable = false;
9150 }
9151 if ( errors[ i ].isWarning() ) {
9152 warning = true;
9153 }
9154 $item = $( '<div>' )
9155 .addClass( 'oo-ui-processDialog-error' )
9156 .append( errors[ i ].getMessage() );
9157 items.push( $item[ 0 ] );
9158 }
9159 this.$errorItems = $( items );
9160 if ( recoverable ) {
9161 abilities[ this.currentAction ] = true;
9162 // Copy the flags from the first matching action
9163 actions = this.actions.get( { actions: this.currentAction } );
9164 if ( actions.length ) {
9165 this.retryButton.clearFlags().setFlags( actions[ 0 ].getFlags() );
9166 }
9167 } else {
9168 abilities[ this.currentAction ] = false;
9169 this.actions.setAbilities( abilities );
9170 }
9171 if ( warning ) {
9172 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-continue' ) );
9173 } else {
9174 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-retry' ) );
9175 }
9176 this.retryButton.toggle( recoverable );
9177 this.$errorsTitle.after( this.$errorItems );
9178 this.$errors.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 );
9179 };
9180
9181 /**
9182 * Hide errors.
9183 *
9184 * @private
9185 */
9186 OO.ui.ProcessDialog.prototype.hideErrors = function () {
9187 this.$errors.addClass( 'oo-ui-element-hidden' );
9188 if ( this.$errorItems ) {
9189 this.$errorItems.remove();
9190 this.$errorItems = null;
9191 }
9192 };
9193
9194 /**
9195 * @inheritdoc
9196 */
9197 OO.ui.ProcessDialog.prototype.getTeardownProcess = function ( data ) {
9198 // Parent method
9199 return OO.ui.ProcessDialog.parent.prototype.getTeardownProcess.call( this, data )
9200 .first( function () {
9201 // Make sure to hide errors
9202 this.hideErrors();
9203 this.fitOnOpen = false;
9204 }, this );
9205 };
9206
9207 /**
9208 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
9209 * which is a widget that is specified by reference before any optional configuration settings.
9210 *
9211 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
9212 *
9213 * - **left**: The label is placed before the field-widget and aligned with the left margin.
9214 * A left-alignment is used for forms with many fields.
9215 * - **right**: The label is placed before the field-widget and aligned to the right margin.
9216 * A right-alignment is used for long but familiar forms which users tab through,
9217 * verifying the current field with a quick glance at the label.
9218 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
9219 * that users fill out from top to bottom.
9220 * - **inline**: The label is placed after the field-widget and aligned to the left.
9221 * An inline-alignment is best used with checkboxes or radio buttons.
9222 *
9223 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
9224 * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
9225 *
9226 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
9227 * @class
9228 * @extends OO.ui.Layout
9229 * @mixins OO.ui.mixin.LabelElement
9230 * @mixins OO.ui.mixin.TitledElement
9231 *
9232 * @constructor
9233 * @param {OO.ui.Widget} fieldWidget Field widget
9234 * @param {Object} [config] Configuration options
9235 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
9236 * @cfg {Array} [errors] Error messages about the widget, which will be displayed below the widget.
9237 * The array may contain strings or OO.ui.HtmlSnippet instances.
9238 * @cfg {Array} [notices] Notices about the widget, which will be displayed below the widget.
9239 * The array may contain strings or OO.ui.HtmlSnippet instances.
9240 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
9241 * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
9242 * For important messages, you are advised to use `notices`, as they are always shown.
9243 *
9244 * @throws {Error} An error is thrown if no widget is specified
9245 */
9246 OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
9247 var hasInputWidget, div;
9248
9249 // Allow passing positional parameters inside the config object
9250 if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
9251 config = fieldWidget;
9252 fieldWidget = config.fieldWidget;
9253 }
9254
9255 // Make sure we have required constructor arguments
9256 if ( fieldWidget === undefined ) {
9257 throw new Error( 'Widget not found' );
9258 }
9259
9260 hasInputWidget = fieldWidget.constructor.static.supportsSimpleLabel;
9261
9262 // Configuration initialization
9263 config = $.extend( { align: 'left' }, config );
9264
9265 // Parent constructor
9266 OO.ui.FieldLayout.parent.call( this, config );
9267
9268 // Mixin constructors
9269 OO.ui.mixin.LabelElement.call( this, config );
9270 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
9271
9272 // Properties
9273 this.fieldWidget = fieldWidget;
9274 this.errors = [];
9275 this.notices = [];
9276 this.$field = $( '<div>' );
9277 this.$messages = $( '<ul>' );
9278 this.$body = $( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
9279 this.align = null;
9280 if ( config.help ) {
9281 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
9282 classes: [ 'oo-ui-fieldLayout-help' ],
9283 framed: false,
9284 icon: 'info'
9285 } );
9286
9287 div = $( '<div>' );
9288 if ( config.help instanceof OO.ui.HtmlSnippet ) {
9289 div.html( config.help.toString() );
9290 } else {
9291 div.text( config.help );
9292 }
9293 this.popupButtonWidget.getPopup().$body.append(
9294 div.addClass( 'oo-ui-fieldLayout-help-content' )
9295 );
9296 this.$help = this.popupButtonWidget.$element;
9297 } else {
9298 this.$help = $( [] );
9299 }
9300
9301 // Events
9302 if ( hasInputWidget ) {
9303 this.$label.on( 'click', this.onLabelClick.bind( this ) );
9304 }
9305 this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
9306
9307 // Initialization
9308 this.$element
9309 .addClass( 'oo-ui-fieldLayout' )
9310 .append( this.$help, this.$body );
9311 this.$body.addClass( 'oo-ui-fieldLayout-body' );
9312 this.$messages.addClass( 'oo-ui-fieldLayout-messages' );
9313 this.$field
9314 .addClass( 'oo-ui-fieldLayout-field' )
9315 .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget.isDisabled() )
9316 .append( this.fieldWidget.$element );
9317
9318 this.setErrors( config.errors || [] );
9319 this.setNotices( config.notices || [] );
9320 this.setAlignment( config.align );
9321 };
9322
9323 /* Setup */
9324
9325 OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
9326 OO.mixinClass( OO.ui.FieldLayout, OO.ui.mixin.LabelElement );
9327 OO.mixinClass( OO.ui.FieldLayout, OO.ui.mixin.TitledElement );
9328
9329 /* Methods */
9330
9331 /**
9332 * Handle field disable events.
9333 *
9334 * @private
9335 * @param {boolean} value Field is disabled
9336 */
9337 OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
9338 this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
9339 };
9340
9341 /**
9342 * Handle label mouse click events.
9343 *
9344 * @private
9345 * @param {jQuery.Event} e Mouse click event
9346 */
9347 OO.ui.FieldLayout.prototype.onLabelClick = function () {
9348 this.fieldWidget.simulateLabelClick();
9349 return false;
9350 };
9351
9352 /**
9353 * Get the widget contained by the field.
9354 *
9355 * @return {OO.ui.Widget} Field widget
9356 */
9357 OO.ui.FieldLayout.prototype.getField = function () {
9358 return this.fieldWidget;
9359 };
9360
9361 /**
9362 * @protected
9363 * @param {string} kind 'error' or 'notice'
9364 * @param {string|OO.ui.HtmlSnippet} text
9365 * @return {jQuery}
9366 */
9367 OO.ui.FieldLayout.prototype.makeMessage = function ( kind, text ) {
9368 var $listItem, $icon, message;
9369 $listItem = $( '<li>' );
9370 if ( kind === 'error' ) {
9371 $icon = new OO.ui.IconWidget( { icon: 'alert', flags: [ 'warning' ] } ).$element;
9372 } else if ( kind === 'notice' ) {
9373 $icon = new OO.ui.IconWidget( { icon: 'info' } ).$element;
9374 } else {
9375 $icon = '';
9376 }
9377 message = new OO.ui.LabelWidget( { label: text } );
9378 $listItem
9379 .append( $icon, message.$element )
9380 .addClass( 'oo-ui-fieldLayout-messages-' + kind );
9381 return $listItem;
9382 };
9383
9384 /**
9385 * Set the field alignment mode.
9386 *
9387 * @private
9388 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
9389 * @chainable
9390 */
9391 OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
9392 if ( value !== this.align ) {
9393 // Default to 'left'
9394 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) {
9395 value = 'left';
9396 }
9397 // Reorder elements
9398 if ( value === 'inline' ) {
9399 this.$body.append( this.$field, this.$label );
9400 } else {
9401 this.$body.append( this.$label, this.$field );
9402 }
9403 // Set classes. The following classes can be used here:
9404 // * oo-ui-fieldLayout-align-left
9405 // * oo-ui-fieldLayout-align-right
9406 // * oo-ui-fieldLayout-align-top
9407 // * oo-ui-fieldLayout-align-inline
9408 if ( this.align ) {
9409 this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
9410 }
9411 this.$element.addClass( 'oo-ui-fieldLayout-align-' + value );
9412 this.align = value;
9413 }
9414
9415 return this;
9416 };
9417
9418 /**
9419 * Set the list of error messages.
9420 *
9421 * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
9422 * The array may contain strings or OO.ui.HtmlSnippet instances.
9423 * @chainable
9424 */
9425 OO.ui.FieldLayout.prototype.setErrors = function ( errors ) {
9426 this.errors = errors.slice();
9427 this.updateMessages();
9428 return this;
9429 };
9430
9431 /**
9432 * Set the list of notice messages.
9433 *
9434 * @param {Array} notices Notices about the widget, which will be displayed below the widget.
9435 * The array may contain strings or OO.ui.HtmlSnippet instances.
9436 * @chainable
9437 */
9438 OO.ui.FieldLayout.prototype.setNotices = function ( notices ) {
9439 this.notices = notices.slice();
9440 this.updateMessages();
9441 return this;
9442 };
9443
9444 /**
9445 * Update the rendering of error and notice messages.
9446 *
9447 * @private
9448 */
9449 OO.ui.FieldLayout.prototype.updateMessages = function () {
9450 var i;
9451 this.$messages.empty();
9452
9453 if ( this.errors.length || this.notices.length ) {
9454 this.$body.after( this.$messages );
9455 } else {
9456 this.$messages.remove();
9457 return;
9458 }
9459
9460 for ( i = 0; i < this.notices.length; i++ ) {
9461 this.$messages.append( this.makeMessage( 'notice', this.notices[ i ] ) );
9462 }
9463 for ( i = 0; i < this.errors.length; i++ ) {
9464 this.$messages.append( this.makeMessage( 'error', this.errors[ i ] ) );
9465 }
9466 };
9467
9468 /**
9469 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
9470 * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
9471 * is required and is specified before any optional configuration settings.
9472 *
9473 * Labels can be aligned in one of four ways:
9474 *
9475 * - **left**: The label is placed before the field-widget and aligned with the left margin.
9476 * A left-alignment is used for forms with many fields.
9477 * - **right**: The label is placed before the field-widget and aligned to the right margin.
9478 * A right-alignment is used for long but familiar forms which users tab through,
9479 * verifying the current field with a quick glance at the label.
9480 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
9481 * that users fill out from top to bottom.
9482 * - **inline**: The label is placed after the field-widget and aligned to the left.
9483 * An inline-alignment is best used with checkboxes or radio buttons.
9484 *
9485 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
9486 * text is specified.
9487 *
9488 * @example
9489 * // Example of an ActionFieldLayout
9490 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
9491 * new OO.ui.TextInputWidget( {
9492 * placeholder: 'Field widget'
9493 * } ),
9494 * new OO.ui.ButtonWidget( {
9495 * label: 'Button'
9496 * } ),
9497 * {
9498 * label: 'An ActionFieldLayout. This label is aligned top',
9499 * align: 'top',
9500 * help: 'This is help text'
9501 * }
9502 * );
9503 *
9504 * $( 'body' ).append( actionFieldLayout.$element );
9505 *
9506 * @class
9507 * @extends OO.ui.FieldLayout
9508 *
9509 * @constructor
9510 * @param {OO.ui.Widget} fieldWidget Field widget
9511 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
9512 */
9513 OO.ui.ActionFieldLayout = function OoUiActionFieldLayout( fieldWidget, buttonWidget, config ) {
9514 // Allow passing positional parameters inside the config object
9515 if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
9516 config = fieldWidget;
9517 fieldWidget = config.fieldWidget;
9518 buttonWidget = config.buttonWidget;
9519 }
9520
9521 // Parent constructor
9522 OO.ui.ActionFieldLayout.parent.call( this, fieldWidget, config );
9523
9524 // Properties
9525 this.buttonWidget = buttonWidget;
9526 this.$button = $( '<div>' );
9527 this.$input = $( '<div>' );
9528
9529 // Initialization
9530 this.$element
9531 .addClass( 'oo-ui-actionFieldLayout' );
9532 this.$button
9533 .addClass( 'oo-ui-actionFieldLayout-button' )
9534 .append( this.buttonWidget.$element );
9535 this.$input
9536 .addClass( 'oo-ui-actionFieldLayout-input' )
9537 .append( this.fieldWidget.$element );
9538 this.$field
9539 .append( this.$input, this.$button );
9540 };
9541
9542 /* Setup */
9543
9544 OO.inheritClass( OO.ui.ActionFieldLayout, OO.ui.FieldLayout );
9545
9546 /**
9547 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
9548 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
9549 * configured with a label as well. For more information and examples,
9550 * please see the [OOjs UI documentation on MediaWiki][1].
9551 *
9552 * @example
9553 * // Example of a fieldset layout
9554 * var input1 = new OO.ui.TextInputWidget( {
9555 * placeholder: 'A text input field'
9556 * } );
9557 *
9558 * var input2 = new OO.ui.TextInputWidget( {
9559 * placeholder: 'A text input field'
9560 * } );
9561 *
9562 * var fieldset = new OO.ui.FieldsetLayout( {
9563 * label: 'Example of a fieldset layout'
9564 * } );
9565 *
9566 * fieldset.addItems( [
9567 * new OO.ui.FieldLayout( input1, {
9568 * label: 'Field One'
9569 * } ),
9570 * new OO.ui.FieldLayout( input2, {
9571 * label: 'Field Two'
9572 * } )
9573 * ] );
9574 * $( 'body' ).append( fieldset.$element );
9575 *
9576 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
9577 *
9578 * @class
9579 * @extends OO.ui.Layout
9580 * @mixins OO.ui.mixin.IconElement
9581 * @mixins OO.ui.mixin.LabelElement
9582 * @mixins OO.ui.mixin.GroupElement
9583 *
9584 * @constructor
9585 * @param {Object} [config] Configuration options
9586 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
9587 */
9588 OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
9589 // Configuration initialization
9590 config = config || {};
9591
9592 // Parent constructor
9593 OO.ui.FieldsetLayout.parent.call( this, config );
9594
9595 // Mixin constructors
9596 OO.ui.mixin.IconElement.call( this, config );
9597 OO.ui.mixin.LabelElement.call( this, config );
9598 OO.ui.mixin.GroupElement.call( this, config );
9599
9600 if ( config.help ) {
9601 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
9602 classes: [ 'oo-ui-fieldsetLayout-help' ],
9603 framed: false,
9604 icon: 'info'
9605 } );
9606
9607 this.popupButtonWidget.getPopup().$body.append(
9608 $( '<div>' )
9609 .text( config.help )
9610 .addClass( 'oo-ui-fieldsetLayout-help-content' )
9611 );
9612 this.$help = this.popupButtonWidget.$element;
9613 } else {
9614 this.$help = $( [] );
9615 }
9616
9617 // Initialization
9618 this.$element
9619 .addClass( 'oo-ui-fieldsetLayout' )
9620 .prepend( this.$help, this.$icon, this.$label, this.$group );
9621 if ( Array.isArray( config.items ) ) {
9622 this.addItems( config.items );
9623 }
9624 };
9625
9626 /* Setup */
9627
9628 OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
9629 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.IconElement );
9630 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.LabelElement );
9631 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.GroupElement );
9632
9633 /**
9634 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
9635 * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
9636 * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
9637 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
9638 *
9639 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
9640 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
9641 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
9642 * some fancier controls. Some controls have both regular and InputWidget variants, for example
9643 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
9644 * often have simplified APIs to match the capabilities of HTML forms.
9645 * See the [OOjs UI Inputs documentation on MediaWiki] [2] for more information about InputWidgets.
9646 *
9647 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Forms
9648 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
9649 *
9650 * @example
9651 * // Example of a form layout that wraps a fieldset layout
9652 * var input1 = new OO.ui.TextInputWidget( {
9653 * placeholder: 'Username'
9654 * } );
9655 * var input2 = new OO.ui.TextInputWidget( {
9656 * placeholder: 'Password',
9657 * type: 'password'
9658 * } );
9659 * var submit = new OO.ui.ButtonInputWidget( {
9660 * label: 'Submit'
9661 * } );
9662 *
9663 * var fieldset = new OO.ui.FieldsetLayout( {
9664 * label: 'A form layout'
9665 * } );
9666 * fieldset.addItems( [
9667 * new OO.ui.FieldLayout( input1, {
9668 * label: 'Username',
9669 * align: 'top'
9670 * } ),
9671 * new OO.ui.FieldLayout( input2, {
9672 * label: 'Password',
9673 * align: 'top'
9674 * } ),
9675 * new OO.ui.FieldLayout( submit )
9676 * ] );
9677 * var form = new OO.ui.FormLayout( {
9678 * items: [ fieldset ],
9679 * action: '/api/formhandler',
9680 * method: 'get'
9681 * } )
9682 * $( 'body' ).append( form.$element );
9683 *
9684 * @class
9685 * @extends OO.ui.Layout
9686 * @mixins OO.ui.mixin.GroupElement
9687 *
9688 * @constructor
9689 * @param {Object} [config] Configuration options
9690 * @cfg {string} [method] HTML form `method` attribute
9691 * @cfg {string} [action] HTML form `action` attribute
9692 * @cfg {string} [enctype] HTML form `enctype` attribute
9693 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
9694 */
9695 OO.ui.FormLayout = function OoUiFormLayout( config ) {
9696 var action;
9697
9698 // Configuration initialization
9699 config = config || {};
9700
9701 // Parent constructor
9702 OO.ui.FormLayout.parent.call( this, config );
9703
9704 // Mixin constructors
9705 OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
9706
9707 // Events
9708 this.$element.on( 'submit', this.onFormSubmit.bind( this ) );
9709
9710 // Make sure the action is safe
9711 action = config.action;
9712 if ( action !== undefined && !OO.ui.isSafeUrl( action ) ) {
9713 action = './' + action;
9714 }
9715
9716 // Initialization
9717 this.$element
9718 .addClass( 'oo-ui-formLayout' )
9719 .attr( {
9720 method: config.method,
9721 action: action,
9722 enctype: config.enctype
9723 } );
9724 if ( Array.isArray( config.items ) ) {
9725 this.addItems( config.items );
9726 }
9727 };
9728
9729 /* Setup */
9730
9731 OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout );
9732 OO.mixinClass( OO.ui.FormLayout, OO.ui.mixin.GroupElement );
9733
9734 /* Events */
9735
9736 /**
9737 * A 'submit' event is emitted when the form is submitted.
9738 *
9739 * @event submit
9740 */
9741
9742 /* Static Properties */
9743
9744 OO.ui.FormLayout.static.tagName = 'form';
9745
9746 /* Methods */
9747
9748 /**
9749 * Handle form submit events.
9750 *
9751 * @private
9752 * @param {jQuery.Event} e Submit event
9753 * @fires submit
9754 */
9755 OO.ui.FormLayout.prototype.onFormSubmit = function () {
9756 if ( this.emit( 'submit' ) ) {
9757 return false;
9758 }
9759 };
9760
9761 /**
9762 * 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)
9763 * and its size is customized with the #menuSize config. The content area will fill all remaining space.
9764 *
9765 * @example
9766 * var menuLayout = new OO.ui.MenuLayout( {
9767 * position: 'top'
9768 * } ),
9769 * menuPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
9770 * contentPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
9771 * select = new OO.ui.SelectWidget( {
9772 * items: [
9773 * new OO.ui.OptionWidget( {
9774 * data: 'before',
9775 * label: 'Before',
9776 * } ),
9777 * new OO.ui.OptionWidget( {
9778 * data: 'after',
9779 * label: 'After',
9780 * } ),
9781 * new OO.ui.OptionWidget( {
9782 * data: 'top',
9783 * label: 'Top',
9784 * } ),
9785 * new OO.ui.OptionWidget( {
9786 * data: 'bottom',
9787 * label: 'Bottom',
9788 * } )
9789 * ]
9790 * } ).on( 'select', function ( item ) {
9791 * menuLayout.setMenuPosition( item.getData() );
9792 * } );
9793 *
9794 * menuLayout.$menu.append(
9795 * menuPanel.$element.append( '<b>Menu panel</b>', select.$element )
9796 * );
9797 * menuLayout.$content.append(
9798 * contentPanel.$element.append( '<b>Content panel</b>', '<p>Note that the menu is positioned relative to the content panel: top, bottom, after, before.</p>')
9799 * );
9800 * $( 'body' ).append( menuLayout.$element );
9801 *
9802 * If menu size needs to be overridden, it can be accomplished using CSS similar to the snippet
9803 * below. MenuLayout's CSS will override the appropriate values with 'auto' or '0' to display the
9804 * menu correctly. If `menuPosition` is known beforehand, CSS rules corresponding to other positions
9805 * may be omitted.
9806 *
9807 * .oo-ui-menuLayout-menu {
9808 * height: 200px;
9809 * width: 200px;
9810 * }
9811 * .oo-ui-menuLayout-content {
9812 * top: 200px;
9813 * left: 200px;
9814 * right: 200px;
9815 * bottom: 200px;
9816 * }
9817 *
9818 * @class
9819 * @extends OO.ui.Layout
9820 *
9821 * @constructor
9822 * @param {Object} [config] Configuration options
9823 * @cfg {boolean} [showMenu=true] Show menu
9824 * @cfg {string} [menuPosition='before'] Position of menu: `top`, `after`, `bottom` or `before`
9825 */
9826 OO.ui.MenuLayout = function OoUiMenuLayout( config ) {
9827 // Configuration initialization
9828 config = $.extend( {
9829 showMenu: true,
9830 menuPosition: 'before'
9831 }, config );
9832
9833 // Parent constructor
9834 OO.ui.MenuLayout.parent.call( this, config );
9835
9836 /**
9837 * Menu DOM node
9838 *
9839 * @property {jQuery}
9840 */
9841 this.$menu = $( '<div>' );
9842 /**
9843 * Content DOM node
9844 *
9845 * @property {jQuery}
9846 */
9847 this.$content = $( '<div>' );
9848
9849 // Initialization
9850 this.$menu
9851 .addClass( 'oo-ui-menuLayout-menu' );
9852 this.$content.addClass( 'oo-ui-menuLayout-content' );
9853 this.$element
9854 .addClass( 'oo-ui-menuLayout' )
9855 .append( this.$content, this.$menu );
9856 this.setMenuPosition( config.menuPosition );
9857 this.toggleMenu( config.showMenu );
9858 };
9859
9860 /* Setup */
9861
9862 OO.inheritClass( OO.ui.MenuLayout, OO.ui.Layout );
9863
9864 /* Methods */
9865
9866 /**
9867 * Toggle menu.
9868 *
9869 * @param {boolean} showMenu Show menu, omit to toggle
9870 * @chainable
9871 */
9872 OO.ui.MenuLayout.prototype.toggleMenu = function ( showMenu ) {
9873 showMenu = showMenu === undefined ? !this.showMenu : !!showMenu;
9874
9875 if ( this.showMenu !== showMenu ) {
9876 this.showMenu = showMenu;
9877 this.$element
9878 .toggleClass( 'oo-ui-menuLayout-showMenu', this.showMenu )
9879 .toggleClass( 'oo-ui-menuLayout-hideMenu', !this.showMenu );
9880 }
9881
9882 return this;
9883 };
9884
9885 /**
9886 * Check if menu is visible
9887 *
9888 * @return {boolean} Menu is visible
9889 */
9890 OO.ui.MenuLayout.prototype.isMenuVisible = function () {
9891 return this.showMenu;
9892 };
9893
9894 /**
9895 * Set menu position.
9896 *
9897 * @param {string} position Position of menu, either `top`, `after`, `bottom` or `before`
9898 * @throws {Error} If position value is not supported
9899 * @chainable
9900 */
9901 OO.ui.MenuLayout.prototype.setMenuPosition = function ( position ) {
9902 this.$element.removeClass( 'oo-ui-menuLayout-' + this.menuPosition );
9903 this.menuPosition = position;
9904 this.$element.addClass( 'oo-ui-menuLayout-' + position );
9905
9906 return this;
9907 };
9908
9909 /**
9910 * Get menu position.
9911 *
9912 * @return {string} Menu position
9913 */
9914 OO.ui.MenuLayout.prototype.getMenuPosition = function () {
9915 return this.menuPosition;
9916 };
9917
9918 /**
9919 * BookletLayouts contain {@link OO.ui.PageLayout page layouts} as well as
9920 * an {@link OO.ui.OutlineSelectWidget outline} that allows users to easily navigate
9921 * through the pages and select which one to display. By default, only one page is
9922 * displayed at a time and the outline is hidden. When a user navigates to a new page,
9923 * the booklet layout automatically focuses on the first focusable element, unless the
9924 * default setting is changed. Optionally, booklets can be configured to show
9925 * {@link OO.ui.OutlineControlsWidget controls} for adding, moving, and removing items.
9926 *
9927 * @example
9928 * // Example of a BookletLayout that contains two PageLayouts.
9929 *
9930 * function PageOneLayout( name, config ) {
9931 * PageOneLayout.parent.call( this, name, config );
9932 * this.$element.append( '<p>First page</p><p>(This booklet has an outline, displayed on the left)</p>' );
9933 * }
9934 * OO.inheritClass( PageOneLayout, OO.ui.PageLayout );
9935 * PageOneLayout.prototype.setupOutlineItem = function () {
9936 * this.outlineItem.setLabel( 'Page One' );
9937 * };
9938 *
9939 * function PageTwoLayout( name, config ) {
9940 * PageTwoLayout.parent.call( this, name, config );
9941 * this.$element.append( '<p>Second page</p>' );
9942 * }
9943 * OO.inheritClass( PageTwoLayout, OO.ui.PageLayout );
9944 * PageTwoLayout.prototype.setupOutlineItem = function () {
9945 * this.outlineItem.setLabel( 'Page Two' );
9946 * };
9947 *
9948 * var page1 = new PageOneLayout( 'one' ),
9949 * page2 = new PageTwoLayout( 'two' );
9950 *
9951 * var booklet = new OO.ui.BookletLayout( {
9952 * outlined: true
9953 * } );
9954 *
9955 * booklet.addPages ( [ page1, page2 ] );
9956 * $( 'body' ).append( booklet.$element );
9957 *
9958 * @class
9959 * @extends OO.ui.MenuLayout
9960 *
9961 * @constructor
9962 * @param {Object} [config] Configuration options
9963 * @cfg {boolean} [continuous=false] Show all pages, one after another
9964 * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new page is displayed.
9965 * @cfg {boolean} [outlined=false] Show the outline. The outline is used to navigate through the pages of the booklet.
9966 * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
9967 */
9968 OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
9969 // Configuration initialization
9970 config = config || {};
9971
9972 // Parent constructor
9973 OO.ui.BookletLayout.parent.call( this, config );
9974
9975 // Properties
9976 this.currentPageName = null;
9977 this.pages = {};
9978 this.ignoreFocus = false;
9979 this.stackLayout = new OO.ui.StackLayout( { continuous: !!config.continuous } );
9980 this.$content.append( this.stackLayout.$element );
9981 this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
9982 this.outlineVisible = false;
9983 this.outlined = !!config.outlined;
9984 if ( this.outlined ) {
9985 this.editable = !!config.editable;
9986 this.outlineControlsWidget = null;
9987 this.outlineSelectWidget = new OO.ui.OutlineSelectWidget();
9988 this.outlinePanel = new OO.ui.PanelLayout( { scrollable: true } );
9989 this.$menu.append( this.outlinePanel.$element );
9990 this.outlineVisible = true;
9991 if ( this.editable ) {
9992 this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
9993 this.outlineSelectWidget
9994 );
9995 }
9996 }
9997 this.toggleMenu( this.outlined );
9998
9999 // Events
10000 this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
10001 if ( this.outlined ) {
10002 this.outlineSelectWidget.connect( this, { select: 'onOutlineSelectWidgetSelect' } );
10003 this.scrolling = false;
10004 this.stackLayout.connect( this, { visibleItemChange: 'onStackLayoutVisibleItemChange' } );
10005 }
10006 if ( this.autoFocus ) {
10007 // Event 'focus' does not bubble, but 'focusin' does
10008 this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
10009 }
10010
10011 // Initialization
10012 this.$element.addClass( 'oo-ui-bookletLayout' );
10013 this.stackLayout.$element.addClass( 'oo-ui-bookletLayout-stackLayout' );
10014 if ( this.outlined ) {
10015 this.outlinePanel.$element
10016 .addClass( 'oo-ui-bookletLayout-outlinePanel' )
10017 .append( this.outlineSelectWidget.$element );
10018 if ( this.editable ) {
10019 this.outlinePanel.$element
10020 .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
10021 .append( this.outlineControlsWidget.$element );
10022 }
10023 }
10024 };
10025
10026 /* Setup */
10027
10028 OO.inheritClass( OO.ui.BookletLayout, OO.ui.MenuLayout );
10029
10030 /* Events */
10031
10032 /**
10033 * A 'set' event is emitted when a page is {@link #setPage set} to be displayed by the booklet layout.
10034 * @event set
10035 * @param {OO.ui.PageLayout} page Current page
10036 */
10037
10038 /**
10039 * An 'add' event is emitted when pages are {@link #addPages added} to the booklet layout.
10040 *
10041 * @event add
10042 * @param {OO.ui.PageLayout[]} page Added pages
10043 * @param {number} index Index pages were added at
10044 */
10045
10046 /**
10047 * A 'remove' event is emitted when pages are {@link #clearPages cleared} or
10048 * {@link #removePages removed} from the booklet.
10049 *
10050 * @event remove
10051 * @param {OO.ui.PageLayout[]} pages Removed pages
10052 */
10053
10054 /* Methods */
10055
10056 /**
10057 * Handle stack layout focus.
10058 *
10059 * @private
10060 * @param {jQuery.Event} e Focusin event
10061 */
10062 OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) {
10063 var name, $target;
10064
10065 // Find the page that an element was focused within
10066 $target = $( e.target ).closest( '.oo-ui-pageLayout' );
10067 for ( name in this.pages ) {
10068 // Check for page match, exclude current page to find only page changes
10069 if ( this.pages[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentPageName ) {
10070 this.setPage( name );
10071 break;
10072 }
10073 }
10074 };
10075
10076 /**
10077 * Handle visibleItemChange events from the stackLayout
10078 *
10079 * The next visible page is set as the current page by selecting it
10080 * in the outline
10081 *
10082 * @param {OO.ui.PageLayout} page The next visible page in the layout
10083 */
10084 OO.ui.BookletLayout.prototype.onStackLayoutVisibleItemChange = function ( page ) {
10085 // Set a flag to so that the resulting call to #onStackLayoutSet doesn't
10086 // try and scroll the item into view again.
10087 this.scrolling = true;
10088 this.outlineSelectWidget.selectItemByData( page.getName() );
10089 this.scrolling = false;
10090 };
10091
10092 /**
10093 * Handle stack layout set events.
10094 *
10095 * @private
10096 * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
10097 */
10098 OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
10099 var layout = this;
10100 if ( !this.scrolling && page ) {
10101 page.scrollElementIntoView( { complete: function () {
10102 if ( layout.autoFocus ) {
10103 layout.focus();
10104 }
10105 } } );
10106 }
10107 };
10108
10109 /**
10110 * Focus the first input in the current page.
10111 *
10112 * If no page is selected, the first selectable page will be selected.
10113 * If the focus is already in an element on the current page, nothing will happen.
10114 * @param {number} [itemIndex] A specific item to focus on
10115 */
10116 OO.ui.BookletLayout.prototype.focus = function ( itemIndex ) {
10117 var page,
10118 items = this.stackLayout.getItems();
10119
10120 if ( itemIndex !== undefined && items[ itemIndex ] ) {
10121 page = items[ itemIndex ];
10122 } else {
10123 page = this.stackLayout.getCurrentItem();
10124 }
10125
10126 if ( !page && this.outlined ) {
10127 this.selectFirstSelectablePage();
10128 page = this.stackLayout.getCurrentItem();
10129 }
10130 if ( !page ) {
10131 return;
10132 }
10133 // Only change the focus if is not already in the current page
10134 if ( !OO.ui.contains( page.$element[ 0 ], this.getElementDocument().activeElement, true ) ) {
10135 page.focus();
10136 }
10137 };
10138
10139 /**
10140 * Find the first focusable input in the booklet layout and focus
10141 * on it.
10142 */
10143 OO.ui.BookletLayout.prototype.focusFirstFocusable = function () {
10144 OO.ui.findFocusable( this.stackLayout.$element ).focus();
10145 };
10146
10147 /**
10148 * Handle outline widget select events.
10149 *
10150 * @private
10151 * @param {OO.ui.OptionWidget|null} item Selected item
10152 */
10153 OO.ui.BookletLayout.prototype.onOutlineSelectWidgetSelect = function ( item ) {
10154 if ( item ) {
10155 this.setPage( item.getData() );
10156 }
10157 };
10158
10159 /**
10160 * Check if booklet has an outline.
10161 *
10162 * @return {boolean} Booklet has an outline
10163 */
10164 OO.ui.BookletLayout.prototype.isOutlined = function () {
10165 return this.outlined;
10166 };
10167
10168 /**
10169 * Check if booklet has editing controls.
10170 *
10171 * @return {boolean} Booklet is editable
10172 */
10173 OO.ui.BookletLayout.prototype.isEditable = function () {
10174 return this.editable;
10175 };
10176
10177 /**
10178 * Check if booklet has a visible outline.
10179 *
10180 * @return {boolean} Outline is visible
10181 */
10182 OO.ui.BookletLayout.prototype.isOutlineVisible = function () {
10183 return this.outlined && this.outlineVisible;
10184 };
10185
10186 /**
10187 * Hide or show the outline.
10188 *
10189 * @param {boolean} [show] Show outline, omit to invert current state
10190 * @chainable
10191 */
10192 OO.ui.BookletLayout.prototype.toggleOutline = function ( show ) {
10193 if ( this.outlined ) {
10194 show = show === undefined ? !this.outlineVisible : !!show;
10195 this.outlineVisible = show;
10196 this.toggleMenu( show );
10197 }
10198
10199 return this;
10200 };
10201
10202 /**
10203 * Get the page closest to the specified page.
10204 *
10205 * @param {OO.ui.PageLayout} page Page to use as a reference point
10206 * @return {OO.ui.PageLayout|null} Page closest to the specified page
10207 */
10208 OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) {
10209 var next, prev, level,
10210 pages = this.stackLayout.getItems(),
10211 index = pages.indexOf( page );
10212
10213 if ( index !== -1 ) {
10214 next = pages[ index + 1 ];
10215 prev = pages[ index - 1 ];
10216 // Prefer adjacent pages at the same level
10217 if ( this.outlined ) {
10218 level = this.outlineSelectWidget.getItemFromData( page.getName() ).getLevel();
10219 if (
10220 prev &&
10221 level === this.outlineSelectWidget.getItemFromData( prev.getName() ).getLevel()
10222 ) {
10223 return prev;
10224 }
10225 if (
10226 next &&
10227 level === this.outlineSelectWidget.getItemFromData( next.getName() ).getLevel()
10228 ) {
10229 return next;
10230 }
10231 }
10232 }
10233 return prev || next || null;
10234 };
10235
10236 /**
10237 * Get the outline widget.
10238 *
10239 * If the booklet is not outlined, the method will return `null`.
10240 *
10241 * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if the booklet is not outlined
10242 */
10243 OO.ui.BookletLayout.prototype.getOutline = function () {
10244 return this.outlineSelectWidget;
10245 };
10246
10247 /**
10248 * Get the outline controls widget.
10249 *
10250 * If the outline is not editable, the method will return `null`.
10251 *
10252 * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
10253 */
10254 OO.ui.BookletLayout.prototype.getOutlineControls = function () {
10255 return this.outlineControlsWidget;
10256 };
10257
10258 /**
10259 * Get a page by its symbolic name.
10260 *
10261 * @param {string} name Symbolic name of page
10262 * @return {OO.ui.PageLayout|undefined} Page, if found
10263 */
10264 OO.ui.BookletLayout.prototype.getPage = function ( name ) {
10265 return this.pages[ name ];
10266 };
10267
10268 /**
10269 * Get the current page.
10270 *
10271 * @return {OO.ui.PageLayout|undefined} Current page, if found
10272 */
10273 OO.ui.BookletLayout.prototype.getCurrentPage = function () {
10274 var name = this.getCurrentPageName();
10275 return name ? this.getPage( name ) : undefined;
10276 };
10277
10278 /**
10279 * Get the symbolic name of the current page.
10280 *
10281 * @return {string|null} Symbolic name of the current page
10282 */
10283 OO.ui.BookletLayout.prototype.getCurrentPageName = function () {
10284 return this.currentPageName;
10285 };
10286
10287 /**
10288 * Add pages to the booklet layout
10289 *
10290 * When pages are added with the same names as existing pages, the existing pages will be
10291 * automatically removed before the new pages are added.
10292 *
10293 * @param {OO.ui.PageLayout[]} pages Pages to add
10294 * @param {number} index Index of the insertion point
10295 * @fires add
10296 * @chainable
10297 */
10298 OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) {
10299 var i, len, name, page, item, currentIndex,
10300 stackLayoutPages = this.stackLayout.getItems(),
10301 remove = [],
10302 items = [];
10303
10304 // Remove pages with same names
10305 for ( i = 0, len = pages.length; i < len; i++ ) {
10306 page = pages[ i ];
10307 name = page.getName();
10308
10309 if ( Object.prototype.hasOwnProperty.call( this.pages, name ) ) {
10310 // Correct the insertion index
10311 currentIndex = stackLayoutPages.indexOf( this.pages[ name ] );
10312 if ( currentIndex !== -1 && currentIndex + 1 < index ) {
10313 index--;
10314 }
10315 remove.push( this.pages[ name ] );
10316 }
10317 }
10318 if ( remove.length ) {
10319 this.removePages( remove );
10320 }
10321
10322 // Add new pages
10323 for ( i = 0, len = pages.length; i < len; i++ ) {
10324 page = pages[ i ];
10325 name = page.getName();
10326 this.pages[ page.getName() ] = page;
10327 if ( this.outlined ) {
10328 item = new OO.ui.OutlineOptionWidget( { data: name } );
10329 page.setOutlineItem( item );
10330 items.push( item );
10331 }
10332 }
10333
10334 if ( this.outlined && items.length ) {
10335 this.outlineSelectWidget.addItems( items, index );
10336 this.selectFirstSelectablePage();
10337 }
10338 this.stackLayout.addItems( pages, index );
10339 this.emit( 'add', pages, index );
10340
10341 return this;
10342 };
10343
10344 /**
10345 * Remove the specified pages from the booklet layout.
10346 *
10347 * To remove all pages from the booklet, you may wish to use the #clearPages method instead.
10348 *
10349 * @param {OO.ui.PageLayout[]} pages An array of pages to remove
10350 * @fires remove
10351 * @chainable
10352 */
10353 OO.ui.BookletLayout.prototype.removePages = function ( pages ) {
10354 var i, len, name, page,
10355 items = [];
10356
10357 for ( i = 0, len = pages.length; i < len; i++ ) {
10358 page = pages[ i ];
10359 name = page.getName();
10360 delete this.pages[ name ];
10361 if ( this.outlined ) {
10362 items.push( this.outlineSelectWidget.getItemFromData( name ) );
10363 page.setOutlineItem( null );
10364 }
10365 }
10366 if ( this.outlined && items.length ) {
10367 this.outlineSelectWidget.removeItems( items );
10368 this.selectFirstSelectablePage();
10369 }
10370 this.stackLayout.removeItems( pages );
10371 this.emit( 'remove', pages );
10372
10373 return this;
10374 };
10375
10376 /**
10377 * Clear all pages from the booklet layout.
10378 *
10379 * To remove only a subset of pages from the booklet, use the #removePages method.
10380 *
10381 * @fires remove
10382 * @chainable
10383 */
10384 OO.ui.BookletLayout.prototype.clearPages = function () {
10385 var i, len,
10386 pages = this.stackLayout.getItems();
10387
10388 this.pages = {};
10389 this.currentPageName = null;
10390 if ( this.outlined ) {
10391 this.outlineSelectWidget.clearItems();
10392 for ( i = 0, len = pages.length; i < len; i++ ) {
10393 pages[ i ].setOutlineItem( null );
10394 }
10395 }
10396 this.stackLayout.clearItems();
10397
10398 this.emit( 'remove', pages );
10399
10400 return this;
10401 };
10402
10403 /**
10404 * Set the current page by symbolic name.
10405 *
10406 * @fires set
10407 * @param {string} name Symbolic name of page
10408 */
10409 OO.ui.BookletLayout.prototype.setPage = function ( name ) {
10410 var selectedItem,
10411 $focused,
10412 page = this.pages[ name ],
10413 previousPage = this.currentPageName && this.pages[ this.currentPageName ];
10414
10415 if ( name !== this.currentPageName ) {
10416 if ( this.outlined ) {
10417 selectedItem = this.outlineSelectWidget.getSelectedItem();
10418 if ( selectedItem && selectedItem.getData() !== name ) {
10419 this.outlineSelectWidget.selectItemByData( name );
10420 }
10421 }
10422 if ( page ) {
10423 if ( previousPage ) {
10424 previousPage.setActive( false );
10425 // Blur anything focused if the next page doesn't have anything focusable.
10426 // This is not needed if the next page has something focusable (because once it is focused
10427 // this blur happens automatically). If the layout is non-continuous, this check is
10428 // meaningless because the next page is not visible yet and thus can't hold focus.
10429 if (
10430 this.autoFocus &&
10431 this.stackLayout.continuous &&
10432 OO.ui.findFocusable( page.$element ).length !== 0
10433 ) {
10434 $focused = previousPage.$element.find( ':focus' );
10435 if ( $focused.length ) {
10436 $focused[ 0 ].blur();
10437 }
10438 }
10439 }
10440 this.currentPageName = name;
10441 page.setActive( true );
10442 this.stackLayout.setItem( page );
10443 if ( !this.stackLayout.continuous && previousPage ) {
10444 // This should not be necessary, since any inputs on the previous page should have been
10445 // blurred when it was hidden, but browsers are not very consistent about this.
10446 $focused = previousPage.$element.find( ':focus' );
10447 if ( $focused.length ) {
10448 $focused[ 0 ].blur();
10449 }
10450 }
10451 this.emit( 'set', page );
10452 }
10453 }
10454 };
10455
10456 /**
10457 * Select the first selectable page.
10458 *
10459 * @chainable
10460 */
10461 OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () {
10462 if ( !this.outlineSelectWidget.getSelectedItem() ) {
10463 this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getFirstSelectableItem() );
10464 }
10465
10466 return this;
10467 };
10468
10469 /**
10470 * IndexLayouts contain {@link OO.ui.CardLayout card layouts} as well as
10471 * {@link OO.ui.TabSelectWidget tabs} that allow users to easily navigate through the cards and
10472 * select which one to display. By default, only one card is displayed at a time. When a user
10473 * navigates to a new card, the index layout automatically focuses on the first focusable element,
10474 * unless the default setting is changed.
10475 *
10476 * TODO: This class is similar to BookletLayout, we may want to refactor to reduce duplication
10477 *
10478 * @example
10479 * // Example of a IndexLayout that contains two CardLayouts.
10480 *
10481 * function CardOneLayout( name, config ) {
10482 * CardOneLayout.parent.call( this, name, config );
10483 * this.$element.append( '<p>First card</p>' );
10484 * }
10485 * OO.inheritClass( CardOneLayout, OO.ui.CardLayout );
10486 * CardOneLayout.prototype.setupTabItem = function () {
10487 * this.tabItem.setLabel( 'Card one' );
10488 * };
10489 *
10490 * var card1 = new CardOneLayout( 'one' ),
10491 * card2 = new CardLayout( 'two', { label: 'Card two' } );
10492 *
10493 * card2.$element.append( '<p>Second card</p>' );
10494 *
10495 * var index = new OO.ui.IndexLayout();
10496 *
10497 * index.addCards ( [ card1, card2 ] );
10498 * $( 'body' ).append( index.$element );
10499 *
10500 * @class
10501 * @extends OO.ui.MenuLayout
10502 *
10503 * @constructor
10504 * @param {Object} [config] Configuration options
10505 * @cfg {boolean} [continuous=false] Show all cards, one after another
10506 * @cfg {boolean} [expanded=true] Expand the content panel to fill the entire parent element.
10507 * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new card is displayed.
10508 */
10509 OO.ui.IndexLayout = function OoUiIndexLayout( config ) {
10510 // Configuration initialization
10511 config = $.extend( {}, config, { menuPosition: 'top' } );
10512
10513 // Parent constructor
10514 OO.ui.IndexLayout.parent.call( this, config );
10515
10516 // Properties
10517 this.currentCardName = null;
10518 this.cards = {};
10519 this.ignoreFocus = false;
10520 this.stackLayout = new OO.ui.StackLayout( {
10521 continuous: !!config.continuous,
10522 expanded: config.expanded
10523 } );
10524 this.$content.append( this.stackLayout.$element );
10525 this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
10526
10527 this.tabSelectWidget = new OO.ui.TabSelectWidget();
10528 this.tabPanel = new OO.ui.PanelLayout();
10529 this.$menu.append( this.tabPanel.$element );
10530
10531 this.toggleMenu( true );
10532
10533 // Events
10534 this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
10535 this.tabSelectWidget.connect( this, { select: 'onTabSelectWidgetSelect' } );
10536 if ( this.autoFocus ) {
10537 // Event 'focus' does not bubble, but 'focusin' does
10538 this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
10539 }
10540
10541 // Initialization
10542 this.$element.addClass( 'oo-ui-indexLayout' );
10543 this.stackLayout.$element.addClass( 'oo-ui-indexLayout-stackLayout' );
10544 this.tabPanel.$element
10545 .addClass( 'oo-ui-indexLayout-tabPanel' )
10546 .append( this.tabSelectWidget.$element );
10547 };
10548
10549 /* Setup */
10550
10551 OO.inheritClass( OO.ui.IndexLayout, OO.ui.MenuLayout );
10552
10553 /* Events */
10554
10555 /**
10556 * A 'set' event is emitted when a card is {@link #setCard set} to be displayed by the index layout.
10557 * @event set
10558 * @param {OO.ui.CardLayout} card Current card
10559 */
10560
10561 /**
10562 * An 'add' event is emitted when cards are {@link #addCards added} to the index layout.
10563 *
10564 * @event add
10565 * @param {OO.ui.CardLayout[]} card Added cards
10566 * @param {number} index Index cards were added at
10567 */
10568
10569 /**
10570 * A 'remove' event is emitted when cards are {@link #clearCards cleared} or
10571 * {@link #removeCards removed} from the index.
10572 *
10573 * @event remove
10574 * @param {OO.ui.CardLayout[]} cards Removed cards
10575 */
10576
10577 /* Methods */
10578
10579 /**
10580 * Handle stack layout focus.
10581 *
10582 * @private
10583 * @param {jQuery.Event} e Focusin event
10584 */
10585 OO.ui.IndexLayout.prototype.onStackLayoutFocus = function ( e ) {
10586 var name, $target;
10587
10588 // Find the card that an element was focused within
10589 $target = $( e.target ).closest( '.oo-ui-cardLayout' );
10590 for ( name in this.cards ) {
10591 // Check for card match, exclude current card to find only card changes
10592 if ( this.cards[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentCardName ) {
10593 this.setCard( name );
10594 break;
10595 }
10596 }
10597 };
10598
10599 /**
10600 * Handle stack layout set events.
10601 *
10602 * @private
10603 * @param {OO.ui.PanelLayout|null} card The card panel that is now the current panel
10604 */
10605 OO.ui.IndexLayout.prototype.onStackLayoutSet = function ( card ) {
10606 var layout = this;
10607 if ( card ) {
10608 card.scrollElementIntoView( { complete: function () {
10609 if ( layout.autoFocus ) {
10610 layout.focus();
10611 }
10612 } } );
10613 }
10614 };
10615
10616 /**
10617 * Focus the first input in the current card.
10618 *
10619 * If no card is selected, the first selectable card will be selected.
10620 * If the focus is already in an element on the current card, nothing will happen.
10621 * @param {number} [itemIndex] A specific item to focus on
10622 */
10623 OO.ui.IndexLayout.prototype.focus = function ( itemIndex ) {
10624 var card,
10625 items = this.stackLayout.getItems();
10626
10627 if ( itemIndex !== undefined && items[ itemIndex ] ) {
10628 card = items[ itemIndex ];
10629 } else {
10630 card = this.stackLayout.getCurrentItem();
10631 }
10632
10633 if ( !card ) {
10634 this.selectFirstSelectableCard();
10635 card = this.stackLayout.getCurrentItem();
10636 }
10637 if ( !card ) {
10638 return;
10639 }
10640 // Only change the focus if is not already in the current page
10641 if ( !OO.ui.contains( card.$element[ 0 ], this.getElementDocument().activeElement, true ) ) {
10642 card.focus();
10643 }
10644 };
10645
10646 /**
10647 * Find the first focusable input in the index layout and focus
10648 * on it.
10649 */
10650 OO.ui.IndexLayout.prototype.focusFirstFocusable = function () {
10651 OO.ui.findFocusable( this.stackLayout.$element ).focus();
10652 };
10653
10654 /**
10655 * Handle tab widget select events.
10656 *
10657 * @private
10658 * @param {OO.ui.OptionWidget|null} item Selected item
10659 */
10660 OO.ui.IndexLayout.prototype.onTabSelectWidgetSelect = function ( item ) {
10661 if ( item ) {
10662 this.setCard( item.getData() );
10663 }
10664 };
10665
10666 /**
10667 * Get the card closest to the specified card.
10668 *
10669 * @param {OO.ui.CardLayout} card Card to use as a reference point
10670 * @return {OO.ui.CardLayout|null} Card closest to the specified card
10671 */
10672 OO.ui.IndexLayout.prototype.getClosestCard = function ( card ) {
10673 var next, prev, level,
10674 cards = this.stackLayout.getItems(),
10675 index = cards.indexOf( card );
10676
10677 if ( index !== -1 ) {
10678 next = cards[ index + 1 ];
10679 prev = cards[ index - 1 ];
10680 // Prefer adjacent cards at the same level
10681 level = this.tabSelectWidget.getItemFromData( card.getName() ).getLevel();
10682 if (
10683 prev &&
10684 level === this.tabSelectWidget.getItemFromData( prev.getName() ).getLevel()
10685 ) {
10686 return prev;
10687 }
10688 if (
10689 next &&
10690 level === this.tabSelectWidget.getItemFromData( next.getName() ).getLevel()
10691 ) {
10692 return next;
10693 }
10694 }
10695 return prev || next || null;
10696 };
10697
10698 /**
10699 * Get the tabs widget.
10700 *
10701 * @return {OO.ui.TabSelectWidget} Tabs widget
10702 */
10703 OO.ui.IndexLayout.prototype.getTabs = function () {
10704 return this.tabSelectWidget;
10705 };
10706
10707 /**
10708 * Get a card by its symbolic name.
10709 *
10710 * @param {string} name Symbolic name of card
10711 * @return {OO.ui.CardLayout|undefined} Card, if found
10712 */
10713 OO.ui.IndexLayout.prototype.getCard = function ( name ) {
10714 return this.cards[ name ];
10715 };
10716
10717 /**
10718 * Get the current card.
10719 *
10720 * @return {OO.ui.CardLayout|undefined} Current card, if found
10721 */
10722 OO.ui.IndexLayout.prototype.getCurrentCard = function () {
10723 var name = this.getCurrentCardName();
10724 return name ? this.getCard( name ) : undefined;
10725 };
10726
10727 /**
10728 * Get the symbolic name of the current card.
10729 *
10730 * @return {string|null} Symbolic name of the current card
10731 */
10732 OO.ui.IndexLayout.prototype.getCurrentCardName = function () {
10733 return this.currentCardName;
10734 };
10735
10736 /**
10737 * Add cards to the index layout
10738 *
10739 * When cards are added with the same names as existing cards, the existing cards will be
10740 * automatically removed before the new cards are added.
10741 *
10742 * @param {OO.ui.CardLayout[]} cards Cards to add
10743 * @param {number} index Index of the insertion point
10744 * @fires add
10745 * @chainable
10746 */
10747 OO.ui.IndexLayout.prototype.addCards = function ( cards, index ) {
10748 var i, len, name, card, item, currentIndex,
10749 stackLayoutCards = this.stackLayout.getItems(),
10750 remove = [],
10751 items = [];
10752
10753 // Remove cards with same names
10754 for ( i = 0, len = cards.length; i < len; i++ ) {
10755 card = cards[ i ];
10756 name = card.getName();
10757
10758 if ( Object.prototype.hasOwnProperty.call( this.cards, name ) ) {
10759 // Correct the insertion index
10760 currentIndex = stackLayoutCards.indexOf( this.cards[ name ] );
10761 if ( currentIndex !== -1 && currentIndex + 1 < index ) {
10762 index--;
10763 }
10764 remove.push( this.cards[ name ] );
10765 }
10766 }
10767 if ( remove.length ) {
10768 this.removeCards( remove );
10769 }
10770
10771 // Add new cards
10772 for ( i = 0, len = cards.length; i < len; i++ ) {
10773 card = cards[ i ];
10774 name = card.getName();
10775 this.cards[ card.getName() ] = card;
10776 item = new OO.ui.TabOptionWidget( { data: name } );
10777 card.setTabItem( item );
10778 items.push( item );
10779 }
10780
10781 if ( items.length ) {
10782 this.tabSelectWidget.addItems( items, index );
10783 this.selectFirstSelectableCard();
10784 }
10785 this.stackLayout.addItems( cards, index );
10786 this.emit( 'add', cards, index );
10787
10788 return this;
10789 };
10790
10791 /**
10792 * Remove the specified cards from the index layout.
10793 *
10794 * To remove all cards from the index, you may wish to use the #clearCards method instead.
10795 *
10796 * @param {OO.ui.CardLayout[]} cards An array of cards to remove
10797 * @fires remove
10798 * @chainable
10799 */
10800 OO.ui.IndexLayout.prototype.removeCards = function ( cards ) {
10801 var i, len, name, card,
10802 items = [];
10803
10804 for ( i = 0, len = cards.length; i < len; i++ ) {
10805 card = cards[ i ];
10806 name = card.getName();
10807 delete this.cards[ name ];
10808 items.push( this.tabSelectWidget.getItemFromData( name ) );
10809 card.setTabItem( null );
10810 }
10811 if ( items.length ) {
10812 this.tabSelectWidget.removeItems( items );
10813 this.selectFirstSelectableCard();
10814 }
10815 this.stackLayout.removeItems( cards );
10816 this.emit( 'remove', cards );
10817
10818 return this;
10819 };
10820
10821 /**
10822 * Clear all cards from the index layout.
10823 *
10824 * To remove only a subset of cards from the index, use the #removeCards method.
10825 *
10826 * @fires remove
10827 * @chainable
10828 */
10829 OO.ui.IndexLayout.prototype.clearCards = function () {
10830 var i, len,
10831 cards = this.stackLayout.getItems();
10832
10833 this.cards = {};
10834 this.currentCardName = null;
10835 this.tabSelectWidget.clearItems();
10836 for ( i = 0, len = cards.length; i < len; i++ ) {
10837 cards[ i ].setTabItem( null );
10838 }
10839 this.stackLayout.clearItems();
10840
10841 this.emit( 'remove', cards );
10842
10843 return this;
10844 };
10845
10846 /**
10847 * Set the current card by symbolic name.
10848 *
10849 * @fires set
10850 * @param {string} name Symbolic name of card
10851 */
10852 OO.ui.IndexLayout.prototype.setCard = function ( name ) {
10853 var selectedItem,
10854 $focused,
10855 card = this.cards[ name ],
10856 previousCard = this.currentCardName && this.cards[ this.currentCardName ];
10857
10858 if ( name !== this.currentCardName ) {
10859 selectedItem = this.tabSelectWidget.getSelectedItem();
10860 if ( selectedItem && selectedItem.getData() !== name ) {
10861 this.tabSelectWidget.selectItemByData( name );
10862 }
10863 if ( card ) {
10864 if ( previousCard ) {
10865 previousCard.setActive( false );
10866 // Blur anything focused if the next card doesn't have anything focusable.
10867 // This is not needed if the next card has something focusable (because once it is focused
10868 // this blur happens automatically). If the layout is non-continuous, this check is
10869 // meaningless because the next card is not visible yet and thus can't hold focus.
10870 if (
10871 this.autoFocus &&
10872 this.stackLayout.continuous &&
10873 OO.ui.findFocusable( card.$element ).length !== 0
10874 ) {
10875 $focused = previousCard.$element.find( ':focus' );
10876 if ( $focused.length ) {
10877 $focused[ 0 ].blur();
10878 }
10879 }
10880 }
10881 this.currentCardName = name;
10882 card.setActive( true );
10883 this.stackLayout.setItem( card );
10884 if ( !this.stackLayout.continuous && previousCard ) {
10885 // This should not be necessary, since any inputs on the previous card should have been
10886 // blurred when it was hidden, but browsers are not very consistent about this.
10887 $focused = previousCard.$element.find( ':focus' );
10888 if ( $focused.length ) {
10889 $focused[ 0 ].blur();
10890 }
10891 }
10892 this.emit( 'set', card );
10893 }
10894 }
10895 };
10896
10897 /**
10898 * Select the first selectable card.
10899 *
10900 * @chainable
10901 */
10902 OO.ui.IndexLayout.prototype.selectFirstSelectableCard = function () {
10903 if ( !this.tabSelectWidget.getSelectedItem() ) {
10904 this.tabSelectWidget.selectItem( this.tabSelectWidget.getFirstSelectableItem() );
10905 }
10906
10907 return this;
10908 };
10909
10910 /**
10911 * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
10912 * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
10913 *
10914 * @example
10915 * // Example of a panel layout
10916 * var panel = new OO.ui.PanelLayout( {
10917 * expanded: false,
10918 * framed: true,
10919 * padded: true,
10920 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
10921 * } );
10922 * $( 'body' ).append( panel.$element );
10923 *
10924 * @class
10925 * @extends OO.ui.Layout
10926 *
10927 * @constructor
10928 * @param {Object} [config] Configuration options
10929 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
10930 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
10931 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
10932 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
10933 */
10934 OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
10935 // Configuration initialization
10936 config = $.extend( {
10937 scrollable: false,
10938 padded: false,
10939 expanded: true,
10940 framed: false
10941 }, config );
10942
10943 // Parent constructor
10944 OO.ui.PanelLayout.parent.call( this, config );
10945
10946 // Initialization
10947 this.$element.addClass( 'oo-ui-panelLayout' );
10948 if ( config.scrollable ) {
10949 this.$element.addClass( 'oo-ui-panelLayout-scrollable' );
10950 }
10951 if ( config.padded ) {
10952 this.$element.addClass( 'oo-ui-panelLayout-padded' );
10953 }
10954 if ( config.expanded ) {
10955 this.$element.addClass( 'oo-ui-panelLayout-expanded' );
10956 }
10957 if ( config.framed ) {
10958 this.$element.addClass( 'oo-ui-panelLayout-framed' );
10959 }
10960 };
10961
10962 /* Setup */
10963
10964 OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
10965
10966 /* Methods */
10967
10968 /**
10969 * Focus the panel layout
10970 *
10971 * The default implementation just focuses the first focusable element in the panel
10972 */
10973 OO.ui.PanelLayout.prototype.focus = function () {
10974 OO.ui.findFocusable( this.$element ).focus();
10975 };
10976
10977 /**
10978 * CardLayouts are used within {@link OO.ui.IndexLayout index layouts} to create cards that users can select and display
10979 * from the index's optional {@link OO.ui.TabSelectWidget tab} navigation. Cards are usually not instantiated directly,
10980 * rather extended to include the required content and functionality.
10981 *
10982 * Each card must have a unique symbolic name, which is passed to the constructor. In addition, the card's tab
10983 * item is customized (with a label) using the #setupTabItem method. See
10984 * {@link OO.ui.IndexLayout IndexLayout} for an example.
10985 *
10986 * @class
10987 * @extends OO.ui.PanelLayout
10988 *
10989 * @constructor
10990 * @param {string} name Unique symbolic name of card
10991 * @param {Object} [config] Configuration options
10992 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] Label for card's tab
10993 */
10994 OO.ui.CardLayout = function OoUiCardLayout( name, config ) {
10995 // Allow passing positional parameters inside the config object
10996 if ( OO.isPlainObject( name ) && config === undefined ) {
10997 config = name;
10998 name = config.name;
10999 }
11000
11001 // Configuration initialization
11002 config = $.extend( { scrollable: true }, config );
11003
11004 // Parent constructor
11005 OO.ui.CardLayout.parent.call( this, config );
11006
11007 // Properties
11008 this.name = name;
11009 this.label = config.label;
11010 this.tabItem = null;
11011 this.active = false;
11012
11013 // Initialization
11014 this.$element.addClass( 'oo-ui-cardLayout' );
11015 };
11016
11017 /* Setup */
11018
11019 OO.inheritClass( OO.ui.CardLayout, OO.ui.PanelLayout );
11020
11021 /* Events */
11022
11023 /**
11024 * An 'active' event is emitted when the card becomes active. Cards become active when they are
11025 * shown in a index layout that is configured to display only one card at a time.
11026 *
11027 * @event active
11028 * @param {boolean} active Card is active
11029 */
11030
11031 /* Methods */
11032
11033 /**
11034 * Get the symbolic name of the card.
11035 *
11036 * @return {string} Symbolic name of card
11037 */
11038 OO.ui.CardLayout.prototype.getName = function () {
11039 return this.name;
11040 };
11041
11042 /**
11043 * Check if card is active.
11044 *
11045 * Cards become active when they are shown in a {@link OO.ui.IndexLayout index layout} that is configured to display
11046 * only one card at a time. Additional CSS is applied to the card's tab item to reflect the active state.
11047 *
11048 * @return {boolean} Card is active
11049 */
11050 OO.ui.CardLayout.prototype.isActive = function () {
11051 return this.active;
11052 };
11053
11054 /**
11055 * Get tab item.
11056 *
11057 * The tab item allows users to access the card from the index's tab
11058 * navigation. The tab item itself can be customized (with a label, level, etc.) using the #setupTabItem method.
11059 *
11060 * @return {OO.ui.TabOptionWidget|null} Tab option widget
11061 */
11062 OO.ui.CardLayout.prototype.getTabItem = function () {
11063 return this.tabItem;
11064 };
11065
11066 /**
11067 * Set or unset the tab item.
11068 *
11069 * Specify a {@link OO.ui.TabOptionWidget tab option} to set it,
11070 * or `null` to clear the tab item. To customize the tab item itself (e.g., to set a label or tab
11071 * level), use #setupTabItem instead of this method.
11072 *
11073 * @param {OO.ui.TabOptionWidget|null} tabItem Tab option widget, null to clear
11074 * @chainable
11075 */
11076 OO.ui.CardLayout.prototype.setTabItem = function ( tabItem ) {
11077 this.tabItem = tabItem || null;
11078 if ( tabItem ) {
11079 this.setupTabItem();
11080 }
11081 return this;
11082 };
11083
11084 /**
11085 * Set up the tab item.
11086 *
11087 * Use this method to customize the tab item (e.g., to add a label or tab level). To set or unset
11088 * the tab item itself (with a {@link OO.ui.TabOptionWidget tab option} or `null`), use
11089 * the #setTabItem method instead.
11090 *
11091 * @param {OO.ui.TabOptionWidget} tabItem Tab option widget to set up
11092 * @chainable
11093 */
11094 OO.ui.CardLayout.prototype.setupTabItem = function () {
11095 if ( this.label ) {
11096 this.tabItem.setLabel( this.label );
11097 }
11098 return this;
11099 };
11100
11101 /**
11102 * Set the card to its 'active' state.
11103 *
11104 * Cards become active when they are shown in a index layout that is configured to display only one card at a time. Additional
11105 * CSS is applied to the tab item to reflect the card's active state. Outside of the index
11106 * context, setting the active state on a card does nothing.
11107 *
11108 * @param {boolean} value Card is active
11109 * @fires active
11110 */
11111 OO.ui.CardLayout.prototype.setActive = function ( active ) {
11112 active = !!active;
11113
11114 if ( active !== this.active ) {
11115 this.active = active;
11116 this.$element.toggleClass( 'oo-ui-cardLayout-active', this.active );
11117 this.emit( 'active', this.active );
11118 }
11119 };
11120
11121 /**
11122 * PageLayouts are used within {@link OO.ui.BookletLayout booklet layouts} to create pages that users can select and display
11123 * from the booklet's optional {@link OO.ui.OutlineSelectWidget outline} navigation. Pages are usually not instantiated directly,
11124 * rather extended to include the required content and functionality.
11125 *
11126 * Each page must have a unique symbolic name, which is passed to the constructor. In addition, the page's outline
11127 * item is customized (with a label, outline level, etc.) using the #setupOutlineItem method. See
11128 * {@link OO.ui.BookletLayout BookletLayout} for an example.
11129 *
11130 * @class
11131 * @extends OO.ui.PanelLayout
11132 *
11133 * @constructor
11134 * @param {string} name Unique symbolic name of page
11135 * @param {Object} [config] Configuration options
11136 */
11137 OO.ui.PageLayout = function OoUiPageLayout( name, config ) {
11138 // Allow passing positional parameters inside the config object
11139 if ( OO.isPlainObject( name ) && config === undefined ) {
11140 config = name;
11141 name = config.name;
11142 }
11143
11144 // Configuration initialization
11145 config = $.extend( { scrollable: true }, config );
11146
11147 // Parent constructor
11148 OO.ui.PageLayout.parent.call( this, config );
11149
11150 // Properties
11151 this.name = name;
11152 this.outlineItem = null;
11153 this.active = false;
11154
11155 // Initialization
11156 this.$element.addClass( 'oo-ui-pageLayout' );
11157 };
11158
11159 /* Setup */
11160
11161 OO.inheritClass( OO.ui.PageLayout, OO.ui.PanelLayout );
11162
11163 /* Events */
11164
11165 /**
11166 * An 'active' event is emitted when the page becomes active. Pages become active when they are
11167 * shown in a booklet layout that is configured to display only one page at a time.
11168 *
11169 * @event active
11170 * @param {boolean} active Page is active
11171 */
11172
11173 /* Methods */
11174
11175 /**
11176 * Get the symbolic name of the page.
11177 *
11178 * @return {string} Symbolic name of page
11179 */
11180 OO.ui.PageLayout.prototype.getName = function () {
11181 return this.name;
11182 };
11183
11184 /**
11185 * Check if page is active.
11186 *
11187 * Pages become active when they are shown in a {@link OO.ui.BookletLayout booklet layout} that is configured to display
11188 * only one page at a time. Additional CSS is applied to the page's outline item to reflect the active state.
11189 *
11190 * @return {boolean} Page is active
11191 */
11192 OO.ui.PageLayout.prototype.isActive = function () {
11193 return this.active;
11194 };
11195
11196 /**
11197 * Get outline item.
11198 *
11199 * The outline item allows users to access the page from the booklet's outline
11200 * navigation. The outline item itself can be customized (with a label, level, etc.) using the #setupOutlineItem method.
11201 *
11202 * @return {OO.ui.OutlineOptionWidget|null} Outline option widget
11203 */
11204 OO.ui.PageLayout.prototype.getOutlineItem = function () {
11205 return this.outlineItem;
11206 };
11207
11208 /**
11209 * Set or unset the outline item.
11210 *
11211 * Specify an {@link OO.ui.OutlineOptionWidget outline option} to set it,
11212 * or `null` to clear the outline item. To customize the outline item itself (e.g., to set a label or outline
11213 * level), use #setupOutlineItem instead of this method.
11214 *
11215 * @param {OO.ui.OutlineOptionWidget|null} outlineItem Outline option widget, null to clear
11216 * @chainable
11217 */
11218 OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) {
11219 this.outlineItem = outlineItem || null;
11220 if ( outlineItem ) {
11221 this.setupOutlineItem();
11222 }
11223 return this;
11224 };
11225
11226 /**
11227 * Set up the outline item.
11228 *
11229 * Use this method to customize the outline item (e.g., to add a label or outline level). To set or unset
11230 * the outline item itself (with an {@link OO.ui.OutlineOptionWidget outline option} or `null`), use
11231 * the #setOutlineItem method instead.
11232 *
11233 * @param {OO.ui.OutlineOptionWidget} outlineItem Outline option widget to set up
11234 * @chainable
11235 */
11236 OO.ui.PageLayout.prototype.setupOutlineItem = function () {
11237 return this;
11238 };
11239
11240 /**
11241 * Set the page to its 'active' state.
11242 *
11243 * Pages become active when they are shown in a booklet layout that is configured to display only one page at a time. Additional
11244 * CSS is applied to the outline item to reflect the page's active state. Outside of the booklet
11245 * context, setting the active state on a page does nothing.
11246 *
11247 * @param {boolean} value Page is active
11248 * @fires active
11249 */
11250 OO.ui.PageLayout.prototype.setActive = function ( active ) {
11251 active = !!active;
11252
11253 if ( active !== this.active ) {
11254 this.active = active;
11255 this.$element.toggleClass( 'oo-ui-pageLayout-active', active );
11256 this.emit( 'active', this.active );
11257 }
11258 };
11259
11260 /**
11261 * StackLayouts contain a series of {@link OO.ui.PanelLayout panel layouts}. By default, only one panel is displayed
11262 * at a time, though the stack layout can also be configured to show all contained panels, one after another,
11263 * by setting the #continuous option to 'true'.
11264 *
11265 * @example
11266 * // A stack layout with two panels, configured to be displayed continously
11267 * var myStack = new OO.ui.StackLayout( {
11268 * items: [
11269 * new OO.ui.PanelLayout( {
11270 * $content: $( '<p>Panel One</p>' ),
11271 * padded: true,
11272 * framed: true
11273 * } ),
11274 * new OO.ui.PanelLayout( {
11275 * $content: $( '<p>Panel Two</p>' ),
11276 * padded: true,
11277 * framed: true
11278 * } )
11279 * ],
11280 * continuous: true
11281 * } );
11282 * $( 'body' ).append( myStack.$element );
11283 *
11284 * @class
11285 * @extends OO.ui.PanelLayout
11286 * @mixins OO.ui.mixin.GroupElement
11287 *
11288 * @constructor
11289 * @param {Object} [config] Configuration options
11290 * @cfg {boolean} [continuous=false] Show all panels, one after another. By default, only one panel is displayed at a time.
11291 * @cfg {OO.ui.Layout[]} [items] Panel layouts to add to the stack layout.
11292 */
11293 OO.ui.StackLayout = function OoUiStackLayout( config ) {
11294 // Configuration initialization
11295 config = $.extend( { scrollable: true }, config );
11296
11297 // Parent constructor
11298 OO.ui.StackLayout.parent.call( this, config );
11299
11300 // Mixin constructors
11301 OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
11302
11303 // Properties
11304 this.currentItem = null;
11305 this.continuous = !!config.continuous;
11306
11307 // Initialization
11308 this.$element.addClass( 'oo-ui-stackLayout' );
11309 if ( this.continuous ) {
11310 this.$element.addClass( 'oo-ui-stackLayout-continuous' );
11311 this.$element.on( 'scroll', OO.ui.debounce( this.onScroll.bind( this ), 250 ) );
11312 }
11313 if ( Array.isArray( config.items ) ) {
11314 this.addItems( config.items );
11315 }
11316 };
11317
11318 /* Setup */
11319
11320 OO.inheritClass( OO.ui.StackLayout, OO.ui.PanelLayout );
11321 OO.mixinClass( OO.ui.StackLayout, OO.ui.mixin.GroupElement );
11322
11323 /* Events */
11324
11325 /**
11326 * A 'set' event is emitted when panels are {@link #addItems added}, {@link #removeItems removed},
11327 * {@link #clearItems cleared} or {@link #setItem displayed}.
11328 *
11329 * @event set
11330 * @param {OO.ui.Layout|null} item Current panel or `null` if no panel is shown
11331 */
11332
11333 /**
11334 * When used in continuous mode, this event is emitted when the user scrolls down
11335 * far enough such that currentItem is no longer visible.
11336 *
11337 * @event visibleItemChange
11338 * @param {OO.ui.PanelLayout} panel The next visible item in the layout
11339 */
11340
11341 /* Methods */
11342
11343 /**
11344 * Handle scroll events from the layout element
11345 *
11346 * @param {jQuery.Event} e
11347 * @fires visibleItemChange
11348 */
11349 OO.ui.StackLayout.prototype.onScroll = function () {
11350 var currentRect,
11351 len = this.items.length,
11352 currentIndex = this.items.indexOf( this.currentItem ),
11353 newIndex = currentIndex,
11354 containerRect = this.$element[ 0 ].getBoundingClientRect();
11355
11356 if ( !containerRect || ( !containerRect.top && !containerRect.bottom ) ) {
11357 // Can't get bounding rect, possibly not attached.
11358 return;
11359 }
11360
11361 function getRect( item ) {
11362 return item.$element[ 0 ].getBoundingClientRect();
11363 }
11364
11365 function isVisible( item ) {
11366 var rect = getRect( item );
11367 return rect.bottom > containerRect.top && rect.top < containerRect.bottom;
11368 }
11369
11370 currentRect = getRect( this.currentItem );
11371
11372 if ( currentRect.bottom < containerRect.top ) {
11373 // Scrolled down past current item
11374 while ( ++newIndex < len ) {
11375 if ( isVisible( this.items[ newIndex ] ) ) {
11376 break;
11377 }
11378 }
11379 } else if ( currentRect.top > containerRect.bottom ) {
11380 // Scrolled up past current item
11381 while ( --newIndex >= 0 ) {
11382 if ( isVisible( this.items[ newIndex ] ) ) {
11383 break;
11384 }
11385 }
11386 }
11387
11388 if ( newIndex !== currentIndex ) {
11389 this.emit( 'visibleItemChange', this.items[ newIndex ] );
11390 }
11391 };
11392
11393 /**
11394 * Get the current panel.
11395 *
11396 * @return {OO.ui.Layout|null}
11397 */
11398 OO.ui.StackLayout.prototype.getCurrentItem = function () {
11399 return this.currentItem;
11400 };
11401
11402 /**
11403 * Unset the current item.
11404 *
11405 * @private
11406 * @param {OO.ui.StackLayout} layout
11407 * @fires set
11408 */
11409 OO.ui.StackLayout.prototype.unsetCurrentItem = function () {
11410 var prevItem = this.currentItem;
11411 if ( prevItem === null ) {
11412 return;
11413 }
11414
11415 this.currentItem = null;
11416 this.emit( 'set', null );
11417 };
11418
11419 /**
11420 * Add panel layouts to the stack layout.
11421 *
11422 * Panels will be added to the end of the stack layout array unless the optional index parameter specifies a different
11423 * insertion point. Adding a panel that is already in the stack will move it to the end of the array or the point specified
11424 * by the index.
11425 *
11426 * @param {OO.ui.Layout[]} items Panels to add
11427 * @param {number} [index] Index of the insertion point
11428 * @chainable
11429 */
11430 OO.ui.StackLayout.prototype.addItems = function ( items, index ) {
11431 // Update the visibility
11432 this.updateHiddenState( items, this.currentItem );
11433
11434 // Mixin method
11435 OO.ui.mixin.GroupElement.prototype.addItems.call( this, items, index );
11436
11437 if ( !this.currentItem && items.length ) {
11438 this.setItem( items[ 0 ] );
11439 }
11440
11441 return this;
11442 };
11443
11444 /**
11445 * Remove the specified panels from the stack layout.
11446 *
11447 * Removed panels are detached from the DOM, not removed, so that they may be reused. To remove all panels,
11448 * you may wish to use the #clearItems method instead.
11449 *
11450 * @param {OO.ui.Layout[]} items Panels to remove
11451 * @chainable
11452 * @fires set
11453 */
11454 OO.ui.StackLayout.prototype.removeItems = function ( items ) {
11455 // Mixin method
11456 OO.ui.mixin.GroupElement.prototype.removeItems.call( this, items );
11457
11458 if ( items.indexOf( this.currentItem ) !== -1 ) {
11459 if ( this.items.length ) {
11460 this.setItem( this.items[ 0 ] );
11461 } else {
11462 this.unsetCurrentItem();
11463 }
11464 }
11465
11466 return this;
11467 };
11468
11469 /**
11470 * Clear all panels from the stack layout.
11471 *
11472 * Cleared panels are detached from the DOM, not removed, so that they may be reused. To remove only
11473 * a subset of panels, use the #removeItems method.
11474 *
11475 * @chainable
11476 * @fires set
11477 */
11478 OO.ui.StackLayout.prototype.clearItems = function () {
11479 this.unsetCurrentItem();
11480 OO.ui.mixin.GroupElement.prototype.clearItems.call( this );
11481
11482 return this;
11483 };
11484
11485 /**
11486 * Show the specified panel.
11487 *
11488 * If another panel is currently displayed, it will be hidden.
11489 *
11490 * @param {OO.ui.Layout} item Panel to show
11491 * @chainable
11492 * @fires set
11493 */
11494 OO.ui.StackLayout.prototype.setItem = function ( item ) {
11495 if ( item !== this.currentItem ) {
11496 this.updateHiddenState( this.items, item );
11497
11498 if ( this.items.indexOf( item ) !== -1 ) {
11499 this.currentItem = item;
11500 this.emit( 'set', item );
11501 } else {
11502 this.unsetCurrentItem();
11503 }
11504 }
11505
11506 return this;
11507 };
11508
11509 /**
11510 * Update the visibility of all items in case of non-continuous view.
11511 *
11512 * Ensure all items are hidden except for the selected one.
11513 * This method does nothing when the stack is continuous.
11514 *
11515 * @private
11516 * @param {OO.ui.Layout[]} items Item list iterate over
11517 * @param {OO.ui.Layout} [selectedItem] Selected item to show
11518 */
11519 OO.ui.StackLayout.prototype.updateHiddenState = function ( items, selectedItem ) {
11520 var i, len;
11521
11522 if ( !this.continuous ) {
11523 for ( i = 0, len = items.length; i < len; i++ ) {
11524 if ( !selectedItem || selectedItem !== items[ i ] ) {
11525 items[ i ].$element.addClass( 'oo-ui-element-hidden' );
11526 }
11527 }
11528 if ( selectedItem ) {
11529 selectedItem.$element.removeClass( 'oo-ui-element-hidden' );
11530 }
11531 }
11532 };
11533
11534 /**
11535 * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
11536 * items), with small margins between them. Convenient when you need to put a number of block-level
11537 * widgets on a single line next to each other.
11538 *
11539 * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
11540 *
11541 * @example
11542 * // HorizontalLayout with a text input and a label
11543 * var layout = new OO.ui.HorizontalLayout( {
11544 * items: [
11545 * new OO.ui.LabelWidget( { label: 'Label' } ),
11546 * new OO.ui.TextInputWidget( { value: 'Text' } )
11547 * ]
11548 * } );
11549 * $( 'body' ).append( layout.$element );
11550 *
11551 * @class
11552 * @extends OO.ui.Layout
11553 * @mixins OO.ui.mixin.GroupElement
11554 *
11555 * @constructor
11556 * @param {Object} [config] Configuration options
11557 * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
11558 */
11559 OO.ui.HorizontalLayout = function OoUiHorizontalLayout( config ) {
11560 // Configuration initialization
11561 config = config || {};
11562
11563 // Parent constructor
11564 OO.ui.HorizontalLayout.parent.call( this, config );
11565
11566 // Mixin constructors
11567 OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
11568
11569 // Initialization
11570 this.$element.addClass( 'oo-ui-horizontalLayout' );
11571 if ( Array.isArray( config.items ) ) {
11572 this.addItems( config.items );
11573 }
11574 };
11575
11576 /* Setup */
11577
11578 OO.inheritClass( OO.ui.HorizontalLayout, OO.ui.Layout );
11579 OO.mixinClass( OO.ui.HorizontalLayout, OO.ui.mixin.GroupElement );
11580
11581 /**
11582 * BarToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
11583 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.MenuToolGroup MenuToolGroup}
11584 * and {@link OO.ui.ListToolGroup ListToolGroup}). The {@link OO.ui.Tool tools} in a BarToolGroup are
11585 * displayed by icon in a single row. The title of the tool is displayed when users move the mouse over
11586 * the tool.
11587 *
11588 * BarToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar is
11589 * set up.
11590 *
11591 * @example
11592 * // Example of a BarToolGroup with two tools
11593 * var toolFactory = new OO.ui.ToolFactory();
11594 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
11595 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
11596 *
11597 * // We will be placing status text in this element when tools are used
11598 * var $area = $( '<p>' ).text( 'Example of a BarToolGroup with two tools.' );
11599 *
11600 * // Define the tools that we're going to place in our toolbar
11601 *
11602 * // Create a class inheriting from OO.ui.Tool
11603 * function SearchTool() {
11604 * SearchTool.parent.apply( this, arguments );
11605 * }
11606 * OO.inheritClass( SearchTool, OO.ui.Tool );
11607 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
11608 * // of 'icon' and 'title' (displayed icon and text).
11609 * SearchTool.static.name = 'search';
11610 * SearchTool.static.icon = 'search';
11611 * SearchTool.static.title = 'Search...';
11612 * // Defines the action that will happen when this tool is selected (clicked).
11613 * SearchTool.prototype.onSelect = function () {
11614 * $area.text( 'Search tool clicked!' );
11615 * // Never display this tool as "active" (selected).
11616 * this.setActive( false );
11617 * };
11618 * SearchTool.prototype.onUpdateState = function () {};
11619 * // Make this tool available in our toolFactory and thus our toolbar
11620 * toolFactory.register( SearchTool );
11621 *
11622 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
11623 * // little popup window (a PopupWidget).
11624 * function HelpTool( toolGroup, config ) {
11625 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
11626 * padded: true,
11627 * label: 'Help',
11628 * head: true
11629 * } }, config ) );
11630 * this.popup.$body.append( '<p>I am helpful!</p>' );
11631 * }
11632 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
11633 * HelpTool.static.name = 'help';
11634 * HelpTool.static.icon = 'help';
11635 * HelpTool.static.title = 'Help';
11636 * toolFactory.register( HelpTool );
11637 *
11638 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
11639 * // used once (but not all defined tools must be used).
11640 * toolbar.setup( [
11641 * {
11642 * // 'bar' tool groups display tools by icon only
11643 * type: 'bar',
11644 * include: [ 'search', 'help' ]
11645 * }
11646 * ] );
11647 *
11648 * // Create some UI around the toolbar and place it in the document
11649 * var frame = new OO.ui.PanelLayout( {
11650 * expanded: false,
11651 * framed: true
11652 * } );
11653 * var contentFrame = new OO.ui.PanelLayout( {
11654 * expanded: false,
11655 * padded: true
11656 * } );
11657 * frame.$element.append(
11658 * toolbar.$element,
11659 * contentFrame.$element.append( $area )
11660 * );
11661 * $( 'body' ).append( frame.$element );
11662 *
11663 * // Here is where the toolbar is actually built. This must be done after inserting it into the
11664 * // document.
11665 * toolbar.initialize();
11666 *
11667 * For more information about how to add tools to a bar tool group, please see {@link OO.ui.ToolGroup toolgroup}.
11668 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
11669 *
11670 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
11671 *
11672 * @class
11673 * @extends OO.ui.ToolGroup
11674 *
11675 * @constructor
11676 * @param {OO.ui.Toolbar} toolbar
11677 * @param {Object} [config] Configuration options
11678 */
11679 OO.ui.BarToolGroup = function OoUiBarToolGroup( toolbar, config ) {
11680 // Allow passing positional parameters inside the config object
11681 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
11682 config = toolbar;
11683 toolbar = config.toolbar;
11684 }
11685
11686 // Parent constructor
11687 OO.ui.BarToolGroup.parent.call( this, toolbar, config );
11688
11689 // Initialization
11690 this.$element.addClass( 'oo-ui-barToolGroup' );
11691 };
11692
11693 /* Setup */
11694
11695 OO.inheritClass( OO.ui.BarToolGroup, OO.ui.ToolGroup );
11696
11697 /* Static Properties */
11698
11699 OO.ui.BarToolGroup.static.titleTooltips = true;
11700
11701 OO.ui.BarToolGroup.static.accelTooltips = true;
11702
11703 OO.ui.BarToolGroup.static.name = 'bar';
11704
11705 /**
11706 * PopupToolGroup is an abstract base class used by both {@link OO.ui.MenuToolGroup MenuToolGroup}
11707 * and {@link OO.ui.ListToolGroup ListToolGroup} to provide a popup--an overlaid menu or list of tools with an
11708 * optional icon and label. This class can be used for other base classes that also use this functionality.
11709 *
11710 * @abstract
11711 * @class
11712 * @extends OO.ui.ToolGroup
11713 * @mixins OO.ui.mixin.IconElement
11714 * @mixins OO.ui.mixin.IndicatorElement
11715 * @mixins OO.ui.mixin.LabelElement
11716 * @mixins OO.ui.mixin.TitledElement
11717 * @mixins OO.ui.mixin.ClippableElement
11718 * @mixins OO.ui.mixin.TabIndexedElement
11719 *
11720 * @constructor
11721 * @param {OO.ui.Toolbar} toolbar
11722 * @param {Object} [config] Configuration options
11723 * @cfg {string} [header] Text to display at the top of the popup
11724 */
11725 OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
11726 // Allow passing positional parameters inside the config object
11727 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
11728 config = toolbar;
11729 toolbar = config.toolbar;
11730 }
11731
11732 // Configuration initialization
11733 config = config || {};
11734
11735 // Parent constructor
11736 OO.ui.PopupToolGroup.parent.call( this, toolbar, config );
11737
11738 // Properties
11739 this.active = false;
11740 this.dragging = false;
11741 this.onBlurHandler = this.onBlur.bind( this );
11742 this.$handle = $( '<span>' );
11743
11744 // Mixin constructors
11745 OO.ui.mixin.IconElement.call( this, config );
11746 OO.ui.mixin.IndicatorElement.call( this, config );
11747 OO.ui.mixin.LabelElement.call( this, config );
11748 OO.ui.mixin.TitledElement.call( this, config );
11749 OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
11750 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
11751
11752 // Events
11753 this.$handle.on( {
11754 keydown: this.onHandleMouseKeyDown.bind( this ),
11755 keyup: this.onHandleMouseKeyUp.bind( this ),
11756 mousedown: this.onHandleMouseKeyDown.bind( this ),
11757 mouseup: this.onHandleMouseKeyUp.bind( this )
11758 } );
11759
11760 // Initialization
11761 this.$handle
11762 .addClass( 'oo-ui-popupToolGroup-handle' )
11763 .append( this.$icon, this.$label, this.$indicator );
11764 // If the pop-up should have a header, add it to the top of the toolGroup.
11765 // Note: If this feature is useful for other widgets, we could abstract it into an
11766 // OO.ui.HeaderedElement mixin constructor.
11767 if ( config.header !== undefined ) {
11768 this.$group
11769 .prepend( $( '<span>' )
11770 .addClass( 'oo-ui-popupToolGroup-header' )
11771 .text( config.header )
11772 );
11773 }
11774 this.$element
11775 .addClass( 'oo-ui-popupToolGroup' )
11776 .prepend( this.$handle );
11777 };
11778
11779 /* Setup */
11780
11781 OO.inheritClass( OO.ui.PopupToolGroup, OO.ui.ToolGroup );
11782 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.IconElement );
11783 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.IndicatorElement );
11784 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.LabelElement );
11785 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.TitledElement );
11786 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.ClippableElement );
11787 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.TabIndexedElement );
11788
11789 /* Methods */
11790
11791 /**
11792 * @inheritdoc
11793 */
11794 OO.ui.PopupToolGroup.prototype.setDisabled = function () {
11795 // Parent method
11796 OO.ui.PopupToolGroup.parent.prototype.setDisabled.apply( this, arguments );
11797
11798 if ( this.isDisabled() && this.isElementAttached() ) {
11799 this.setActive( false );
11800 }
11801 };
11802
11803 /**
11804 * Handle focus being lost.
11805 *
11806 * The event is actually generated from a mouseup/keyup, so it is not a normal blur event object.
11807 *
11808 * @protected
11809 * @param {jQuery.Event} e Mouse up or key up event
11810 */
11811 OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) {
11812 // Only deactivate when clicking outside the dropdown element
11813 if ( $( e.target ).closest( '.oo-ui-popupToolGroup' )[ 0 ] !== this.$element[ 0 ] ) {
11814 this.setActive( false );
11815 }
11816 };
11817
11818 /**
11819 * @inheritdoc
11820 */
11821 OO.ui.PopupToolGroup.prototype.onMouseKeyUp = function ( e ) {
11822 // Only close toolgroup when a tool was actually selected
11823 if (
11824 !this.isDisabled() && this.pressed && this.pressed === this.getTargetTool( e ) &&
11825 ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
11826 ) {
11827 this.setActive( false );
11828 }
11829 return OO.ui.PopupToolGroup.parent.prototype.onMouseKeyUp.call( this, e );
11830 };
11831
11832 /**
11833 * Handle mouse up and key up events.
11834 *
11835 * @protected
11836 * @param {jQuery.Event} e Mouse up or key up event
11837 */
11838 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyUp = function ( e ) {
11839 if (
11840 !this.isDisabled() &&
11841 ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
11842 ) {
11843 return false;
11844 }
11845 };
11846
11847 /**
11848 * Handle mouse down and key down events.
11849 *
11850 * @protected
11851 * @param {jQuery.Event} e Mouse down or key down event
11852 */
11853 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyDown = function ( e ) {
11854 if (
11855 !this.isDisabled() &&
11856 ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
11857 ) {
11858 this.setActive( !this.active );
11859 return false;
11860 }
11861 };
11862
11863 /**
11864 * Switch into 'active' mode.
11865 *
11866 * When active, the popup is visible. A mouseup event anywhere in the document will trigger
11867 * deactivation.
11868 */
11869 OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
11870 var containerWidth, containerLeft;
11871 value = !!value;
11872 if ( this.active !== value ) {
11873 this.active = value;
11874 if ( value ) {
11875 this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler, true );
11876 this.getElementDocument().addEventListener( 'keyup', this.onBlurHandler, true );
11877
11878 this.$clippable.css( 'left', '' );
11879 // Try anchoring the popup to the left first
11880 this.$element.addClass( 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left' );
11881 this.toggleClipping( true );
11882 if ( this.isClippedHorizontally() ) {
11883 // Anchoring to the left caused the popup to clip, so anchor it to the right instead
11884 this.toggleClipping( false );
11885 this.$element
11886 .removeClass( 'oo-ui-popupToolGroup-left' )
11887 .addClass( 'oo-ui-popupToolGroup-right' );
11888 this.toggleClipping( true );
11889 }
11890 if ( this.isClippedHorizontally() ) {
11891 // Anchoring to the right also caused the popup to clip, so just make it fill the container
11892 containerWidth = this.$clippableScrollableContainer.width();
11893 containerLeft = this.$clippableScrollableContainer.offset().left;
11894
11895 this.toggleClipping( false );
11896 this.$element.removeClass( 'oo-ui-popupToolGroup-right' );
11897
11898 this.$clippable.css( {
11899 left: -( this.$element.offset().left - containerLeft ),
11900 width: containerWidth
11901 } );
11902 }
11903 } else {
11904 this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler, true );
11905 this.getElementDocument().removeEventListener( 'keyup', this.onBlurHandler, true );
11906 this.$element.removeClass(
11907 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left oo-ui-popupToolGroup-right'
11908 );
11909 this.toggleClipping( false );
11910 }
11911 }
11912 };
11913
11914 /**
11915 * ListToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
11916 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.MenuToolGroup MenuToolGroup}
11917 * and {@link OO.ui.BarToolGroup BarToolGroup}). The {@link OO.ui.Tool tools} in a ListToolGroup are displayed
11918 * by label in a dropdown menu. The title of the tool is used as the label text. The menu itself can be configured
11919 * with a label, icon, indicator, header, and title.
11920 *
11921 * ListToolGroups can be configured to be expanded and collapsed. Collapsed lists will have a ‘More’ option that
11922 * users can select to see the full list of tools. If a collapsed toolgroup is expanded, a ‘Fewer’ option permits
11923 * users to collapse the list again.
11924 *
11925 * ListToolGroups are created by a {@link OO.ui.ToolGroupFactory toolgroup factory} when the toolbar is set up. The factory
11926 * requires the ListToolGroup's symbolic name, 'list', which is specified along with the other configurations. For more
11927 * information about how to add tools to a ListToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
11928 *
11929 * @example
11930 * // Example of a ListToolGroup
11931 * var toolFactory = new OO.ui.ToolFactory();
11932 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
11933 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
11934 *
11935 * // Configure and register two tools
11936 * function SettingsTool() {
11937 * SettingsTool.parent.apply( this, arguments );
11938 * }
11939 * OO.inheritClass( SettingsTool, OO.ui.Tool );
11940 * SettingsTool.static.name = 'settings';
11941 * SettingsTool.static.icon = 'settings';
11942 * SettingsTool.static.title = 'Change settings';
11943 * SettingsTool.prototype.onSelect = function () {
11944 * this.setActive( false );
11945 * };
11946 * SettingsTool.prototype.onUpdateState = function () {};
11947 * toolFactory.register( SettingsTool );
11948 * // Register two more tools, nothing interesting here
11949 * function StuffTool() {
11950 * StuffTool.parent.apply( this, arguments );
11951 * }
11952 * OO.inheritClass( StuffTool, OO.ui.Tool );
11953 * StuffTool.static.name = 'stuff';
11954 * StuffTool.static.icon = 'search';
11955 * StuffTool.static.title = 'Change the world';
11956 * StuffTool.prototype.onSelect = function () {
11957 * this.setActive( false );
11958 * };
11959 * StuffTool.prototype.onUpdateState = function () {};
11960 * toolFactory.register( StuffTool );
11961 * toolbar.setup( [
11962 * {
11963 * // Configurations for list toolgroup.
11964 * type: 'list',
11965 * label: 'ListToolGroup',
11966 * indicator: 'down',
11967 * icon: 'ellipsis',
11968 * title: 'This is the title, displayed when user moves the mouse over the list toolgroup',
11969 * header: 'This is the header',
11970 * include: [ 'settings', 'stuff' ],
11971 * allowCollapse: ['stuff']
11972 * }
11973 * ] );
11974 *
11975 * // Create some UI around the toolbar and place it in the document
11976 * var frame = new OO.ui.PanelLayout( {
11977 * expanded: false,
11978 * framed: true
11979 * } );
11980 * frame.$element.append(
11981 * toolbar.$element
11982 * );
11983 * $( 'body' ).append( frame.$element );
11984 * // Build the toolbar. This must be done after the toolbar has been appended to the document.
11985 * toolbar.initialize();
11986 *
11987 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
11988 *
11989 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
11990 *
11991 * @class
11992 * @extends OO.ui.PopupToolGroup
11993 *
11994 * @constructor
11995 * @param {OO.ui.Toolbar} toolbar
11996 * @param {Object} [config] Configuration options
11997 * @cfg {Array} [allowCollapse] Allow the specified tools to be collapsed. By default, collapsible tools
11998 * will only be displayed if users click the ‘More’ option displayed at the bottom of the list. If
11999 * the list is expanded, a ‘Fewer’ option permits users to collapse the list again. Any tools that
12000 * are included in the toolgroup, but are not designated as collapsible, will always be displayed.
12001 * To open a collapsible list in its expanded state, set #expanded to 'true'.
12002 * @cfg {Array} [forceExpand] Expand the specified tools. All other tools will be designated as collapsible.
12003 * Unless #expanded is set to true, the collapsible tools will be collapsed when the list is first opened.
12004 * @cfg {boolean} [expanded=false] Expand collapsible tools. This config is only relevant if tools have
12005 * been designated as collapsible. When expanded is set to true, all tools in the group will be displayed
12006 * when the list is first opened. Users can collapse the list with a ‘Fewer’ option at the bottom.
12007 */
12008 OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
12009 // Allow passing positional parameters inside the config object
12010 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
12011 config = toolbar;
12012 toolbar = config.toolbar;
12013 }
12014
12015 // Configuration initialization
12016 config = config || {};
12017
12018 // Properties (must be set before parent constructor, which calls #populate)
12019 this.allowCollapse = config.allowCollapse;
12020 this.forceExpand = config.forceExpand;
12021 this.expanded = config.expanded !== undefined ? config.expanded : false;
12022 this.collapsibleTools = [];
12023
12024 // Parent constructor
12025 OO.ui.ListToolGroup.parent.call( this, toolbar, config );
12026
12027 // Initialization
12028 this.$element.addClass( 'oo-ui-listToolGroup' );
12029 };
12030
12031 /* Setup */
12032
12033 OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup );
12034
12035 /* Static Properties */
12036
12037 OO.ui.ListToolGroup.static.name = 'list';
12038
12039 /* Methods */
12040
12041 /**
12042 * @inheritdoc
12043 */
12044 OO.ui.ListToolGroup.prototype.populate = function () {
12045 var i, len, allowCollapse = [];
12046
12047 OO.ui.ListToolGroup.parent.prototype.populate.call( this );
12048
12049 // Update the list of collapsible tools
12050 if ( this.allowCollapse !== undefined ) {
12051 allowCollapse = this.allowCollapse;
12052 } else if ( this.forceExpand !== undefined ) {
12053 allowCollapse = OO.simpleArrayDifference( Object.keys( this.tools ), this.forceExpand );
12054 }
12055
12056 this.collapsibleTools = [];
12057 for ( i = 0, len = allowCollapse.length; i < len; i++ ) {
12058 if ( this.tools[ allowCollapse[ i ] ] !== undefined ) {
12059 this.collapsibleTools.push( this.tools[ allowCollapse[ i ] ] );
12060 }
12061 }
12062
12063 // Keep at the end, even when tools are added
12064 this.$group.append( this.getExpandCollapseTool().$element );
12065
12066 this.getExpandCollapseTool().toggle( this.collapsibleTools.length !== 0 );
12067 this.updateCollapsibleState();
12068 };
12069
12070 OO.ui.ListToolGroup.prototype.getExpandCollapseTool = function () {
12071 var ExpandCollapseTool;
12072 if ( this.expandCollapseTool === undefined ) {
12073 ExpandCollapseTool = function () {
12074 ExpandCollapseTool.parent.apply( this, arguments );
12075 };
12076
12077 OO.inheritClass( ExpandCollapseTool, OO.ui.Tool );
12078
12079 ExpandCollapseTool.prototype.onSelect = function () {
12080 this.toolGroup.expanded = !this.toolGroup.expanded;
12081 this.toolGroup.updateCollapsibleState();
12082 this.setActive( false );
12083 };
12084 ExpandCollapseTool.prototype.onUpdateState = function () {
12085 // Do nothing. Tool interface requires an implementation of this function.
12086 };
12087
12088 ExpandCollapseTool.static.name = 'more-fewer';
12089
12090 this.expandCollapseTool = new ExpandCollapseTool( this );
12091 }
12092 return this.expandCollapseTool;
12093 };
12094
12095 /**
12096 * @inheritdoc
12097 */
12098 OO.ui.ListToolGroup.prototype.onMouseKeyUp = function ( e ) {
12099 // Do not close the popup when the user wants to show more/fewer tools
12100 if (
12101 $( e.target ).closest( '.oo-ui-tool-name-more-fewer' ).length &&
12102 ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
12103 ) {
12104 // HACK: Prevent the popup list from being hidden. Skip the PopupToolGroup implementation (which
12105 // hides the popup list when a tool is selected) and call ToolGroup's implementation directly.
12106 return OO.ui.ListToolGroup.parent.parent.prototype.onMouseKeyUp.call( this, e );
12107 } else {
12108 return OO.ui.ListToolGroup.parent.prototype.onMouseKeyUp.call( this, e );
12109 }
12110 };
12111
12112 OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () {
12113 var i, len;
12114
12115 this.getExpandCollapseTool()
12116 .setIcon( this.expanded ? 'collapse' : 'expand' )
12117 .setTitle( OO.ui.msg( this.expanded ? 'ooui-toolgroup-collapse' : 'ooui-toolgroup-expand' ) );
12118
12119 for ( i = 0, len = this.collapsibleTools.length; i < len; i++ ) {
12120 this.collapsibleTools[ i ].toggle( this.expanded );
12121 }
12122 };
12123
12124 /**
12125 * MenuToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
12126 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.BarToolGroup BarToolGroup}
12127 * and {@link OO.ui.ListToolGroup ListToolGroup}). MenuToolGroups contain selectable {@link OO.ui.Tool tools},
12128 * which are displayed by label in a dropdown menu. The tool's title is used as the label text, and the
12129 * menu label is updated to reflect which tool or tools are currently selected. If no tools are selected,
12130 * the menu label is empty. The menu can be configured with an indicator, icon, title, and/or header.
12131 *
12132 * MenuToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar
12133 * is set up.
12134 *
12135 * @example
12136 * // Example of a MenuToolGroup
12137 * var toolFactory = new OO.ui.ToolFactory();
12138 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
12139 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
12140 *
12141 * // We will be placing status text in this element when tools are used
12142 * var $area = $( '<p>' ).text( 'An example of a MenuToolGroup. Select a tool from the dropdown menu.' );
12143 *
12144 * // Define the tools that we're going to place in our toolbar
12145 *
12146 * function SettingsTool() {
12147 * SettingsTool.parent.apply( this, arguments );
12148 * this.reallyActive = false;
12149 * }
12150 * OO.inheritClass( SettingsTool, OO.ui.Tool );
12151 * SettingsTool.static.name = 'settings';
12152 * SettingsTool.static.icon = 'settings';
12153 * SettingsTool.static.title = 'Change settings';
12154 * SettingsTool.prototype.onSelect = function () {
12155 * $area.text( 'Settings tool clicked!' );
12156 * // Toggle the active state on each click
12157 * this.reallyActive = !this.reallyActive;
12158 * this.setActive( this.reallyActive );
12159 * // To update the menu label
12160 * this.toolbar.emit( 'updateState' );
12161 * };
12162 * SettingsTool.prototype.onUpdateState = function () {};
12163 * toolFactory.register( SettingsTool );
12164 *
12165 * function StuffTool() {
12166 * StuffTool.parent.apply( this, arguments );
12167 * this.reallyActive = false;
12168 * }
12169 * OO.inheritClass( StuffTool, OO.ui.Tool );
12170 * StuffTool.static.name = 'stuff';
12171 * StuffTool.static.icon = 'ellipsis';
12172 * StuffTool.static.title = 'More stuff';
12173 * StuffTool.prototype.onSelect = function () {
12174 * $area.text( 'More stuff tool clicked!' );
12175 * // Toggle the active state on each click
12176 * this.reallyActive = !this.reallyActive;
12177 * this.setActive( this.reallyActive );
12178 * // To update the menu label
12179 * this.toolbar.emit( 'updateState' );
12180 * };
12181 * StuffTool.prototype.onUpdateState = function () {};
12182 * toolFactory.register( StuffTool );
12183 *
12184 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
12185 * // used once (but not all defined tools must be used).
12186 * toolbar.setup( [
12187 * {
12188 * type: 'menu',
12189 * header: 'This is the (optional) header',
12190 * title: 'This is the (optional) title',
12191 * indicator: 'down',
12192 * include: [ 'settings', 'stuff' ]
12193 * }
12194 * ] );
12195 *
12196 * // Create some UI around the toolbar and place it in the document
12197 * var frame = new OO.ui.PanelLayout( {
12198 * expanded: false,
12199 * framed: true
12200 * } );
12201 * var contentFrame = new OO.ui.PanelLayout( {
12202 * expanded: false,
12203 * padded: true
12204 * } );
12205 * frame.$element.append(
12206 * toolbar.$element,
12207 * contentFrame.$element.append( $area )
12208 * );
12209 * $( 'body' ).append( frame.$element );
12210 *
12211 * // Here is where the toolbar is actually built. This must be done after inserting it into the
12212 * // document.
12213 * toolbar.initialize();
12214 * toolbar.emit( 'updateState' );
12215 *
12216 * For more information about how to add tools to a MenuToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
12217 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki] [1].
12218 *
12219 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
12220 *
12221 * @class
12222 * @extends OO.ui.PopupToolGroup
12223 *
12224 * @constructor
12225 * @param {OO.ui.Toolbar} toolbar
12226 * @param {Object} [config] Configuration options
12227 */
12228 OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
12229 // Allow passing positional parameters inside the config object
12230 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
12231 config = toolbar;
12232 toolbar = config.toolbar;
12233 }
12234
12235 // Configuration initialization
12236 config = config || {};
12237
12238 // Parent constructor
12239 OO.ui.MenuToolGroup.parent.call( this, toolbar, config );
12240
12241 // Events
12242 this.toolbar.connect( this, { updateState: 'onUpdateState' } );
12243
12244 // Initialization
12245 this.$element.addClass( 'oo-ui-menuToolGroup' );
12246 };
12247
12248 /* Setup */
12249
12250 OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup );
12251
12252 /* Static Properties */
12253
12254 OO.ui.MenuToolGroup.static.name = 'menu';
12255
12256 /* Methods */
12257
12258 /**
12259 * Handle the toolbar state being updated.
12260 *
12261 * When the state changes, the title of each active item in the menu will be joined together and
12262 * used as a label for the group. The label will be empty if none of the items are active.
12263 *
12264 * @private
12265 */
12266 OO.ui.MenuToolGroup.prototype.onUpdateState = function () {
12267 var name,
12268 labelTexts = [];
12269
12270 for ( name in this.tools ) {
12271 if ( this.tools[ name ].isActive() ) {
12272 labelTexts.push( this.tools[ name ].getTitle() );
12273 }
12274 }
12275
12276 this.setLabel( labelTexts.join( ', ' ) || ' ' );
12277 };
12278
12279 /**
12280 * Popup tools open a popup window when they are selected from the {@link OO.ui.Toolbar toolbar}. Each popup tool is configured
12281 * 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
12282 * an #onSelect or #onUpdateState method, as these methods have been implemented already.
12283 *
12284 * // Example of a popup tool. When selected, a popup tool displays
12285 * // a popup window.
12286 * function HelpTool( toolGroup, config ) {
12287 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
12288 * padded: true,
12289 * label: 'Help',
12290 * head: true
12291 * } }, config ) );
12292 * this.popup.$body.append( '<p>I am helpful!</p>' );
12293 * };
12294 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
12295 * HelpTool.static.name = 'help';
12296 * HelpTool.static.icon = 'help';
12297 * HelpTool.static.title = 'Help';
12298 * toolFactory.register( HelpTool );
12299 *
12300 * For an example of a toolbar that contains a popup tool, see {@link OO.ui.Toolbar toolbars}. For more information about
12301 * toolbars in genreral, please see the [OOjs UI documentation on MediaWiki][1].
12302 *
12303 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
12304 *
12305 * @abstract
12306 * @class
12307 * @extends OO.ui.Tool
12308 * @mixins OO.ui.mixin.PopupElement
12309 *
12310 * @constructor
12311 * @param {OO.ui.ToolGroup} toolGroup
12312 * @param {Object} [config] Configuration options
12313 */
12314 OO.ui.PopupTool = function OoUiPopupTool( toolGroup, config ) {
12315 // Allow passing positional parameters inside the config object
12316 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
12317 config = toolGroup;
12318 toolGroup = config.toolGroup;
12319 }
12320
12321 // Parent constructor
12322 OO.ui.PopupTool.parent.call( this, toolGroup, config );
12323
12324 // Mixin constructors
12325 OO.ui.mixin.PopupElement.call( this, config );
12326
12327 // Initialization
12328 this.$element
12329 .addClass( 'oo-ui-popupTool' )
12330 .append( this.popup.$element );
12331 };
12332
12333 /* Setup */
12334
12335 OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool );
12336 OO.mixinClass( OO.ui.PopupTool, OO.ui.mixin.PopupElement );
12337
12338 /* Methods */
12339
12340 /**
12341 * Handle the tool being selected.
12342 *
12343 * @inheritdoc
12344 */
12345 OO.ui.PopupTool.prototype.onSelect = function () {
12346 if ( !this.isDisabled() ) {
12347 this.popup.toggle();
12348 }
12349 this.setActive( false );
12350 return false;
12351 };
12352
12353 /**
12354 * Handle the toolbar state being updated.
12355 *
12356 * @inheritdoc
12357 */
12358 OO.ui.PopupTool.prototype.onUpdateState = function () {
12359 this.setActive( false );
12360 };
12361
12362 /**
12363 * A ToolGroupTool is a special sort of tool that can contain other {@link OO.ui.Tool tools}
12364 * and {@link OO.ui.ToolGroup toolgroups}. The ToolGroupTool was specifically designed to be used
12365 * inside a {@link OO.ui.BarToolGroup bar} toolgroup to provide access to additional tools from
12366 * the bar item. Included tools will be displayed in a dropdown {@link OO.ui.ListToolGroup list}
12367 * when the ToolGroupTool is selected.
12368 *
12369 * // Example: ToolGroupTool with two nested tools, 'setting1' and 'setting2', defined elsewhere.
12370 *
12371 * function SettingsTool() {
12372 * SettingsTool.parent.apply( this, arguments );
12373 * };
12374 * OO.inheritClass( SettingsTool, OO.ui.ToolGroupTool );
12375 * SettingsTool.static.name = 'settings';
12376 * SettingsTool.static.title = 'Change settings';
12377 * SettingsTool.static.groupConfig = {
12378 * icon: 'settings',
12379 * label: 'ToolGroupTool',
12380 * include: [ 'setting1', 'setting2' ]
12381 * };
12382 * toolFactory.register( SettingsTool );
12383 *
12384 * For more information, please see the [OOjs UI documentation on MediaWiki][1].
12385 *
12386 * Please note that this implementation is subject to change per [T74159] [2].
12387 *
12388 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars#ToolGroupTool
12389 * [2]: https://phabricator.wikimedia.org/T74159
12390 *
12391 * @abstract
12392 * @class
12393 * @extends OO.ui.Tool
12394 *
12395 * @constructor
12396 * @param {OO.ui.ToolGroup} toolGroup
12397 * @param {Object} [config] Configuration options
12398 */
12399 OO.ui.ToolGroupTool = function OoUiToolGroupTool( toolGroup, config ) {
12400 // Allow passing positional parameters inside the config object
12401 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
12402 config = toolGroup;
12403 toolGroup = config.toolGroup;
12404 }
12405
12406 // Parent constructor
12407 OO.ui.ToolGroupTool.parent.call( this, toolGroup, config );
12408
12409 // Properties
12410 this.innerToolGroup = this.createGroup( this.constructor.static.groupConfig );
12411
12412 // Events
12413 this.innerToolGroup.connect( this, { disable: 'onToolGroupDisable' } );
12414
12415 // Initialization
12416 this.$link.remove();
12417 this.$element
12418 .addClass( 'oo-ui-toolGroupTool' )
12419 .append( this.innerToolGroup.$element );
12420 };
12421
12422 /* Setup */
12423
12424 OO.inheritClass( OO.ui.ToolGroupTool, OO.ui.Tool );
12425
12426 /* Static Properties */
12427
12428 /**
12429 * Toolgroup configuration.
12430 *
12431 * The toolgroup configuration consists of the tools to include, as well as an icon and label
12432 * to use for the bar item. Tools can be included by symbolic name, group, or with the
12433 * wildcard selector. Please see {@link OO.ui.ToolGroup toolgroup} for more information.
12434 *
12435 * @property {Object.<string,Array>}
12436 */
12437 OO.ui.ToolGroupTool.static.groupConfig = {};
12438
12439 /* Methods */
12440
12441 /**
12442 * Handle the tool being selected.
12443 *
12444 * @inheritdoc
12445 */
12446 OO.ui.ToolGroupTool.prototype.onSelect = function () {
12447 this.innerToolGroup.setActive( !this.innerToolGroup.active );
12448 return false;
12449 };
12450
12451 /**
12452 * Synchronize disabledness state of the tool with the inner toolgroup.
12453 *
12454 * @private
12455 * @param {boolean} disabled Element is disabled
12456 */
12457 OO.ui.ToolGroupTool.prototype.onToolGroupDisable = function ( disabled ) {
12458 this.setDisabled( disabled );
12459 };
12460
12461 /**
12462 * Handle the toolbar state being updated.
12463 *
12464 * @inheritdoc
12465 */
12466 OO.ui.ToolGroupTool.prototype.onUpdateState = function () {
12467 this.setActive( false );
12468 };
12469
12470 /**
12471 * Build a {@link OO.ui.ToolGroup toolgroup} from the specified configuration.
12472 *
12473 * @param {Object.<string,Array>} group Toolgroup configuration. Please see {@link OO.ui.ToolGroup toolgroup} for
12474 * more information.
12475 * @return {OO.ui.ListToolGroup}
12476 */
12477 OO.ui.ToolGroupTool.prototype.createGroup = function ( group ) {
12478 if ( group.include === '*' ) {
12479 // Apply defaults to catch-all groups
12480 if ( group.label === undefined ) {
12481 group.label = OO.ui.msg( 'ooui-toolbar-more' );
12482 }
12483 }
12484
12485 return this.toolbar.getToolGroupFactory().create( 'list', this.toolbar, group );
12486 };
12487
12488 /**
12489 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
12490 *
12491 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
12492 *
12493 * @private
12494 * @abstract
12495 * @class
12496 * @extends OO.ui.mixin.GroupElement
12497 *
12498 * @constructor
12499 * @param {Object} [config] Configuration options
12500 */
12501 OO.ui.mixin.GroupWidget = function OoUiMixinGroupWidget( config ) {
12502 // Parent constructor
12503 OO.ui.mixin.GroupWidget.parent.call( this, config );
12504 };
12505
12506 /* Setup */
12507
12508 OO.inheritClass( OO.ui.mixin.GroupWidget, OO.ui.mixin.GroupElement );
12509
12510 /* Methods */
12511
12512 /**
12513 * Set the disabled state of the widget.
12514 *
12515 * This will also update the disabled state of child widgets.
12516 *
12517 * @param {boolean} disabled Disable widget
12518 * @chainable
12519 */
12520 OO.ui.mixin.GroupWidget.prototype.setDisabled = function ( disabled ) {
12521 var i, len;
12522
12523 // Parent method
12524 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
12525 OO.ui.Widget.prototype.setDisabled.call( this, disabled );
12526
12527 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
12528 if ( this.items ) {
12529 for ( i = 0, len = this.items.length; i < len; i++ ) {
12530 this.items[ i ].updateDisabled();
12531 }
12532 }
12533
12534 return this;
12535 };
12536
12537 /**
12538 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
12539 *
12540 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
12541 * allows bidirectional communication.
12542 *
12543 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
12544 *
12545 * @private
12546 * @abstract
12547 * @class
12548 *
12549 * @constructor
12550 */
12551 OO.ui.mixin.ItemWidget = function OoUiMixinItemWidget() {
12552 //
12553 };
12554
12555 /* Methods */
12556
12557 /**
12558 * Check if widget is disabled.
12559 *
12560 * Checks parent if present, making disabled state inheritable.
12561 *
12562 * @return {boolean} Widget is disabled
12563 */
12564 OO.ui.mixin.ItemWidget.prototype.isDisabled = function () {
12565 return this.disabled ||
12566 ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() );
12567 };
12568
12569 /**
12570 * Set group element is in.
12571 *
12572 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
12573 * @chainable
12574 */
12575 OO.ui.mixin.ItemWidget.prototype.setElementGroup = function ( group ) {
12576 // Parent method
12577 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
12578 OO.ui.Element.prototype.setElementGroup.call( this, group );
12579
12580 // Initialize item disabled states
12581 this.updateDisabled();
12582
12583 return this;
12584 };
12585
12586 /**
12587 * OutlineControlsWidget is a set of controls for an {@link OO.ui.OutlineSelectWidget outline select widget}.
12588 * Controls include moving items up and down, removing items, and adding different kinds of items.
12589 *
12590 * **Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.**
12591 *
12592 * @class
12593 * @extends OO.ui.Widget
12594 * @mixins OO.ui.mixin.GroupElement
12595 * @mixins OO.ui.mixin.IconElement
12596 *
12597 * @constructor
12598 * @param {OO.ui.OutlineSelectWidget} outline Outline to control
12599 * @param {Object} [config] Configuration options
12600 * @cfg {Object} [abilities] List of abilties
12601 * @cfg {boolean} [abilities.move=true] Allow moving movable items
12602 * @cfg {boolean} [abilities.remove=true] Allow removing removable items
12603 */
12604 OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
12605 // Allow passing positional parameters inside the config object
12606 if ( OO.isPlainObject( outline ) && config === undefined ) {
12607 config = outline;
12608 outline = config.outline;
12609 }
12610
12611 // Configuration initialization
12612 config = $.extend( { icon: 'add' }, config );
12613
12614 // Parent constructor
12615 OO.ui.OutlineControlsWidget.parent.call( this, config );
12616
12617 // Mixin constructors
12618 OO.ui.mixin.GroupElement.call( this, config );
12619 OO.ui.mixin.IconElement.call( this, config );
12620
12621 // Properties
12622 this.outline = outline;
12623 this.$movers = $( '<div>' );
12624 this.upButton = new OO.ui.ButtonWidget( {
12625 framed: false,
12626 icon: 'collapse',
12627 title: OO.ui.msg( 'ooui-outline-control-move-up' )
12628 } );
12629 this.downButton = new OO.ui.ButtonWidget( {
12630 framed: false,
12631 icon: 'expand',
12632 title: OO.ui.msg( 'ooui-outline-control-move-down' )
12633 } );
12634 this.removeButton = new OO.ui.ButtonWidget( {
12635 framed: false,
12636 icon: 'remove',
12637 title: OO.ui.msg( 'ooui-outline-control-remove' )
12638 } );
12639 this.abilities = { move: true, remove: true };
12640
12641 // Events
12642 outline.connect( this, {
12643 select: 'onOutlineChange',
12644 add: 'onOutlineChange',
12645 remove: 'onOutlineChange'
12646 } );
12647 this.upButton.connect( this, { click: [ 'emit', 'move', -1 ] } );
12648 this.downButton.connect( this, { click: [ 'emit', 'move', 1 ] } );
12649 this.removeButton.connect( this, { click: [ 'emit', 'remove' ] } );
12650
12651 // Initialization
12652 this.$element.addClass( 'oo-ui-outlineControlsWidget' );
12653 this.$group.addClass( 'oo-ui-outlineControlsWidget-items' );
12654 this.$movers
12655 .addClass( 'oo-ui-outlineControlsWidget-movers' )
12656 .append( this.removeButton.$element, this.upButton.$element, this.downButton.$element );
12657 this.$element.append( this.$icon, this.$group, this.$movers );
12658 this.setAbilities( config.abilities || {} );
12659 };
12660
12661 /* Setup */
12662
12663 OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget );
12664 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.mixin.GroupElement );
12665 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.mixin.IconElement );
12666
12667 /* Events */
12668
12669 /**
12670 * @event move
12671 * @param {number} places Number of places to move
12672 */
12673
12674 /**
12675 * @event remove
12676 */
12677
12678 /* Methods */
12679
12680 /**
12681 * Set abilities.
12682 *
12683 * @param {Object} abilities List of abilties
12684 * @param {boolean} [abilities.move] Allow moving movable items
12685 * @param {boolean} [abilities.remove] Allow removing removable items
12686 */
12687 OO.ui.OutlineControlsWidget.prototype.setAbilities = function ( abilities ) {
12688 var ability;
12689
12690 for ( ability in this.abilities ) {
12691 if ( abilities[ ability ] !== undefined ) {
12692 this.abilities[ ability ] = !!abilities[ ability ];
12693 }
12694 }
12695
12696 this.onOutlineChange();
12697 };
12698
12699 /**
12700 * @private
12701 * Handle outline change events.
12702 */
12703 OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () {
12704 var i, len, firstMovable, lastMovable,
12705 items = this.outline.getItems(),
12706 selectedItem = this.outline.getSelectedItem(),
12707 movable = this.abilities.move && selectedItem && selectedItem.isMovable(),
12708 removable = this.abilities.remove && selectedItem && selectedItem.isRemovable();
12709
12710 if ( movable ) {
12711 i = -1;
12712 len = items.length;
12713 while ( ++i < len ) {
12714 if ( items[ i ].isMovable() ) {
12715 firstMovable = items[ i ];
12716 break;
12717 }
12718 }
12719 i = len;
12720 while ( i-- ) {
12721 if ( items[ i ].isMovable() ) {
12722 lastMovable = items[ i ];
12723 break;
12724 }
12725 }
12726 }
12727 this.upButton.setDisabled( !movable || selectedItem === firstMovable );
12728 this.downButton.setDisabled( !movable || selectedItem === lastMovable );
12729 this.removeButton.setDisabled( !removable );
12730 };
12731
12732 /**
12733 * ToggleWidget implements basic behavior of widgets with an on/off state.
12734 * Please see OO.ui.ToggleButtonWidget and OO.ui.ToggleSwitchWidget for examples.
12735 *
12736 * @abstract
12737 * @class
12738 * @extends OO.ui.Widget
12739 *
12740 * @constructor
12741 * @param {Object} [config] Configuration options
12742 * @cfg {boolean} [value=false] The toggle’s initial on/off state.
12743 * By default, the toggle is in the 'off' state.
12744 */
12745 OO.ui.ToggleWidget = function OoUiToggleWidget( config ) {
12746 // Configuration initialization
12747 config = config || {};
12748
12749 // Parent constructor
12750 OO.ui.ToggleWidget.parent.call( this, config );
12751
12752 // Properties
12753 this.value = null;
12754
12755 // Initialization
12756 this.$element.addClass( 'oo-ui-toggleWidget' );
12757 this.setValue( !!config.value );
12758 };
12759
12760 /* Setup */
12761
12762 OO.inheritClass( OO.ui.ToggleWidget, OO.ui.Widget );
12763
12764 /* Events */
12765
12766 /**
12767 * @event change
12768 *
12769 * A change event is emitted when the on/off state of the toggle changes.
12770 *
12771 * @param {boolean} value Value representing the new state of the toggle
12772 */
12773
12774 /* Methods */
12775
12776 /**
12777 * Get the value representing the toggle’s state.
12778 *
12779 * @return {boolean} The on/off state of the toggle
12780 */
12781 OO.ui.ToggleWidget.prototype.getValue = function () {
12782 return this.value;
12783 };
12784
12785 /**
12786 * Set the state of the toggle: `true` for 'on', `false' for 'off'.
12787 *
12788 * @param {boolean} value The state of the toggle
12789 * @fires change
12790 * @chainable
12791 */
12792 OO.ui.ToggleWidget.prototype.setValue = function ( value ) {
12793 value = !!value;
12794 if ( this.value !== value ) {
12795 this.value = value;
12796 this.emit( 'change', value );
12797 this.$element.toggleClass( 'oo-ui-toggleWidget-on', value );
12798 this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value );
12799 this.$element.attr( 'aria-checked', value.toString() );
12800 }
12801 return this;
12802 };
12803
12804 /**
12805 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
12806 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
12807 * removed, and cleared from the group.
12808 *
12809 * @example
12810 * // Example: A ButtonGroupWidget with two buttons
12811 * var button1 = new OO.ui.PopupButtonWidget( {
12812 * label: 'Select a category',
12813 * icon: 'menu',
12814 * popup: {
12815 * $content: $( '<p>List of categories...</p>' ),
12816 * padded: true,
12817 * align: 'left'
12818 * }
12819 * } );
12820 * var button2 = new OO.ui.ButtonWidget( {
12821 * label: 'Add item'
12822 * });
12823 * var buttonGroup = new OO.ui.ButtonGroupWidget( {
12824 * items: [button1, button2]
12825 * } );
12826 * $( 'body' ).append( buttonGroup.$element );
12827 *
12828 * @class
12829 * @extends OO.ui.Widget
12830 * @mixins OO.ui.mixin.GroupElement
12831 *
12832 * @constructor
12833 * @param {Object} [config] Configuration options
12834 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
12835 */
12836 OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
12837 // Configuration initialization
12838 config = config || {};
12839
12840 // Parent constructor
12841 OO.ui.ButtonGroupWidget.parent.call( this, config );
12842
12843 // Mixin constructors
12844 OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
12845
12846 // Initialization
12847 this.$element.addClass( 'oo-ui-buttonGroupWidget' );
12848 if ( Array.isArray( config.items ) ) {
12849 this.addItems( config.items );
12850 }
12851 };
12852
12853 /* Setup */
12854
12855 OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget );
12856 OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.mixin.GroupElement );
12857
12858 /**
12859 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
12860 * feels, and functionality can be customized via the class’s configuration options
12861 * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
12862 * and examples.
12863 *
12864 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
12865 *
12866 * @example
12867 * // A button widget
12868 * var button = new OO.ui.ButtonWidget( {
12869 * label: 'Button with Icon',
12870 * icon: 'remove',
12871 * iconTitle: 'Remove'
12872 * } );
12873 * $( 'body' ).append( button.$element );
12874 *
12875 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
12876 *
12877 * @class
12878 * @extends OO.ui.Widget
12879 * @mixins OO.ui.mixin.ButtonElement
12880 * @mixins OO.ui.mixin.IconElement
12881 * @mixins OO.ui.mixin.IndicatorElement
12882 * @mixins OO.ui.mixin.LabelElement
12883 * @mixins OO.ui.mixin.TitledElement
12884 * @mixins OO.ui.mixin.FlaggedElement
12885 * @mixins OO.ui.mixin.TabIndexedElement
12886 * @mixins OO.ui.mixin.AccessKeyedElement
12887 *
12888 * @constructor
12889 * @param {Object} [config] Configuration options
12890 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
12891 * @cfg {string} [target] The frame or window in which to open the hyperlink.
12892 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
12893 */
12894 OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
12895 // Configuration initialization
12896 config = config || {};
12897
12898 // Parent constructor
12899 OO.ui.ButtonWidget.parent.call( this, config );
12900
12901 // Mixin constructors
12902 OO.ui.mixin.ButtonElement.call( this, config );
12903 OO.ui.mixin.IconElement.call( this, config );
12904 OO.ui.mixin.IndicatorElement.call( this, config );
12905 OO.ui.mixin.LabelElement.call( this, config );
12906 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
12907 OO.ui.mixin.FlaggedElement.call( this, config );
12908 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
12909 OO.ui.mixin.AccessKeyedElement.call( this, $.extend( {}, config, { $accessKeyed: this.$button } ) );
12910
12911 // Properties
12912 this.href = null;
12913 this.target = null;
12914 this.noFollow = false;
12915
12916 // Events
12917 this.connect( this, { disable: 'onDisable' } );
12918
12919 // Initialization
12920 this.$button.append( this.$icon, this.$label, this.$indicator );
12921 this.$element
12922 .addClass( 'oo-ui-buttonWidget' )
12923 .append( this.$button );
12924 this.setHref( config.href );
12925 this.setTarget( config.target );
12926 this.setNoFollow( config.noFollow );
12927 };
12928
12929 /* Setup */
12930
12931 OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget );
12932 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.ButtonElement );
12933 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.IconElement );
12934 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.IndicatorElement );
12935 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.LabelElement );
12936 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.TitledElement );
12937 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.FlaggedElement );
12938 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.TabIndexedElement );
12939 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.AccessKeyedElement );
12940
12941 /* Methods */
12942
12943 /**
12944 * @inheritdoc
12945 */
12946 OO.ui.ButtonWidget.prototype.onMouseDown = function ( e ) {
12947 if ( !this.isDisabled() ) {
12948 // Remove the tab-index while the button is down to prevent the button from stealing focus
12949 this.$button.removeAttr( 'tabindex' );
12950 }
12951
12952 return OO.ui.mixin.ButtonElement.prototype.onMouseDown.call( this, e );
12953 };
12954
12955 /**
12956 * @inheritdoc
12957 */
12958 OO.ui.ButtonWidget.prototype.onMouseUp = function ( e ) {
12959 if ( !this.isDisabled() ) {
12960 // Restore the tab-index after the button is up to restore the button's accessibility
12961 this.$button.attr( 'tabindex', this.tabIndex );
12962 }
12963
12964 return OO.ui.mixin.ButtonElement.prototype.onMouseUp.call( this, e );
12965 };
12966
12967 /**
12968 * Get hyperlink location.
12969 *
12970 * @return {string} Hyperlink location
12971 */
12972 OO.ui.ButtonWidget.prototype.getHref = function () {
12973 return this.href;
12974 };
12975
12976 /**
12977 * Get hyperlink target.
12978 *
12979 * @return {string} Hyperlink target
12980 */
12981 OO.ui.ButtonWidget.prototype.getTarget = function () {
12982 return this.target;
12983 };
12984
12985 /**
12986 * Get search engine traversal hint.
12987 *
12988 * @return {boolean} Whether search engines should avoid traversing this hyperlink
12989 */
12990 OO.ui.ButtonWidget.prototype.getNoFollow = function () {
12991 return this.noFollow;
12992 };
12993
12994 /**
12995 * Set hyperlink location.
12996 *
12997 * @param {string|null} href Hyperlink location, null to remove
12998 */
12999 OO.ui.ButtonWidget.prototype.setHref = function ( href ) {
13000 href = typeof href === 'string' ? href : null;
13001 if ( href !== null && !OO.ui.isSafeUrl( href ) ) {
13002 href = './' + href;
13003 }
13004
13005 if ( href !== this.href ) {
13006 this.href = href;
13007 this.updateHref();
13008 }
13009
13010 return this;
13011 };
13012
13013 /**
13014 * Update the `href` attribute, in case of changes to href or
13015 * disabled state.
13016 *
13017 * @private
13018 * @chainable
13019 */
13020 OO.ui.ButtonWidget.prototype.updateHref = function () {
13021 if ( this.href !== null && !this.isDisabled() ) {
13022 this.$button.attr( 'href', this.href );
13023 } else {
13024 this.$button.removeAttr( 'href' );
13025 }
13026
13027 return this;
13028 };
13029
13030 /**
13031 * Handle disable events.
13032 *
13033 * @private
13034 * @param {boolean} disabled Element is disabled
13035 */
13036 OO.ui.ButtonWidget.prototype.onDisable = function () {
13037 this.updateHref();
13038 };
13039
13040 /**
13041 * Set hyperlink target.
13042 *
13043 * @param {string|null} target Hyperlink target, null to remove
13044 */
13045 OO.ui.ButtonWidget.prototype.setTarget = function ( target ) {
13046 target = typeof target === 'string' ? target : null;
13047
13048 if ( target !== this.target ) {
13049 this.target = target;
13050 if ( target !== null ) {
13051 this.$button.attr( 'target', target );
13052 } else {
13053 this.$button.removeAttr( 'target' );
13054 }
13055 }
13056
13057 return this;
13058 };
13059
13060 /**
13061 * Set search engine traversal hint.
13062 *
13063 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
13064 */
13065 OO.ui.ButtonWidget.prototype.setNoFollow = function ( noFollow ) {
13066 noFollow = typeof noFollow === 'boolean' ? noFollow : true;
13067
13068 if ( noFollow !== this.noFollow ) {
13069 this.noFollow = noFollow;
13070 if ( noFollow ) {
13071 this.$button.attr( 'rel', 'nofollow' );
13072 } else {
13073 this.$button.removeAttr( 'rel' );
13074 }
13075 }
13076
13077 return this;
13078 };
13079
13080 /**
13081 * An ActionWidget is a {@link OO.ui.ButtonWidget button widget} that executes an action.
13082 * Action widgets are used with OO.ui.ActionSet, which manages the behavior and availability
13083 * of the actions.
13084 *
13085 * Both actions and action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
13086 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information
13087 * and examples.
13088 *
13089 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
13090 *
13091 * @class
13092 * @extends OO.ui.ButtonWidget
13093 * @mixins OO.ui.mixin.PendingElement
13094 *
13095 * @constructor
13096 * @param {Object} [config] Configuration options
13097 * @cfg {string} [action] Symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
13098 * @cfg {string[]} [modes] Symbolic names of the modes (e.g., ‘edit’ or ‘read’) in which the action
13099 * should be made available. See the action set's {@link OO.ui.ActionSet#setMode setMode} method
13100 * for more information about setting modes.
13101 * @cfg {boolean} [framed=false] Render the action button with a frame
13102 */
13103 OO.ui.ActionWidget = function OoUiActionWidget( config ) {
13104 // Configuration initialization
13105 config = $.extend( { framed: false }, config );
13106
13107 // Parent constructor
13108 OO.ui.ActionWidget.parent.call( this, config );
13109
13110 // Mixin constructors
13111 OO.ui.mixin.PendingElement.call( this, config );
13112
13113 // Properties
13114 this.action = config.action || '';
13115 this.modes = config.modes || [];
13116 this.width = 0;
13117 this.height = 0;
13118
13119 // Initialization
13120 this.$element.addClass( 'oo-ui-actionWidget' );
13121 };
13122
13123 /* Setup */
13124
13125 OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget );
13126 OO.mixinClass( OO.ui.ActionWidget, OO.ui.mixin.PendingElement );
13127
13128 /* Events */
13129
13130 /**
13131 * A resize event is emitted when the size of the widget changes.
13132 *
13133 * @event resize
13134 */
13135
13136 /* Methods */
13137
13138 /**
13139 * Check if the action is configured to be available in the specified `mode`.
13140 *
13141 * @param {string} mode Name of mode
13142 * @return {boolean} The action is configured with the mode
13143 */
13144 OO.ui.ActionWidget.prototype.hasMode = function ( mode ) {
13145 return this.modes.indexOf( mode ) !== -1;
13146 };
13147
13148 /**
13149 * Get the symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
13150 *
13151 * @return {string}
13152 */
13153 OO.ui.ActionWidget.prototype.getAction = function () {
13154 return this.action;
13155 };
13156
13157 /**
13158 * Get the symbolic name of the mode or modes for which the action is configured to be available.
13159 *
13160 * The current mode is set with the action set's {@link OO.ui.ActionSet#setMode setMode} method.
13161 * Only actions that are configured to be avaiable in the current mode will be visible. All other actions
13162 * are hidden.
13163 *
13164 * @return {string[]}
13165 */
13166 OO.ui.ActionWidget.prototype.getModes = function () {
13167 return this.modes.slice();
13168 };
13169
13170 /**
13171 * Emit a resize event if the size has changed.
13172 *
13173 * @private
13174 * @chainable
13175 */
13176 OO.ui.ActionWidget.prototype.propagateResize = function () {
13177 var width, height;
13178
13179 if ( this.isElementAttached() ) {
13180 width = this.$element.width();
13181 height = this.$element.height();
13182
13183 if ( width !== this.width || height !== this.height ) {
13184 this.width = width;
13185 this.height = height;
13186 this.emit( 'resize' );
13187 }
13188 }
13189
13190 return this;
13191 };
13192
13193 /**
13194 * @inheritdoc
13195 */
13196 OO.ui.ActionWidget.prototype.setIcon = function () {
13197 // Mixin method
13198 OO.ui.mixin.IconElement.prototype.setIcon.apply( this, arguments );
13199 this.propagateResize();
13200
13201 return this;
13202 };
13203
13204 /**
13205 * @inheritdoc
13206 */
13207 OO.ui.ActionWidget.prototype.setLabel = function () {
13208 // Mixin method
13209 OO.ui.mixin.LabelElement.prototype.setLabel.apply( this, arguments );
13210 this.propagateResize();
13211
13212 return this;
13213 };
13214
13215 /**
13216 * @inheritdoc
13217 */
13218 OO.ui.ActionWidget.prototype.setFlags = function () {
13219 // Mixin method
13220 OO.ui.mixin.FlaggedElement.prototype.setFlags.apply( this, arguments );
13221 this.propagateResize();
13222
13223 return this;
13224 };
13225
13226 /**
13227 * @inheritdoc
13228 */
13229 OO.ui.ActionWidget.prototype.clearFlags = function () {
13230 // Mixin method
13231 OO.ui.mixin.FlaggedElement.prototype.clearFlags.apply( this, arguments );
13232 this.propagateResize();
13233
13234 return this;
13235 };
13236
13237 /**
13238 * Toggle the visibility of the action button.
13239 *
13240 * @param {boolean} [show] Show button, omit to toggle visibility
13241 * @chainable
13242 */
13243 OO.ui.ActionWidget.prototype.toggle = function () {
13244 // Parent method
13245 OO.ui.ActionWidget.parent.prototype.toggle.apply( this, arguments );
13246 this.propagateResize();
13247
13248 return this;
13249 };
13250
13251 /**
13252 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
13253 * which is used to display additional information or options.
13254 *
13255 * @example
13256 * // Example of a popup button.
13257 * var popupButton = new OO.ui.PopupButtonWidget( {
13258 * label: 'Popup button with options',
13259 * icon: 'menu',
13260 * popup: {
13261 * $content: $( '<p>Additional options here.</p>' ),
13262 * padded: true,
13263 * align: 'force-left'
13264 * }
13265 * } );
13266 * // Append the button to the DOM.
13267 * $( 'body' ).append( popupButton.$element );
13268 *
13269 * @class
13270 * @extends OO.ui.ButtonWidget
13271 * @mixins OO.ui.mixin.PopupElement
13272 *
13273 * @constructor
13274 * @param {Object} [config] Configuration options
13275 */
13276 OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
13277 // Parent constructor
13278 OO.ui.PopupButtonWidget.parent.call( this, config );
13279
13280 // Mixin constructors
13281 OO.ui.mixin.PopupElement.call( this, config );
13282
13283 // Events
13284 this.connect( this, { click: 'onAction' } );
13285
13286 // Initialization
13287 this.$element
13288 .addClass( 'oo-ui-popupButtonWidget' )
13289 .attr( 'aria-haspopup', 'true' )
13290 .append( this.popup.$element );
13291 };
13292
13293 /* Setup */
13294
13295 OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
13296 OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.mixin.PopupElement );
13297
13298 /* Methods */
13299
13300 /**
13301 * Handle the button action being triggered.
13302 *
13303 * @private
13304 */
13305 OO.ui.PopupButtonWidget.prototype.onAction = function () {
13306 this.popup.toggle();
13307 };
13308
13309 /**
13310 * ToggleButtons are buttons that have a state (‘on’ or ‘off’) that is represented by a
13311 * Boolean value. Like other {@link OO.ui.ButtonWidget buttons}, toggle buttons can be
13312 * configured with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators},
13313 * {@link OO.ui.mixin.TitledElement titles}, {@link OO.ui.mixin.FlaggedElement styling flags},
13314 * and {@link OO.ui.mixin.LabelElement labels}. Please see
13315 * the [OOjs UI documentation][1] on MediaWiki for more information.
13316 *
13317 * @example
13318 * // Toggle buttons in the 'off' and 'on' state.
13319 * var toggleButton1 = new OO.ui.ToggleButtonWidget( {
13320 * label: 'Toggle Button off'
13321 * } );
13322 * var toggleButton2 = new OO.ui.ToggleButtonWidget( {
13323 * label: 'Toggle Button on',
13324 * value: true
13325 * } );
13326 * // Append the buttons to the DOM.
13327 * $( 'body' ).append( toggleButton1.$element, toggleButton2.$element );
13328 *
13329 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Toggle_buttons
13330 *
13331 * @class
13332 * @extends OO.ui.ToggleWidget
13333 * @mixins OO.ui.mixin.ButtonElement
13334 * @mixins OO.ui.mixin.IconElement
13335 * @mixins OO.ui.mixin.IndicatorElement
13336 * @mixins OO.ui.mixin.LabelElement
13337 * @mixins OO.ui.mixin.TitledElement
13338 * @mixins OO.ui.mixin.FlaggedElement
13339 * @mixins OO.ui.mixin.TabIndexedElement
13340 *
13341 * @constructor
13342 * @param {Object} [config] Configuration options
13343 * @cfg {boolean} [value=false] The toggle button’s initial on/off
13344 * state. By default, the button is in the 'off' state.
13345 */
13346 OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) {
13347 // Configuration initialization
13348 config = config || {};
13349
13350 // Parent constructor
13351 OO.ui.ToggleButtonWidget.parent.call( this, config );
13352
13353 // Mixin constructors
13354 OO.ui.mixin.ButtonElement.call( this, config );
13355 OO.ui.mixin.IconElement.call( this, config );
13356 OO.ui.mixin.IndicatorElement.call( this, config );
13357 OO.ui.mixin.LabelElement.call( this, config );
13358 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
13359 OO.ui.mixin.FlaggedElement.call( this, config );
13360 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
13361
13362 // Events
13363 this.connect( this, { click: 'onAction' } );
13364
13365 // Initialization
13366 this.$button.append( this.$icon, this.$label, this.$indicator );
13367 this.$element
13368 .addClass( 'oo-ui-toggleButtonWidget' )
13369 .append( this.$button );
13370 };
13371
13372 /* Setup */
13373
13374 OO.inheritClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget );
13375 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.ButtonElement );
13376 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.IconElement );
13377 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.IndicatorElement );
13378 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.LabelElement );
13379 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.TitledElement );
13380 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.FlaggedElement );
13381 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.TabIndexedElement );
13382
13383 /* Methods */
13384
13385 /**
13386 * Handle the button action being triggered.
13387 *
13388 * @private
13389 */
13390 OO.ui.ToggleButtonWidget.prototype.onAction = function () {
13391 this.setValue( !this.value );
13392 };
13393
13394 /**
13395 * @inheritdoc
13396 */
13397 OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
13398 value = !!value;
13399 if ( value !== this.value ) {
13400 // Might be called from parent constructor before ButtonElement constructor
13401 if ( this.$button ) {
13402 this.$button.attr( 'aria-pressed', value.toString() );
13403 }
13404 this.setActive( value );
13405 }
13406
13407 // Parent method
13408 OO.ui.ToggleButtonWidget.parent.prototype.setValue.call( this, value );
13409
13410 return this;
13411 };
13412
13413 /**
13414 * @inheritdoc
13415 */
13416 OO.ui.ToggleButtonWidget.prototype.setButtonElement = function ( $button ) {
13417 if ( this.$button ) {
13418 this.$button.removeAttr( 'aria-pressed' );
13419 }
13420 OO.ui.mixin.ButtonElement.prototype.setButtonElement.call( this, $button );
13421 this.$button.attr( 'aria-pressed', this.value.toString() );
13422 };
13423
13424 /**
13425 * CapsuleMultiSelectWidgets are something like a {@link OO.ui.ComboBoxInputWidget combo box widget}
13426 * that allows for selecting multiple values.
13427 *
13428 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
13429 *
13430 * @example
13431 * // Example: A CapsuleMultiSelectWidget.
13432 * var capsule = new OO.ui.CapsuleMultiSelectWidget( {
13433 * label: 'CapsuleMultiSelectWidget',
13434 * selected: [ 'Option 1', 'Option 3' ],
13435 * menu: {
13436 * items: [
13437 * new OO.ui.MenuOptionWidget( {
13438 * data: 'Option 1',
13439 * label: 'Option One'
13440 * } ),
13441 * new OO.ui.MenuOptionWidget( {
13442 * data: 'Option 2',
13443 * label: 'Option Two'
13444 * } ),
13445 * new OO.ui.MenuOptionWidget( {
13446 * data: 'Option 3',
13447 * label: 'Option Three'
13448 * } ),
13449 * new OO.ui.MenuOptionWidget( {
13450 * data: 'Option 4',
13451 * label: 'Option Four'
13452 * } ),
13453 * new OO.ui.MenuOptionWidget( {
13454 * data: 'Option 5',
13455 * label: 'Option Five'
13456 * } )
13457 * ]
13458 * }
13459 * } );
13460 * $( 'body' ).append( capsule.$element );
13461 *
13462 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
13463 *
13464 * @class
13465 * @extends OO.ui.Widget
13466 * @mixins OO.ui.mixin.TabIndexedElement
13467 * @mixins OO.ui.mixin.GroupElement
13468 *
13469 * @constructor
13470 * @param {Object} [config] Configuration options
13471 * @cfg {boolean} [allowArbitrary=false] Allow data items to be added even if not present in the menu.
13472 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}.
13473 * @cfg {Object} [popup] Configuration options to pass to the {@link OO.ui.PopupWidget popup widget}.
13474 * If specified, this popup will be shown instead of the menu (but the menu
13475 * will still be used for item labels and allowArbitrary=false). The widgets
13476 * in the popup should use this.addItemsFromData() or this.addItems() as necessary.
13477 * @cfg {jQuery} [$overlay] Render the menu or popup into a separate layer.
13478 * This configuration is useful in cases where the expanded menu is larger than
13479 * its containing `<div>`. The specified overlay layer is usually on top of
13480 * the containing `<div>` and has a larger area. By default, the menu uses
13481 * relative positioning.
13482 */
13483 OO.ui.CapsuleMultiSelectWidget = function OoUiCapsuleMultiSelectWidget( config ) {
13484 var $tabFocus;
13485
13486 // Configuration initialization
13487 config = config || {};
13488
13489 // Parent constructor
13490 OO.ui.CapsuleMultiSelectWidget.parent.call( this, config );
13491
13492 // Properties (must be set before mixin constructor calls)
13493 this.$input = config.popup ? null : $( '<input>' );
13494 this.$handle = $( '<div>' );
13495
13496 // Mixin constructors
13497 OO.ui.mixin.GroupElement.call( this, config );
13498 if ( config.popup ) {
13499 config.popup = $.extend( {}, config.popup, {
13500 align: 'forwards',
13501 anchor: false
13502 } );
13503 OO.ui.mixin.PopupElement.call( this, config );
13504 $tabFocus = $( '<span>' );
13505 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: $tabFocus } ) );
13506 } else {
13507 this.popup = null;
13508 $tabFocus = null;
13509 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$input } ) );
13510 }
13511 OO.ui.mixin.IndicatorElement.call( this, config );
13512 OO.ui.mixin.IconElement.call( this, config );
13513
13514 // Properties
13515 this.$content = $( '<div>' );
13516 this.allowArbitrary = !!config.allowArbitrary;
13517 this.$overlay = config.$overlay || this.$element;
13518 this.menu = new OO.ui.FloatingMenuSelectWidget( $.extend(
13519 {
13520 widget: this,
13521 $input: this.$input,
13522 $container: this.$element,
13523 filterFromInput: true,
13524 disabled: this.isDisabled()
13525 },
13526 config.menu
13527 ) );
13528
13529 // Events
13530 if ( this.popup ) {
13531 $tabFocus.on( {
13532 focus: this.onFocusForPopup.bind( this )
13533 } );
13534 this.popup.$element.on( 'focusout', this.onPopupFocusOut.bind( this ) );
13535 if ( this.popup.$autoCloseIgnore ) {
13536 this.popup.$autoCloseIgnore.on( 'focusout', this.onPopupFocusOut.bind( this ) );
13537 }
13538 this.popup.connect( this, {
13539 toggle: function ( visible ) {
13540 $tabFocus.toggle( !visible );
13541 }
13542 } );
13543 } else {
13544 this.$input.on( {
13545 focus: this.onInputFocus.bind( this ),
13546 blur: this.onInputBlur.bind( this ),
13547 'propertychange change click mouseup keydown keyup input cut paste select focus':
13548 OO.ui.debounce( this.updateInputSize.bind( this ) ),
13549 keydown: this.onKeyDown.bind( this ),
13550 keypress: this.onKeyPress.bind( this )
13551 } );
13552 }
13553 this.menu.connect( this, {
13554 choose: 'onMenuChoose',
13555 add: 'onMenuItemsChange',
13556 remove: 'onMenuItemsChange'
13557 } );
13558 this.$handle.on( {
13559 mousedown: this.onMouseDown.bind( this )
13560 } );
13561
13562 // Initialization
13563 if ( this.$input ) {
13564 this.$input.prop( 'disabled', this.isDisabled() );
13565 this.$input.attr( {
13566 role: 'combobox',
13567 'aria-autocomplete': 'list'
13568 } );
13569 this.updateInputSize();
13570 }
13571 if ( config.data ) {
13572 this.setItemsFromData( config.data );
13573 }
13574 this.$content.addClass( 'oo-ui-capsuleMultiSelectWidget-content' )
13575 .append( this.$group );
13576 this.$group.addClass( 'oo-ui-capsuleMultiSelectWidget-group' );
13577 this.$handle.addClass( 'oo-ui-capsuleMultiSelectWidget-handle' )
13578 .append( this.$indicator, this.$icon, this.$content );
13579 this.$element.addClass( 'oo-ui-capsuleMultiSelectWidget' )
13580 .append( this.$handle );
13581 if ( this.popup ) {
13582 this.$content.append( $tabFocus );
13583 this.$overlay.append( this.popup.$element );
13584 } else {
13585 this.$content.append( this.$input );
13586 this.$overlay.append( this.menu.$element );
13587 }
13588 this.onMenuItemsChange();
13589 };
13590
13591 /* Setup */
13592
13593 OO.inheritClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.Widget );
13594 OO.mixinClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.mixin.GroupElement );
13595 OO.mixinClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.mixin.PopupElement );
13596 OO.mixinClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.mixin.TabIndexedElement );
13597 OO.mixinClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.mixin.IndicatorElement );
13598 OO.mixinClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.mixin.IconElement );
13599
13600 /* Events */
13601
13602 /**
13603 * @event change
13604 *
13605 * A change event is emitted when the set of selected items changes.
13606 *
13607 * @param {Mixed[]} datas Data of the now-selected items
13608 */
13609
13610 /* Methods */
13611
13612 /**
13613 * Construct a OO.ui.CapsuleItemWidget (or a subclass thereof) from given label and data.
13614 *
13615 * @protected
13616 * @param {Mixed} data Custom data of any type.
13617 * @param {string} label The label text.
13618 * @return {OO.ui.CapsuleItemWidget}
13619 */
13620 OO.ui.CapsuleMultiSelectWidget.prototype.createItemWidget = function ( data, label ) {
13621 return new OO.ui.CapsuleItemWidget( { data: data, label: label } );
13622 };
13623
13624 /**
13625 * Get the data of the items in the capsule
13626 * @return {Mixed[]}
13627 */
13628 OO.ui.CapsuleMultiSelectWidget.prototype.getItemsData = function () {
13629 return $.map( this.getItems(), function ( e ) { return e.data; } );
13630 };
13631
13632 /**
13633 * Set the items in the capsule by providing data
13634 * @chainable
13635 * @param {Mixed[]} datas
13636 * @return {OO.ui.CapsuleMultiSelectWidget}
13637 */
13638 OO.ui.CapsuleMultiSelectWidget.prototype.setItemsFromData = function ( datas ) {
13639 var widget = this,
13640 menu = this.menu,
13641 items = this.getItems();
13642
13643 $.each( datas, function ( i, data ) {
13644 var j, label,
13645 item = menu.getItemFromData( data );
13646
13647 if ( item ) {
13648 label = item.label;
13649 } else if ( widget.allowArbitrary ) {
13650 label = String( data );
13651 } else {
13652 return;
13653 }
13654
13655 item = null;
13656 for ( j = 0; j < items.length; j++ ) {
13657 if ( items[ j ].data === data && items[ j ].label === label ) {
13658 item = items[ j ];
13659 items.splice( j, 1 );
13660 break;
13661 }
13662 }
13663 if ( !item ) {
13664 item = widget.createItemWidget( data, label );
13665 }
13666 widget.addItems( [ item ], i );
13667 } );
13668
13669 if ( items.length ) {
13670 widget.removeItems( items );
13671 }
13672
13673 return this;
13674 };
13675
13676 /**
13677 * Add items to the capsule by providing their data
13678 * @chainable
13679 * @param {Mixed[]} datas
13680 * @return {OO.ui.CapsuleMultiSelectWidget}
13681 */
13682 OO.ui.CapsuleMultiSelectWidget.prototype.addItemsFromData = function ( datas ) {
13683 var widget = this,
13684 menu = this.menu,
13685 items = [];
13686
13687 $.each( datas, function ( i, data ) {
13688 var item;
13689
13690 if ( !widget.getItemFromData( data ) ) {
13691 item = menu.getItemFromData( data );
13692 if ( item ) {
13693 items.push( widget.createItemWidget( data, item.label ) );
13694 } else if ( widget.allowArbitrary ) {
13695 items.push( widget.createItemWidget( data, String( data ) ) );
13696 }
13697 }
13698 } );
13699
13700 if ( items.length ) {
13701 this.addItems( items );
13702 }
13703
13704 return this;
13705 };
13706
13707 /**
13708 * Remove items by data
13709 * @chainable
13710 * @param {Mixed[]} datas
13711 * @return {OO.ui.CapsuleMultiSelectWidget}
13712 */
13713 OO.ui.CapsuleMultiSelectWidget.prototype.removeItemsFromData = function ( datas ) {
13714 var widget = this,
13715 items = [];
13716
13717 $.each( datas, function ( i, data ) {
13718 var item = widget.getItemFromData( data );
13719 if ( item ) {
13720 items.push( item );
13721 }
13722 } );
13723
13724 if ( items.length ) {
13725 this.removeItems( items );
13726 }
13727
13728 return this;
13729 };
13730
13731 /**
13732 * @inheritdoc
13733 */
13734 OO.ui.CapsuleMultiSelectWidget.prototype.addItems = function ( items ) {
13735 var same, i, l,
13736 oldItems = this.items.slice();
13737
13738 OO.ui.mixin.GroupElement.prototype.addItems.call( this, items );
13739
13740 if ( this.items.length !== oldItems.length ) {
13741 same = false;
13742 } else {
13743 same = true;
13744 for ( i = 0, l = oldItems.length; same && i < l; i++ ) {
13745 same = same && this.items[ i ] === oldItems[ i ];
13746 }
13747 }
13748 if ( !same ) {
13749 this.emit( 'change', this.getItemsData() );
13750 this.menu.position();
13751 }
13752
13753 return this;
13754 };
13755
13756 /**
13757 * @inheritdoc
13758 */
13759 OO.ui.CapsuleMultiSelectWidget.prototype.removeItems = function ( items ) {
13760 var same, i, l,
13761 oldItems = this.items.slice();
13762
13763 OO.ui.mixin.GroupElement.prototype.removeItems.call( this, items );
13764
13765 if ( this.items.length !== oldItems.length ) {
13766 same = false;
13767 } else {
13768 same = true;
13769 for ( i = 0, l = oldItems.length; same && i < l; i++ ) {
13770 same = same && this.items[ i ] === oldItems[ i ];
13771 }
13772 }
13773 if ( !same ) {
13774 this.emit( 'change', this.getItemsData() );
13775 this.menu.position();
13776 }
13777
13778 return this;
13779 };
13780
13781 /**
13782 * @inheritdoc
13783 */
13784 OO.ui.CapsuleMultiSelectWidget.prototype.clearItems = function () {
13785 if ( this.items.length ) {
13786 OO.ui.mixin.GroupElement.prototype.clearItems.call( this );
13787 this.emit( 'change', this.getItemsData() );
13788 this.menu.position();
13789 }
13790 return this;
13791 };
13792
13793 /**
13794 * Get the capsule widget's menu.
13795 * @return {OO.ui.MenuSelectWidget} Menu widget
13796 */
13797 OO.ui.CapsuleMultiSelectWidget.prototype.getMenu = function () {
13798 return this.menu;
13799 };
13800
13801 /**
13802 * Handle focus events
13803 *
13804 * @private
13805 * @param {jQuery.Event} event
13806 */
13807 OO.ui.CapsuleMultiSelectWidget.prototype.onInputFocus = function () {
13808 if ( !this.isDisabled() ) {
13809 this.menu.toggle( true );
13810 }
13811 };
13812
13813 /**
13814 * Handle blur events
13815 *
13816 * @private
13817 * @param {jQuery.Event} event
13818 */
13819 OO.ui.CapsuleMultiSelectWidget.prototype.onInputBlur = function () {
13820 if ( this.allowArbitrary && this.$input.val().trim() !== '' ) {
13821 this.addItemsFromData( [ this.$input.val() ] );
13822 }
13823 this.clearInput();
13824 };
13825
13826 /**
13827 * Handle focus events
13828 *
13829 * @private
13830 * @param {jQuery.Event} event
13831 */
13832 OO.ui.CapsuleMultiSelectWidget.prototype.onFocusForPopup = function () {
13833 if ( !this.isDisabled() ) {
13834 this.popup.setSize( this.$handle.width() );
13835 this.popup.toggle( true );
13836 this.popup.$element.find( '*' )
13837 .filter( function () { return OO.ui.isFocusableElement( $( this ), true ); } )
13838 .first()
13839 .focus();
13840 }
13841 };
13842
13843 /**
13844 * Handles popup focus out events.
13845 *
13846 * @private
13847 * @param {Event} e Focus out event
13848 */
13849 OO.ui.CapsuleMultiSelectWidget.prototype.onPopupFocusOut = function () {
13850 var widget = this.popup;
13851
13852 setTimeout( function () {
13853 if (
13854 widget.isVisible() &&
13855 !OO.ui.contains( widget.$element[ 0 ], document.activeElement, true ) &&
13856 ( !widget.$autoCloseIgnore || !widget.$autoCloseIgnore.has( document.activeElement ).length )
13857 ) {
13858 widget.toggle( false );
13859 }
13860 } );
13861 };
13862
13863 /**
13864 * Handle mouse down events.
13865 *
13866 * @private
13867 * @param {jQuery.Event} e Mouse down event
13868 */
13869 OO.ui.CapsuleMultiSelectWidget.prototype.onMouseDown = function ( e ) {
13870 if ( e.which === OO.ui.MouseButtons.LEFT ) {
13871 this.focus();
13872 return false;
13873 } else {
13874 this.updateInputSize();
13875 }
13876 };
13877
13878 /**
13879 * Handle key press events.
13880 *
13881 * @private
13882 * @param {jQuery.Event} e Key press event
13883 */
13884 OO.ui.CapsuleMultiSelectWidget.prototype.onKeyPress = function ( e ) {
13885 var item;
13886
13887 if ( !this.isDisabled() ) {
13888 if ( e.which === OO.ui.Keys.ESCAPE ) {
13889 this.clearInput();
13890 return false;
13891 }
13892
13893 if ( !this.popup ) {
13894 this.menu.toggle( true );
13895 if ( e.which === OO.ui.Keys.ENTER ) {
13896 item = this.menu.getItemFromLabel( this.$input.val(), true );
13897 if ( item ) {
13898 this.addItemsFromData( [ item.data ] );
13899 this.clearInput();
13900 } else if ( this.allowArbitrary && this.$input.val().trim() !== '' ) {
13901 this.addItemsFromData( [ this.$input.val() ] );
13902 this.clearInput();
13903 }
13904 return false;
13905 }
13906
13907 // Make sure the input gets resized.
13908 setTimeout( this.updateInputSize.bind( this ), 0 );
13909 }
13910 }
13911 };
13912
13913 /**
13914 * Handle key down events.
13915 *
13916 * @private
13917 * @param {jQuery.Event} e Key down event
13918 */
13919 OO.ui.CapsuleMultiSelectWidget.prototype.onKeyDown = function ( e ) {
13920 if ( !this.isDisabled() ) {
13921 // 'keypress' event is not triggered for Backspace
13922 if ( e.keyCode === OO.ui.Keys.BACKSPACE && this.$input.val() === '' ) {
13923 if ( this.items.length ) {
13924 this.removeItems( this.items.slice( -1 ) );
13925 }
13926 return false;
13927 }
13928 }
13929 };
13930
13931 /**
13932 * Update the dimensions of the text input field to encompass all available area.
13933 *
13934 * @private
13935 * @param {jQuery.Event} e Event of some sort
13936 */
13937 OO.ui.CapsuleMultiSelectWidget.prototype.updateInputSize = function () {
13938 var $lastItem, direction, contentWidth, currentWidth, bestWidth;
13939 if ( !this.isDisabled() ) {
13940 this.$input.css( 'width', '1em' );
13941 $lastItem = this.$group.children().last();
13942 direction = OO.ui.Element.static.getDir( this.$handle );
13943 contentWidth = this.$input[ 0 ].scrollWidth;
13944 currentWidth = this.$input.width();
13945
13946 if ( contentWidth < currentWidth ) {
13947 // All is fine, don't perform expensive calculations
13948 return;
13949 }
13950
13951 if ( !$lastItem.length ) {
13952 bestWidth = this.$content.innerWidth();
13953 } else {
13954 bestWidth = direction === 'ltr' ?
13955 this.$content.innerWidth() - $lastItem.position().left - $lastItem.outerWidth() :
13956 $lastItem.position().left;
13957 }
13958 // Some safety margin for sanity, because I *really* don't feel like finding out where the few
13959 // pixels this is off by are coming from.
13960 bestWidth -= 10;
13961 if ( contentWidth > bestWidth ) {
13962 // This will result in the input getting shifted to the next line
13963 bestWidth = this.$content.innerWidth() - 10;
13964 }
13965 this.$input.width( Math.floor( bestWidth ) );
13966
13967 this.menu.position();
13968 }
13969 };
13970
13971 /**
13972 * Handle menu choose events.
13973 *
13974 * @private
13975 * @param {OO.ui.OptionWidget} item Chosen item
13976 */
13977 OO.ui.CapsuleMultiSelectWidget.prototype.onMenuChoose = function ( item ) {
13978 if ( item && item.isVisible() ) {
13979 this.addItemsFromData( [ item.getData() ] );
13980 this.clearInput();
13981 }
13982 };
13983
13984 /**
13985 * Handle menu item change events.
13986 *
13987 * @private
13988 */
13989 OO.ui.CapsuleMultiSelectWidget.prototype.onMenuItemsChange = function () {
13990 this.setItemsFromData( this.getItemsData() );
13991 this.$element.toggleClass( 'oo-ui-capsuleMultiSelectWidget-empty', this.menu.isEmpty() );
13992 };
13993
13994 /**
13995 * Clear the input field
13996 * @private
13997 */
13998 OO.ui.CapsuleMultiSelectWidget.prototype.clearInput = function () {
13999 if ( this.$input ) {
14000 this.$input.val( '' );
14001 this.updateInputSize();
14002 }
14003 if ( this.popup ) {
14004 this.popup.toggle( false );
14005 }
14006 this.menu.toggle( false );
14007 this.menu.selectItem();
14008 this.menu.highlightItem();
14009 };
14010
14011 /**
14012 * @inheritdoc
14013 */
14014 OO.ui.CapsuleMultiSelectWidget.prototype.setDisabled = function ( disabled ) {
14015 var i, len;
14016
14017 // Parent method
14018 OO.ui.CapsuleMultiSelectWidget.parent.prototype.setDisabled.call( this, disabled );
14019
14020 if ( this.$input ) {
14021 this.$input.prop( 'disabled', this.isDisabled() );
14022 }
14023 if ( this.menu ) {
14024 this.menu.setDisabled( this.isDisabled() );
14025 }
14026 if ( this.popup ) {
14027 this.popup.setDisabled( this.isDisabled() );
14028 }
14029
14030 if ( this.items ) {
14031 for ( i = 0, len = this.items.length; i < len; i++ ) {
14032 this.items[ i ].updateDisabled();
14033 }
14034 }
14035
14036 return this;
14037 };
14038
14039 /**
14040 * Focus the widget
14041 * @chainable
14042 * @return {OO.ui.CapsuleMultiSelectWidget}
14043 */
14044 OO.ui.CapsuleMultiSelectWidget.prototype.focus = function () {
14045 if ( !this.isDisabled() ) {
14046 if ( this.popup ) {
14047 this.popup.setSize( this.$handle.width() );
14048 this.popup.toggle( true );
14049 this.popup.$element.find( '*' )
14050 .filter( function () { return OO.ui.isFocusableElement( $( this ), true ); } )
14051 .first()
14052 .focus();
14053 } else {
14054 this.updateInputSize();
14055 this.menu.toggle( true );
14056 this.$input.focus();
14057 }
14058 }
14059 return this;
14060 };
14061
14062 /**
14063 * CapsuleItemWidgets are used within a {@link OO.ui.CapsuleMultiSelectWidget
14064 * CapsuleMultiSelectWidget} to display the selected items.
14065 *
14066 * @class
14067 * @extends OO.ui.Widget
14068 * @mixins OO.ui.mixin.ItemWidget
14069 * @mixins OO.ui.mixin.IndicatorElement
14070 * @mixins OO.ui.mixin.LabelElement
14071 * @mixins OO.ui.mixin.FlaggedElement
14072 * @mixins OO.ui.mixin.TabIndexedElement
14073 *
14074 * @constructor
14075 * @param {Object} [config] Configuration options
14076 */
14077 OO.ui.CapsuleItemWidget = function OoUiCapsuleItemWidget( config ) {
14078 // Configuration initialization
14079 config = config || {};
14080
14081 // Parent constructor
14082 OO.ui.CapsuleItemWidget.parent.call( this, config );
14083
14084 // Properties (must be set before mixin constructor calls)
14085 this.$indicator = $( '<span>' );
14086
14087 // Mixin constructors
14088 OO.ui.mixin.ItemWidget.call( this );
14089 OO.ui.mixin.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$indicator, indicator: 'clear' } ) );
14090 OO.ui.mixin.LabelElement.call( this, config );
14091 OO.ui.mixin.FlaggedElement.call( this, config );
14092 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$indicator } ) );
14093
14094 // Events
14095 this.$indicator.on( {
14096 keydown: this.onCloseKeyDown.bind( this ),
14097 click: this.onCloseClick.bind( this )
14098 } );
14099
14100 // Initialization
14101 this.$element
14102 .addClass( 'oo-ui-capsuleItemWidget' )
14103 .append( this.$indicator, this.$label );
14104 };
14105
14106 /* Setup */
14107
14108 OO.inheritClass( OO.ui.CapsuleItemWidget, OO.ui.Widget );
14109 OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.ItemWidget );
14110 OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.IndicatorElement );
14111 OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.LabelElement );
14112 OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.FlaggedElement );
14113 OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.TabIndexedElement );
14114
14115 /* Methods */
14116
14117 /**
14118 * Handle close icon clicks
14119 * @param {jQuery.Event} event
14120 */
14121 OO.ui.CapsuleItemWidget.prototype.onCloseClick = function () {
14122 var element = this.getElementGroup();
14123
14124 if ( !this.isDisabled() && element && $.isFunction( element.removeItems ) ) {
14125 element.removeItems( [ this ] );
14126 element.focus();
14127 }
14128 };
14129
14130 /**
14131 * Handle close keyboard events
14132 * @param {jQuery.Event} event Key down event
14133 */
14134 OO.ui.CapsuleItemWidget.prototype.onCloseKeyDown = function ( e ) {
14135 if ( !this.isDisabled() && $.isFunction( this.getElementGroup().removeItems ) ) {
14136 switch ( e.which ) {
14137 case OO.ui.Keys.ENTER:
14138 case OO.ui.Keys.BACKSPACE:
14139 case OO.ui.Keys.SPACE:
14140 this.getElementGroup().removeItems( [ this ] );
14141 return false;
14142 }
14143 }
14144 };
14145
14146 /**
14147 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
14148 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
14149 * users can interact with it.
14150 *
14151 * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
14152 * OO.ui.DropdownInputWidget instead.
14153 *
14154 * @example
14155 * // Example: A DropdownWidget with a menu that contains three options
14156 * var dropDown = new OO.ui.DropdownWidget( {
14157 * label: 'Dropdown menu: Select a menu option',
14158 * menu: {
14159 * items: [
14160 * new OO.ui.MenuOptionWidget( {
14161 * data: 'a',
14162 * label: 'First'
14163 * } ),
14164 * new OO.ui.MenuOptionWidget( {
14165 * data: 'b',
14166 * label: 'Second'
14167 * } ),
14168 * new OO.ui.MenuOptionWidget( {
14169 * data: 'c',
14170 * label: 'Third'
14171 * } )
14172 * ]
14173 * }
14174 * } );
14175 *
14176 * $( 'body' ).append( dropDown.$element );
14177 *
14178 * dropDown.getMenu().selectItemByData( 'b' );
14179 *
14180 * dropDown.getMenu().getSelectedItem().getData(); // returns 'b'
14181 *
14182 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
14183 *
14184 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
14185 *
14186 * @class
14187 * @extends OO.ui.Widget
14188 * @mixins OO.ui.mixin.IconElement
14189 * @mixins OO.ui.mixin.IndicatorElement
14190 * @mixins OO.ui.mixin.LabelElement
14191 * @mixins OO.ui.mixin.TitledElement
14192 * @mixins OO.ui.mixin.TabIndexedElement
14193 *
14194 * @constructor
14195 * @param {Object} [config] Configuration options
14196 * @cfg {Object} [menu] Configuration options to pass to {@link OO.ui.FloatingMenuSelectWidget menu select widget}
14197 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
14198 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
14199 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
14200 */
14201 OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
14202 // Configuration initialization
14203 config = $.extend( { indicator: 'down' }, config );
14204
14205 // Parent constructor
14206 OO.ui.DropdownWidget.parent.call( this, config );
14207
14208 // Properties (must be set before TabIndexedElement constructor call)
14209 this.$handle = this.$( '<span>' );
14210 this.$overlay = config.$overlay || this.$element;
14211
14212 // Mixin constructors
14213 OO.ui.mixin.IconElement.call( this, config );
14214 OO.ui.mixin.IndicatorElement.call( this, config );
14215 OO.ui.mixin.LabelElement.call( this, config );
14216 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
14217 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
14218
14219 // Properties
14220 this.menu = new OO.ui.FloatingMenuSelectWidget( $.extend( {
14221 widget: this,
14222 $container: this.$element
14223 }, config.menu ) );
14224
14225 // Events
14226 this.$handle.on( {
14227 click: this.onClick.bind( this ),
14228 keypress: this.onKeyPress.bind( this )
14229 } );
14230 this.menu.connect( this, { select: 'onMenuSelect' } );
14231
14232 // Initialization
14233 this.$handle
14234 .addClass( 'oo-ui-dropdownWidget-handle' )
14235 .append( this.$icon, this.$label, this.$indicator );
14236 this.$element
14237 .addClass( 'oo-ui-dropdownWidget' )
14238 .append( this.$handle );
14239 this.$overlay.append( this.menu.$element );
14240 };
14241
14242 /* Setup */
14243
14244 OO.inheritClass( OO.ui.DropdownWidget, OO.ui.Widget );
14245 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.IconElement );
14246 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.IndicatorElement );
14247 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.LabelElement );
14248 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.TitledElement );
14249 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.TabIndexedElement );
14250
14251 /* Methods */
14252
14253 /**
14254 * Get the menu.
14255 *
14256 * @return {OO.ui.MenuSelectWidget} Menu of widget
14257 */
14258 OO.ui.DropdownWidget.prototype.getMenu = function () {
14259 return this.menu;
14260 };
14261
14262 /**
14263 * Handles menu select events.
14264 *
14265 * @private
14266 * @param {OO.ui.MenuOptionWidget} item Selected menu item
14267 */
14268 OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
14269 var selectedLabel;
14270
14271 if ( !item ) {
14272 this.setLabel( null );
14273 return;
14274 }
14275
14276 selectedLabel = item.getLabel();
14277
14278 // If the label is a DOM element, clone it, because setLabel will append() it
14279 if ( selectedLabel instanceof jQuery ) {
14280 selectedLabel = selectedLabel.clone();
14281 }
14282
14283 this.setLabel( selectedLabel );
14284 };
14285
14286 /**
14287 * Handle mouse click events.
14288 *
14289 * @private
14290 * @param {jQuery.Event} e Mouse click event
14291 */
14292 OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
14293 if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
14294 this.menu.toggle();
14295 }
14296 return false;
14297 };
14298
14299 /**
14300 * Handle key press events.
14301 *
14302 * @private
14303 * @param {jQuery.Event} e Key press event
14304 */
14305 OO.ui.DropdownWidget.prototype.onKeyPress = function ( e ) {
14306 if ( !this.isDisabled() &&
14307 ( ( e.which === OO.ui.Keys.SPACE && !this.menu.isVisible() ) || e.which === OO.ui.Keys.ENTER )
14308 ) {
14309 this.menu.toggle();
14310 return false;
14311 }
14312 };
14313
14314 /**
14315 * SelectFileWidgets allow for selecting files, using the HTML5 File API. These
14316 * widgets can be configured with {@link OO.ui.mixin.IconElement icons} and {@link
14317 * OO.ui.mixin.IndicatorElement indicators}.
14318 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
14319 *
14320 * @example
14321 * // Example of a file select widget
14322 * var selectFile = new OO.ui.SelectFileWidget();
14323 * $( 'body' ).append( selectFile.$element );
14324 *
14325 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets
14326 *
14327 * @class
14328 * @extends OO.ui.Widget
14329 * @mixins OO.ui.mixin.IconElement
14330 * @mixins OO.ui.mixin.IndicatorElement
14331 * @mixins OO.ui.mixin.PendingElement
14332 * @mixins OO.ui.mixin.LabelElement
14333 *
14334 * @constructor
14335 * @param {Object} [config] Configuration options
14336 * @cfg {string[]|null} [accept=null] MIME types to accept. null accepts all types.
14337 * @cfg {string} [placeholder] Text to display when no file is selected.
14338 * @cfg {string} [notsupported] Text to display when file support is missing in the browser.
14339 * @cfg {boolean} [droppable=true] Whether to accept files by drag and drop.
14340 * @cfg {boolean} [showDropTarget=false] Whether to show a drop target. Requires droppable to be true.
14341 * @cfg {boolean} [dragDropUI=false] Deprecated alias for showDropTarget
14342 */
14343 OO.ui.SelectFileWidget = function OoUiSelectFileWidget( config ) {
14344 var dragHandler;
14345
14346 // TODO: Remove in next release
14347 if ( config && config.dragDropUI ) {
14348 config.showDropTarget = true;
14349 }
14350
14351 // Configuration initialization
14352 config = $.extend( {
14353 accept: null,
14354 placeholder: OO.ui.msg( 'ooui-selectfile-placeholder' ),
14355 notsupported: OO.ui.msg( 'ooui-selectfile-not-supported' ),
14356 droppable: true,
14357 showDropTarget: false
14358 }, config );
14359
14360 // Parent constructor
14361 OO.ui.SelectFileWidget.parent.call( this, config );
14362
14363 // Mixin constructors
14364 OO.ui.mixin.IconElement.call( this, config );
14365 OO.ui.mixin.IndicatorElement.call( this, config );
14366 OO.ui.mixin.PendingElement.call( this, $.extend( {}, config, { $pending: this.$info } ) );
14367 OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, { autoFitLabel: true } ) );
14368
14369 // Properties
14370 this.$info = $( '<span>' );
14371
14372 // Properties
14373 this.showDropTarget = config.showDropTarget;
14374 this.isSupported = this.constructor.static.isSupported();
14375 this.currentFile = null;
14376 if ( Array.isArray( config.accept ) ) {
14377 this.accept = config.accept;
14378 } else {
14379 this.accept = null;
14380 }
14381 this.placeholder = config.placeholder;
14382 this.notsupported = config.notsupported;
14383 this.onFileSelectedHandler = this.onFileSelected.bind( this );
14384
14385 this.selectButton = new OO.ui.ButtonWidget( {
14386 classes: [ 'oo-ui-selectFileWidget-selectButton' ],
14387 label: OO.ui.msg( 'ooui-selectfile-button-select' ),
14388 disabled: this.disabled || !this.isSupported
14389 } );
14390
14391 this.clearButton = new OO.ui.ButtonWidget( {
14392 classes: [ 'oo-ui-selectFileWidget-clearButton' ],
14393 framed: false,
14394 icon: 'remove',
14395 disabled: this.disabled
14396 } );
14397
14398 // Events
14399 this.selectButton.$button.on( {
14400 keypress: this.onKeyPress.bind( this )
14401 } );
14402 this.clearButton.connect( this, {
14403 click: 'onClearClick'
14404 } );
14405 if ( config.droppable ) {
14406 dragHandler = this.onDragEnterOrOver.bind( this );
14407 this.$element.on( {
14408 dragenter: dragHandler,
14409 dragover: dragHandler,
14410 dragleave: this.onDragLeave.bind( this ),
14411 drop: this.onDrop.bind( this )
14412 } );
14413 }
14414
14415 // Initialization
14416 this.addInput();
14417 this.updateUI();
14418 this.$label.addClass( 'oo-ui-selectFileWidget-label' );
14419 this.$info
14420 .addClass( 'oo-ui-selectFileWidget-info' )
14421 .append( this.$icon, this.$label, this.clearButton.$element, this.$indicator );
14422 this.$element
14423 .addClass( 'oo-ui-selectFileWidget' )
14424 .append( this.$info, this.selectButton.$element );
14425 if ( config.droppable && config.showDropTarget ) {
14426 this.$dropTarget = $( '<div>' )
14427 .addClass( 'oo-ui-selectFileWidget-dropTarget' )
14428 .text( OO.ui.msg( 'ooui-selectfile-dragdrop-placeholder' ) )
14429 .on( {
14430 click: this.onDropTargetClick.bind( this )
14431 } );
14432 this.$element.prepend( this.$dropTarget );
14433 }
14434 };
14435
14436 /* Setup */
14437
14438 OO.inheritClass( OO.ui.SelectFileWidget, OO.ui.Widget );
14439 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.IconElement );
14440 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.IndicatorElement );
14441 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.PendingElement );
14442 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.LabelElement );
14443
14444 /* Static Properties */
14445
14446 /**
14447 * Check if this widget is supported
14448 *
14449 * @static
14450 * @return {boolean}
14451 */
14452 OO.ui.SelectFileWidget.static.isSupported = function () {
14453 var $input;
14454 if ( OO.ui.SelectFileWidget.static.isSupportedCache === null ) {
14455 $input = $( '<input type="file">' );
14456 OO.ui.SelectFileWidget.static.isSupportedCache = $input[ 0 ].files !== undefined;
14457 }
14458 return OO.ui.SelectFileWidget.static.isSupportedCache;
14459 };
14460
14461 OO.ui.SelectFileWidget.static.isSupportedCache = null;
14462
14463 /* Events */
14464
14465 /**
14466 * @event change
14467 *
14468 * A change event is emitted when the on/off state of the toggle changes.
14469 *
14470 * @param {File|null} value New value
14471 */
14472
14473 /* Methods */
14474
14475 /**
14476 * Get the current value of the field
14477 *
14478 * @return {File|null}
14479 */
14480 OO.ui.SelectFileWidget.prototype.getValue = function () {
14481 return this.currentFile;
14482 };
14483
14484 /**
14485 * Set the current value of the field
14486 *
14487 * @param {File|null} file File to select
14488 */
14489 OO.ui.SelectFileWidget.prototype.setValue = function ( file ) {
14490 if ( this.currentFile !== file ) {
14491 this.currentFile = file;
14492 this.updateUI();
14493 this.emit( 'change', this.currentFile );
14494 }
14495 };
14496
14497 /**
14498 * Focus the widget.
14499 *
14500 * Focusses the select file button.
14501 *
14502 * @chainable
14503 */
14504 OO.ui.SelectFileWidget.prototype.focus = function () {
14505 this.selectButton.$button[ 0 ].focus();
14506 return this;
14507 };
14508
14509 /**
14510 * Update the user interface when a file is selected or unselected
14511 *
14512 * @protected
14513 */
14514 OO.ui.SelectFileWidget.prototype.updateUI = function () {
14515 var $label;
14516 if ( !this.isSupported ) {
14517 this.$element.addClass( 'oo-ui-selectFileWidget-notsupported' );
14518 this.$element.removeClass( 'oo-ui-selectFileWidget-empty' );
14519 this.setLabel( this.notsupported );
14520 } else {
14521 this.$element.addClass( 'oo-ui-selectFileWidget-supported' );
14522 if ( this.currentFile ) {
14523 this.$element.removeClass( 'oo-ui-selectFileWidget-empty' );
14524 $label = $( [] );
14525 $label = $label.add(
14526 $( '<span>' )
14527 .addClass( 'oo-ui-selectFileWidget-fileName' )
14528 .text( this.currentFile.name )
14529 );
14530 if ( this.currentFile.type !== '' ) {
14531 $label = $label.add(
14532 $( '<span>' )
14533 .addClass( 'oo-ui-selectFileWidget-fileType' )
14534 .text( this.currentFile.type )
14535 );
14536 }
14537 this.setLabel( $label );
14538 } else {
14539 this.$element.addClass( 'oo-ui-selectFileWidget-empty' );
14540 this.setLabel( this.placeholder );
14541 }
14542 }
14543 };
14544
14545 /**
14546 * Add the input to the widget
14547 *
14548 * @private
14549 */
14550 OO.ui.SelectFileWidget.prototype.addInput = function () {
14551 if ( this.$input ) {
14552 this.$input.remove();
14553 }
14554
14555 if ( !this.isSupported ) {
14556 this.$input = null;
14557 return;
14558 }
14559
14560 this.$input = $( '<input type="file">' );
14561 this.$input.on( 'change', this.onFileSelectedHandler );
14562 this.$input.attr( {
14563 tabindex: -1
14564 } );
14565 if ( this.accept ) {
14566 this.$input.attr( 'accept', this.accept.join( ', ' ) );
14567 }
14568 this.selectButton.$button.append( this.$input );
14569 };
14570
14571 /**
14572 * Determine if we should accept this file
14573 *
14574 * @private
14575 * @param {string} File MIME type
14576 * @return {boolean}
14577 */
14578 OO.ui.SelectFileWidget.prototype.isAllowedType = function ( mimeType ) {
14579 var i, mimeTest;
14580
14581 if ( !this.accept || !mimeType ) {
14582 return true;
14583 }
14584
14585 for ( i = 0; i < this.accept.length; i++ ) {
14586 mimeTest = this.accept[ i ];
14587 if ( mimeTest === mimeType ) {
14588 return true;
14589 } else if ( mimeTest.substr( -2 ) === '/*' ) {
14590 mimeTest = mimeTest.substr( 0, mimeTest.length - 1 );
14591 if ( mimeType.substr( 0, mimeTest.length ) === mimeTest ) {
14592 return true;
14593 }
14594 }
14595 }
14596
14597 return false;
14598 };
14599
14600 /**
14601 * Handle file selection from the input
14602 *
14603 * @private
14604 * @param {jQuery.Event} e
14605 */
14606 OO.ui.SelectFileWidget.prototype.onFileSelected = function ( e ) {
14607 var file = OO.getProp( e.target, 'files', 0 ) || null;
14608
14609 if ( file && !this.isAllowedType( file.type ) ) {
14610 file = null;
14611 }
14612
14613 this.setValue( file );
14614 this.addInput();
14615 };
14616
14617 /**
14618 * Handle clear button click events.
14619 *
14620 * @private
14621 */
14622 OO.ui.SelectFileWidget.prototype.onClearClick = function () {
14623 this.setValue( null );
14624 return false;
14625 };
14626
14627 /**
14628 * Handle key press events.
14629 *
14630 * @private
14631 * @param {jQuery.Event} e Key press event
14632 */
14633 OO.ui.SelectFileWidget.prototype.onKeyPress = function ( e ) {
14634 if ( this.isSupported && !this.isDisabled() && this.$input &&
14635 ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
14636 ) {
14637 this.$input.click();
14638 return false;
14639 }
14640 };
14641
14642 /**
14643 * Handle drop target click events.
14644 *
14645 * @private
14646 * @param {jQuery.Event} e Key press event
14647 */
14648 OO.ui.SelectFileWidget.prototype.onDropTargetClick = function () {
14649 if ( this.isSupported && !this.isDisabled() && this.$input ) {
14650 this.$input.click();
14651 return false;
14652 }
14653 };
14654
14655 /**
14656 * Handle drag enter and over events
14657 *
14658 * @private
14659 * @param {jQuery.Event} e Drag event
14660 */
14661 OO.ui.SelectFileWidget.prototype.onDragEnterOrOver = function ( e ) {
14662 var itemOrFile,
14663 droppableFile = false,
14664 dt = e.originalEvent.dataTransfer;
14665
14666 e.preventDefault();
14667 e.stopPropagation();
14668
14669 if ( this.isDisabled() || !this.isSupported ) {
14670 this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
14671 dt.dropEffect = 'none';
14672 return false;
14673 }
14674
14675 // DataTransferItem and File both have a type property, but in Chrome files
14676 // have no information at this point.
14677 itemOrFile = OO.getProp( dt, 'items', 0 ) || OO.getProp( dt, 'files', 0 );
14678 if ( itemOrFile ) {
14679 if ( this.isAllowedType( itemOrFile.type ) ) {
14680 droppableFile = true;
14681 }
14682 // dt.types is Array-like, but not an Array
14683 } else if ( Array.prototype.indexOf.call( OO.getProp( dt, 'types' ) || [], 'Files' ) !== -1 ) {
14684 // File information is not available at this point for security so just assume
14685 // it is acceptable for now.
14686 // https://bugzilla.mozilla.org/show_bug.cgi?id=640534
14687 droppableFile = true;
14688 }
14689
14690 this.$element.toggleClass( 'oo-ui-selectFileWidget-canDrop', droppableFile );
14691 if ( !droppableFile ) {
14692 dt.dropEffect = 'none';
14693 }
14694
14695 return false;
14696 };
14697
14698 /**
14699 * Handle drag leave events
14700 *
14701 * @private
14702 * @param {jQuery.Event} e Drag event
14703 */
14704 OO.ui.SelectFileWidget.prototype.onDragLeave = function () {
14705 this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
14706 };
14707
14708 /**
14709 * Handle drop events
14710 *
14711 * @private
14712 * @param {jQuery.Event} e Drop event
14713 */
14714 OO.ui.SelectFileWidget.prototype.onDrop = function ( e ) {
14715 var file = null,
14716 dt = e.originalEvent.dataTransfer;
14717
14718 e.preventDefault();
14719 e.stopPropagation();
14720 this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
14721
14722 if ( this.isDisabled() || !this.isSupported ) {
14723 return false;
14724 }
14725
14726 file = OO.getProp( dt, 'files', 0 );
14727 if ( file && !this.isAllowedType( file.type ) ) {
14728 file = null;
14729 }
14730 if ( file ) {
14731 this.setValue( file );
14732 }
14733
14734 return false;
14735 };
14736
14737 /**
14738 * @inheritdoc
14739 */
14740 OO.ui.SelectFileWidget.prototype.setDisabled = function ( disabled ) {
14741 OO.ui.SelectFileWidget.parent.prototype.setDisabled.call( this, disabled );
14742 if ( this.selectButton ) {
14743 this.selectButton.setDisabled( disabled );
14744 }
14745 if ( this.clearButton ) {
14746 this.clearButton.setDisabled( disabled );
14747 }
14748 return this;
14749 };
14750
14751 /**
14752 * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
14753 * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
14754 * for a list of icons included in the library.
14755 *
14756 * @example
14757 * // An icon widget with a label
14758 * var myIcon = new OO.ui.IconWidget( {
14759 * icon: 'help',
14760 * iconTitle: 'Help'
14761 * } );
14762 * // Create a label.
14763 * var iconLabel = new OO.ui.LabelWidget( {
14764 * label: 'Help'
14765 * } );
14766 * $( 'body' ).append( myIcon.$element, iconLabel.$element );
14767 *
14768 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
14769 *
14770 * @class
14771 * @extends OO.ui.Widget
14772 * @mixins OO.ui.mixin.IconElement
14773 * @mixins OO.ui.mixin.TitledElement
14774 * @mixins OO.ui.mixin.FlaggedElement
14775 *
14776 * @constructor
14777 * @param {Object} [config] Configuration options
14778 */
14779 OO.ui.IconWidget = function OoUiIconWidget( config ) {
14780 // Configuration initialization
14781 config = config || {};
14782
14783 // Parent constructor
14784 OO.ui.IconWidget.parent.call( this, config );
14785
14786 // Mixin constructors
14787 OO.ui.mixin.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) );
14788 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
14789 OO.ui.mixin.FlaggedElement.call( this, $.extend( {}, config, { $flagged: this.$element } ) );
14790
14791 // Initialization
14792 this.$element.addClass( 'oo-ui-iconWidget' );
14793 };
14794
14795 /* Setup */
14796
14797 OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
14798 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.IconElement );
14799 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.TitledElement );
14800 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.FlaggedElement );
14801
14802 /* Static Properties */
14803
14804 OO.ui.IconWidget.static.tagName = 'span';
14805
14806 /**
14807 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
14808 * attention to the status of an item or to clarify the function of a control. For a list of
14809 * indicators included in the library, please see the [OOjs UI documentation on MediaWiki][1].
14810 *
14811 * @example
14812 * // Example of an indicator widget
14813 * var indicator1 = new OO.ui.IndicatorWidget( {
14814 * indicator: 'alert'
14815 * } );
14816 *
14817 * // Create a fieldset layout to add a label
14818 * var fieldset = new OO.ui.FieldsetLayout();
14819 * fieldset.addItems( [
14820 * new OO.ui.FieldLayout( indicator1, { label: 'An alert indicator:' } )
14821 * ] );
14822 * $( 'body' ).append( fieldset.$element );
14823 *
14824 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
14825 *
14826 * @class
14827 * @extends OO.ui.Widget
14828 * @mixins OO.ui.mixin.IndicatorElement
14829 * @mixins OO.ui.mixin.TitledElement
14830 *
14831 * @constructor
14832 * @param {Object} [config] Configuration options
14833 */
14834 OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
14835 // Configuration initialization
14836 config = config || {};
14837
14838 // Parent constructor
14839 OO.ui.IndicatorWidget.parent.call( this, config );
14840
14841 // Mixin constructors
14842 OO.ui.mixin.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) );
14843 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
14844
14845 // Initialization
14846 this.$element.addClass( 'oo-ui-indicatorWidget' );
14847 };
14848
14849 /* Setup */
14850
14851 OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
14852 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.mixin.IndicatorElement );
14853 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.mixin.TitledElement );
14854
14855 /* Static Properties */
14856
14857 OO.ui.IndicatorWidget.static.tagName = 'span';
14858
14859 /**
14860 * InputWidget is the base class for all input widgets, which
14861 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
14862 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
14863 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
14864 *
14865 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
14866 *
14867 * @abstract
14868 * @class
14869 * @extends OO.ui.Widget
14870 * @mixins OO.ui.mixin.FlaggedElement
14871 * @mixins OO.ui.mixin.TabIndexedElement
14872 * @mixins OO.ui.mixin.TitledElement
14873 * @mixins OO.ui.mixin.AccessKeyedElement
14874 *
14875 * @constructor
14876 * @param {Object} [config] Configuration options
14877 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
14878 * @cfg {string} [value=''] The value of the input.
14879 * @cfg {string} [dir] The directionality of the input (ltr/rtl).
14880 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
14881 * before it is accepted.
14882 */
14883 OO.ui.InputWidget = function OoUiInputWidget( config ) {
14884 // Configuration initialization
14885 config = config || {};
14886
14887 // Parent constructor
14888 OO.ui.InputWidget.parent.call( this, config );
14889
14890 // Properties
14891 this.$input = this.getInputElement( config );
14892 this.value = '';
14893 this.inputFilter = config.inputFilter;
14894
14895 // Mixin constructors
14896 OO.ui.mixin.FlaggedElement.call( this, config );
14897 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$input } ) );
14898 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
14899 OO.ui.mixin.AccessKeyedElement.call( this, $.extend( {}, config, { $accessKeyed: this.$input } ) );
14900
14901 // Events
14902 this.$input.on( 'keydown mouseup cut paste change input select', this.onEdit.bind( this ) );
14903
14904 // Initialization
14905 this.$input
14906 .addClass( 'oo-ui-inputWidget-input' )
14907 .attr( 'name', config.name )
14908 .prop( 'disabled', this.isDisabled() );
14909 this.$element
14910 .addClass( 'oo-ui-inputWidget' )
14911 .append( this.$input );
14912 this.setValue( config.value );
14913 this.setAccessKey( config.accessKey );
14914 if ( config.dir ) {
14915 this.setDir( config.dir );
14916 }
14917 };
14918
14919 /* Setup */
14920
14921 OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
14922 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.FlaggedElement );
14923 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.TabIndexedElement );
14924 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.TitledElement );
14925 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.AccessKeyedElement );
14926
14927 /* Static Properties */
14928
14929 OO.ui.InputWidget.static.supportsSimpleLabel = true;
14930
14931 /* Static Methods */
14932
14933 /**
14934 * @inheritdoc
14935 */
14936 OO.ui.InputWidget.static.reusePreInfuseDOM = function ( node, config ) {
14937 config = OO.ui.InputWidget.parent.static.reusePreInfuseDOM( node, config );
14938 // Reusing $input lets browsers preserve inputted values across page reloads (T114134)
14939 config.$input = $( node ).find( '.oo-ui-inputWidget-input' );
14940 return config;
14941 };
14942
14943 /**
14944 * @inheritdoc
14945 */
14946 OO.ui.InputWidget.static.gatherPreInfuseState = function ( node, config ) {
14947 var state = OO.ui.InputWidget.parent.static.gatherPreInfuseState( node, config );
14948 state.value = config.$input.val();
14949 // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
14950 state.focus = config.$input.is( ':focus' );
14951 return state;
14952 };
14953
14954 /* Events */
14955
14956 /**
14957 * @event change
14958 *
14959 * A change event is emitted when the value of the input changes.
14960 *
14961 * @param {string} value
14962 */
14963
14964 /* Methods */
14965
14966 /**
14967 * Get input element.
14968 *
14969 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
14970 * different circumstances. The element must have a `value` property (like form elements).
14971 *
14972 * @protected
14973 * @param {Object} config Configuration options
14974 * @return {jQuery} Input element
14975 */
14976 OO.ui.InputWidget.prototype.getInputElement = function ( config ) {
14977 // See #reusePreInfuseDOM about config.$input
14978 return config.$input || $( '<input>' );
14979 };
14980
14981 /**
14982 * Handle potentially value-changing events.
14983 *
14984 * @private
14985 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
14986 */
14987 OO.ui.InputWidget.prototype.onEdit = function () {
14988 var widget = this;
14989 if ( !this.isDisabled() ) {
14990 // Allow the stack to clear so the value will be updated
14991 setTimeout( function () {
14992 widget.setValue( widget.$input.val() );
14993 } );
14994 }
14995 };
14996
14997 /**
14998 * Get the value of the input.
14999 *
15000 * @return {string} Input value
15001 */
15002 OO.ui.InputWidget.prototype.getValue = function () {
15003 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
15004 // it, and we won't know unless they're kind enough to trigger a 'change' event.
15005 var value = this.$input.val();
15006 if ( this.value !== value ) {
15007 this.setValue( value );
15008 }
15009 return this.value;
15010 };
15011
15012 /**
15013 * Set the directionality of the input, either RTL (right-to-left) or LTR (left-to-right).
15014 *
15015 * @deprecated since v0.13.1, use #setDir directly
15016 * @param {boolean} isRTL Directionality is right-to-left
15017 * @chainable
15018 */
15019 OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
15020 this.setDir( isRTL ? 'rtl' : 'ltr' );
15021 return this;
15022 };
15023
15024 /**
15025 * Set the directionality of the input.
15026 *
15027 * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
15028 * @chainable
15029 */
15030 OO.ui.InputWidget.prototype.setDir = function ( dir ) {
15031 this.$input.prop( 'dir', dir );
15032 return this;
15033 };
15034
15035 /**
15036 * Set the value of the input.
15037 *
15038 * @param {string} value New value
15039 * @fires change
15040 * @chainable
15041 */
15042 OO.ui.InputWidget.prototype.setValue = function ( value ) {
15043 value = this.cleanUpValue( value );
15044 // Update the DOM if it has changed. Note that with cleanUpValue, it
15045 // is possible for the DOM value to change without this.value changing.
15046 if ( this.$input.val() !== value ) {
15047 this.$input.val( value );
15048 }
15049 if ( this.value !== value ) {
15050 this.value = value;
15051 this.emit( 'change', this.value );
15052 }
15053 return this;
15054 };
15055
15056 /**
15057 * Set the input's access key.
15058 * FIXME: This is the same code as in OO.ui.mixin.ButtonElement, maybe find a better place for it?
15059 *
15060 * @param {string} accessKey Input's access key, use empty string to remove
15061 * @chainable
15062 */
15063 OO.ui.InputWidget.prototype.setAccessKey = function ( accessKey ) {
15064 accessKey = typeof accessKey === 'string' && accessKey.length ? accessKey : null;
15065
15066 if ( this.accessKey !== accessKey ) {
15067 if ( this.$input ) {
15068 if ( accessKey !== null ) {
15069 this.$input.attr( 'accesskey', accessKey );
15070 } else {
15071 this.$input.removeAttr( 'accesskey' );
15072 }
15073 }
15074 this.accessKey = accessKey;
15075 }
15076
15077 return this;
15078 };
15079
15080 /**
15081 * Clean up incoming value.
15082 *
15083 * Ensures value is a string, and converts undefined and null to empty string.
15084 *
15085 * @private
15086 * @param {string} value Original value
15087 * @return {string} Cleaned up value
15088 */
15089 OO.ui.InputWidget.prototype.cleanUpValue = function ( value ) {
15090 if ( value === undefined || value === null ) {
15091 return '';
15092 } else if ( this.inputFilter ) {
15093 return this.inputFilter( String( value ) );
15094 } else {
15095 return String( value );
15096 }
15097 };
15098
15099 /**
15100 * Simulate the behavior of clicking on a label bound to this input. This method is only called by
15101 * {@link OO.ui.LabelWidget LabelWidget} and {@link OO.ui.FieldLayout FieldLayout}. It should not be
15102 * called directly.
15103 */
15104 OO.ui.InputWidget.prototype.simulateLabelClick = function () {
15105 if ( !this.isDisabled() ) {
15106 if ( this.$input.is( ':checkbox, :radio' ) ) {
15107 this.$input.click();
15108 }
15109 if ( this.$input.is( ':input' ) ) {
15110 this.$input[ 0 ].focus();
15111 }
15112 }
15113 };
15114
15115 /**
15116 * @inheritdoc
15117 */
15118 OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
15119 OO.ui.InputWidget.parent.prototype.setDisabled.call( this, state );
15120 if ( this.$input ) {
15121 this.$input.prop( 'disabled', this.isDisabled() );
15122 }
15123 return this;
15124 };
15125
15126 /**
15127 * Focus the input.
15128 *
15129 * @chainable
15130 */
15131 OO.ui.InputWidget.prototype.focus = function () {
15132 this.$input[ 0 ].focus();
15133 return this;
15134 };
15135
15136 /**
15137 * Blur the input.
15138 *
15139 * @chainable
15140 */
15141 OO.ui.InputWidget.prototype.blur = function () {
15142 this.$input[ 0 ].blur();
15143 return this;
15144 };
15145
15146 /**
15147 * @inheritdoc
15148 */
15149 OO.ui.InputWidget.prototype.restorePreInfuseState = function ( state ) {
15150 OO.ui.InputWidget.parent.prototype.restorePreInfuseState.call( this, state );
15151 if ( state.value !== undefined && state.value !== this.getValue() ) {
15152 this.setValue( state.value );
15153 }
15154 if ( state.focus ) {
15155 this.focus();
15156 }
15157 };
15158
15159 /**
15160 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
15161 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
15162 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
15163 * HTML `<button/>` (the default) or an HTML `<input/>` tags. See the
15164 * [OOjs UI documentation on MediaWiki] [1] for more information.
15165 *
15166 * @example
15167 * // A ButtonInputWidget rendered as an HTML button, the default.
15168 * var button = new OO.ui.ButtonInputWidget( {
15169 * label: 'Input button',
15170 * icon: 'check',
15171 * value: 'check'
15172 * } );
15173 * $( 'body' ).append( button.$element );
15174 *
15175 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
15176 *
15177 * @class
15178 * @extends OO.ui.InputWidget
15179 * @mixins OO.ui.mixin.ButtonElement
15180 * @mixins OO.ui.mixin.IconElement
15181 * @mixins OO.ui.mixin.IndicatorElement
15182 * @mixins OO.ui.mixin.LabelElement
15183 * @mixins OO.ui.mixin.TitledElement
15184 *
15185 * @constructor
15186 * @param {Object} [config] Configuration options
15187 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
15188 * @cfg {boolean} [useInputTag=false] Use an `<input/>` tag instead of a `<button/>` tag, the default.
15189 * Widgets configured to be an `<input/>` do not support {@link #icon icons} and {@link #indicator indicators},
15190 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
15191 * be set to `true` when there’s need to support IE6 in a form with multiple buttons.
15192 */
15193 OO.ui.ButtonInputWidget = function OoUiButtonInputWidget( config ) {
15194 // Configuration initialization
15195 config = $.extend( { type: 'button', useInputTag: false }, config );
15196
15197 // Properties (must be set before parent constructor, which calls #setValue)
15198 this.useInputTag = config.useInputTag;
15199
15200 // Parent constructor
15201 OO.ui.ButtonInputWidget.parent.call( this, config );
15202
15203 // Mixin constructors
15204 OO.ui.mixin.ButtonElement.call( this, $.extend( {}, config, { $button: this.$input } ) );
15205 OO.ui.mixin.IconElement.call( this, config );
15206 OO.ui.mixin.IndicatorElement.call( this, config );
15207 OO.ui.mixin.LabelElement.call( this, config );
15208 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
15209
15210 // Initialization
15211 if ( !config.useInputTag ) {
15212 this.$input.append( this.$icon, this.$label, this.$indicator );
15213 }
15214 this.$element.addClass( 'oo-ui-buttonInputWidget' );
15215 };
15216
15217 /* Setup */
15218
15219 OO.inheritClass( OO.ui.ButtonInputWidget, OO.ui.InputWidget );
15220 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.ButtonElement );
15221 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IconElement );
15222 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IndicatorElement );
15223 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.LabelElement );
15224 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.TitledElement );
15225
15226 /* Static Properties */
15227
15228 /**
15229 * Disable generating `<label>` elements for buttons. One would very rarely need additional label
15230 * for a button, and it's already a big clickable target, and it causes unexpected rendering.
15231 */
15232 OO.ui.ButtonInputWidget.static.supportsSimpleLabel = false;
15233
15234 /* Methods */
15235
15236 /**
15237 * @inheritdoc
15238 * @protected
15239 */
15240 OO.ui.ButtonInputWidget.prototype.getInputElement = function ( config ) {
15241 var type;
15242 // See InputWidget#reusePreInfuseDOM about config.$input
15243 if ( config.$input ) {
15244 return config.$input.empty();
15245 }
15246 type = [ 'button', 'submit', 'reset' ].indexOf( config.type ) !== -1 ? config.type : 'button';
15247 return $( '<' + ( config.useInputTag ? 'input' : 'button' ) + ' type="' + type + '">' );
15248 };
15249
15250 /**
15251 * Set label value.
15252 *
15253 * If #useInputTag is `true`, the label is set as the `value` of the `<input/>` tag.
15254 *
15255 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
15256 * text, or `null` for no label
15257 * @chainable
15258 */
15259 OO.ui.ButtonInputWidget.prototype.setLabel = function ( label ) {
15260 OO.ui.mixin.LabelElement.prototype.setLabel.call( this, label );
15261
15262 if ( this.useInputTag ) {
15263 if ( typeof label === 'function' ) {
15264 label = OO.ui.resolveMsg( label );
15265 }
15266 if ( label instanceof jQuery ) {
15267 label = label.text();
15268 }
15269 if ( !label ) {
15270 label = '';
15271 }
15272 this.$input.val( label );
15273 }
15274
15275 return this;
15276 };
15277
15278 /**
15279 * Set the value of the input.
15280 *
15281 * This method is disabled for button inputs configured as {@link #useInputTag <input/> tags}, as
15282 * they do not support {@link #value values}.
15283 *
15284 * @param {string} value New value
15285 * @chainable
15286 */
15287 OO.ui.ButtonInputWidget.prototype.setValue = function ( value ) {
15288 if ( !this.useInputTag ) {
15289 OO.ui.ButtonInputWidget.parent.prototype.setValue.call( this, value );
15290 }
15291 return this;
15292 };
15293
15294 /**
15295 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
15296 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
15297 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
15298 * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
15299 *
15300 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
15301 *
15302 * @example
15303 * // An example of selected, unselected, and disabled checkbox inputs
15304 * var checkbox1=new OO.ui.CheckboxInputWidget( {
15305 * value: 'a',
15306 * selected: true
15307 * } );
15308 * var checkbox2=new OO.ui.CheckboxInputWidget( {
15309 * value: 'b'
15310 * } );
15311 * var checkbox3=new OO.ui.CheckboxInputWidget( {
15312 * value:'c',
15313 * disabled: true
15314 * } );
15315 * // Create a fieldset layout with fields for each checkbox.
15316 * var fieldset = new OO.ui.FieldsetLayout( {
15317 * label: 'Checkboxes'
15318 * } );
15319 * fieldset.addItems( [
15320 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
15321 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
15322 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
15323 * ] );
15324 * $( 'body' ).append( fieldset.$element );
15325 *
15326 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
15327 *
15328 * @class
15329 * @extends OO.ui.InputWidget
15330 *
15331 * @constructor
15332 * @param {Object} [config] Configuration options
15333 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
15334 */
15335 OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
15336 // Configuration initialization
15337 config = config || {};
15338
15339 // Parent constructor
15340 OO.ui.CheckboxInputWidget.parent.call( this, config );
15341
15342 // Initialization
15343 this.$element
15344 .addClass( 'oo-ui-checkboxInputWidget' )
15345 // Required for pretty styling in MediaWiki theme
15346 .append( $( '<span>' ) );
15347 this.setSelected( config.selected !== undefined ? config.selected : false );
15348 };
15349
15350 /* Setup */
15351
15352 OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
15353
15354 /* Static Methods */
15355
15356 /**
15357 * @inheritdoc
15358 */
15359 OO.ui.CheckboxInputWidget.static.gatherPreInfuseState = function ( node, config ) {
15360 var state = OO.ui.CheckboxInputWidget.parent.static.gatherPreInfuseState( node, config );
15361 state.checked = config.$input.prop( 'checked' );
15362 return state;
15363 };
15364
15365 /* Methods */
15366
15367 /**
15368 * @inheritdoc
15369 * @protected
15370 */
15371 OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
15372 return $( '<input type="checkbox" />' );
15373 };
15374
15375 /**
15376 * @inheritdoc
15377 */
15378 OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
15379 var widget = this;
15380 if ( !this.isDisabled() ) {
15381 // Allow the stack to clear so the value will be updated
15382 setTimeout( function () {
15383 widget.setSelected( widget.$input.prop( 'checked' ) );
15384 } );
15385 }
15386 };
15387
15388 /**
15389 * Set selection state of this checkbox.
15390 *
15391 * @param {boolean} state `true` for selected
15392 * @chainable
15393 */
15394 OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state ) {
15395 state = !!state;
15396 if ( this.selected !== state ) {
15397 this.selected = state;
15398 this.$input.prop( 'checked', this.selected );
15399 this.emit( 'change', this.selected );
15400 }
15401 return this;
15402 };
15403
15404 /**
15405 * Check if this checkbox is selected.
15406 *
15407 * @return {boolean} Checkbox is selected
15408 */
15409 OO.ui.CheckboxInputWidget.prototype.isSelected = function () {
15410 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
15411 // it, and we won't know unless they're kind enough to trigger a 'change' event.
15412 var selected = this.$input.prop( 'checked' );
15413 if ( this.selected !== selected ) {
15414 this.setSelected( selected );
15415 }
15416 return this.selected;
15417 };
15418
15419 /**
15420 * @inheritdoc
15421 */
15422 OO.ui.CheckboxInputWidget.prototype.restorePreInfuseState = function ( state ) {
15423 OO.ui.CheckboxInputWidget.parent.prototype.restorePreInfuseState.call( this, state );
15424 if ( state.checked !== undefined && state.checked !== this.isSelected() ) {
15425 this.setSelected( state.checked );
15426 }
15427 };
15428
15429 /**
15430 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
15431 * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
15432 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
15433 * more information about input widgets.
15434 *
15435 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
15436 * are no options. If no `value` configuration option is provided, the first option is selected.
15437 * If you need a state representing no value (no option being selected), use a DropdownWidget.
15438 *
15439 * This and OO.ui.RadioSelectInputWidget support the same configuration options.
15440 *
15441 * @example
15442 * // Example: A DropdownInputWidget with three options
15443 * var dropdownInput = new OO.ui.DropdownInputWidget( {
15444 * options: [
15445 * { data: 'a', label: 'First' },
15446 * { data: 'b', label: 'Second'},
15447 * { data: 'c', label: 'Third' }
15448 * ]
15449 * } );
15450 * $( 'body' ).append( dropdownInput.$element );
15451 *
15452 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
15453 *
15454 * @class
15455 * @extends OO.ui.InputWidget
15456 * @mixins OO.ui.mixin.TitledElement
15457 *
15458 * @constructor
15459 * @param {Object} [config] Configuration options
15460 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
15461 * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
15462 */
15463 OO.ui.DropdownInputWidget = function OoUiDropdownInputWidget( config ) {
15464 // Configuration initialization
15465 config = config || {};
15466
15467 // Properties (must be done before parent constructor which calls #setDisabled)
15468 this.dropdownWidget = new OO.ui.DropdownWidget( config.dropdown );
15469
15470 // Parent constructor
15471 OO.ui.DropdownInputWidget.parent.call( this, config );
15472
15473 // Mixin constructors
15474 OO.ui.mixin.TitledElement.call( this, config );
15475
15476 // Events
15477 this.dropdownWidget.getMenu().connect( this, { select: 'onMenuSelect' } );
15478
15479 // Initialization
15480 this.setOptions( config.options || [] );
15481 this.$element
15482 .addClass( 'oo-ui-dropdownInputWidget' )
15483 .append( this.dropdownWidget.$element );
15484 };
15485
15486 /* Setup */
15487
15488 OO.inheritClass( OO.ui.DropdownInputWidget, OO.ui.InputWidget );
15489 OO.mixinClass( OO.ui.DropdownInputWidget, OO.ui.mixin.TitledElement );
15490
15491 /* Methods */
15492
15493 /**
15494 * @inheritdoc
15495 * @protected
15496 */
15497 OO.ui.DropdownInputWidget.prototype.getInputElement = function ( config ) {
15498 // See InputWidget#reusePreInfuseDOM about config.$input
15499 if ( config.$input ) {
15500 return config.$input.addClass( 'oo-ui-element-hidden' );
15501 }
15502 return $( '<input type="hidden">' );
15503 };
15504
15505 /**
15506 * Handles menu select events.
15507 *
15508 * @private
15509 * @param {OO.ui.MenuOptionWidget} item Selected menu item
15510 */
15511 OO.ui.DropdownInputWidget.prototype.onMenuSelect = function ( item ) {
15512 this.setValue( item.getData() );
15513 };
15514
15515 /**
15516 * @inheritdoc
15517 */
15518 OO.ui.DropdownInputWidget.prototype.setValue = function ( value ) {
15519 value = this.cleanUpValue( value );
15520 this.dropdownWidget.getMenu().selectItemByData( value );
15521 OO.ui.DropdownInputWidget.parent.prototype.setValue.call( this, value );
15522 return this;
15523 };
15524
15525 /**
15526 * @inheritdoc
15527 */
15528 OO.ui.DropdownInputWidget.prototype.setDisabled = function ( state ) {
15529 this.dropdownWidget.setDisabled( state );
15530 OO.ui.DropdownInputWidget.parent.prototype.setDisabled.call( this, state );
15531 return this;
15532 };
15533
15534 /**
15535 * Set the options available for this input.
15536 *
15537 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
15538 * @chainable
15539 */
15540 OO.ui.DropdownInputWidget.prototype.setOptions = function ( options ) {
15541 var
15542 value = this.getValue(),
15543 widget = this;
15544
15545 // Rebuild the dropdown menu
15546 this.dropdownWidget.getMenu()
15547 .clearItems()
15548 .addItems( options.map( function ( opt ) {
15549 var optValue = widget.cleanUpValue( opt.data );
15550 return new OO.ui.MenuOptionWidget( {
15551 data: optValue,
15552 label: opt.label !== undefined ? opt.label : optValue
15553 } );
15554 } ) );
15555
15556 // Restore the previous value, or reset to something sensible
15557 if ( this.dropdownWidget.getMenu().getItemFromData( value ) ) {
15558 // Previous value is still available, ensure consistency with the dropdown
15559 this.setValue( value );
15560 } else {
15561 // No longer valid, reset
15562 if ( options.length ) {
15563 this.setValue( options[ 0 ].data );
15564 }
15565 }
15566
15567 return this;
15568 };
15569
15570 /**
15571 * @inheritdoc
15572 */
15573 OO.ui.DropdownInputWidget.prototype.focus = function () {
15574 this.dropdownWidget.getMenu().toggle( true );
15575 return this;
15576 };
15577
15578 /**
15579 * @inheritdoc
15580 */
15581 OO.ui.DropdownInputWidget.prototype.blur = function () {
15582 this.dropdownWidget.getMenu().toggle( false );
15583 return this;
15584 };
15585
15586 /**
15587 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
15588 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
15589 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
15590 * please see the [OOjs UI documentation on MediaWiki][1].
15591 *
15592 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
15593 *
15594 * @example
15595 * // An example of selected, unselected, and disabled radio inputs
15596 * var radio1 = new OO.ui.RadioInputWidget( {
15597 * value: 'a',
15598 * selected: true
15599 * } );
15600 * var radio2 = new OO.ui.RadioInputWidget( {
15601 * value: 'b'
15602 * } );
15603 * var radio3 = new OO.ui.RadioInputWidget( {
15604 * value: 'c',
15605 * disabled: true
15606 * } );
15607 * // Create a fieldset layout with fields for each radio button.
15608 * var fieldset = new OO.ui.FieldsetLayout( {
15609 * label: 'Radio inputs'
15610 * } );
15611 * fieldset.addItems( [
15612 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
15613 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
15614 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
15615 * ] );
15616 * $( 'body' ).append( fieldset.$element );
15617 *
15618 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
15619 *
15620 * @class
15621 * @extends OO.ui.InputWidget
15622 *
15623 * @constructor
15624 * @param {Object} [config] Configuration options
15625 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
15626 */
15627 OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) {
15628 // Configuration initialization
15629 config = config || {};
15630
15631 // Parent constructor
15632 OO.ui.RadioInputWidget.parent.call( this, config );
15633
15634 // Initialization
15635 this.$element
15636 .addClass( 'oo-ui-radioInputWidget' )
15637 // Required for pretty styling in MediaWiki theme
15638 .append( $( '<span>' ) );
15639 this.setSelected( config.selected !== undefined ? config.selected : false );
15640 };
15641
15642 /* Setup */
15643
15644 OO.inheritClass( OO.ui.RadioInputWidget, OO.ui.InputWidget );
15645
15646 /* Static Methods */
15647
15648 /**
15649 * @inheritdoc
15650 */
15651 OO.ui.RadioInputWidget.static.gatherPreInfuseState = function ( node, config ) {
15652 var state = OO.ui.RadioInputWidget.parent.static.gatherPreInfuseState( node, config );
15653 state.checked = config.$input.prop( 'checked' );
15654 return state;
15655 };
15656
15657 /* Methods */
15658
15659 /**
15660 * @inheritdoc
15661 * @protected
15662 */
15663 OO.ui.RadioInputWidget.prototype.getInputElement = function () {
15664 return $( '<input type="radio" />' );
15665 };
15666
15667 /**
15668 * @inheritdoc
15669 */
15670 OO.ui.RadioInputWidget.prototype.onEdit = function () {
15671 // RadioInputWidget doesn't track its state.
15672 };
15673
15674 /**
15675 * Set selection state of this radio button.
15676 *
15677 * @param {boolean} state `true` for selected
15678 * @chainable
15679 */
15680 OO.ui.RadioInputWidget.prototype.setSelected = function ( state ) {
15681 // RadioInputWidget doesn't track its state.
15682 this.$input.prop( 'checked', state );
15683 return this;
15684 };
15685
15686 /**
15687 * Check if this radio button is selected.
15688 *
15689 * @return {boolean} Radio is selected
15690 */
15691 OO.ui.RadioInputWidget.prototype.isSelected = function () {
15692 return this.$input.prop( 'checked' );
15693 };
15694
15695 /**
15696 * @inheritdoc
15697 */
15698 OO.ui.RadioInputWidget.prototype.restorePreInfuseState = function ( state ) {
15699 OO.ui.RadioInputWidget.parent.prototype.restorePreInfuseState.call( this, state );
15700 if ( state.checked !== undefined && state.checked !== this.isSelected() ) {
15701 this.setSelected( state.checked );
15702 }
15703 };
15704
15705 /**
15706 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
15707 * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
15708 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
15709 * more information about input widgets.
15710 *
15711 * This and OO.ui.DropdownInputWidget support the same configuration options.
15712 *
15713 * @example
15714 * // Example: A RadioSelectInputWidget with three options
15715 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
15716 * options: [
15717 * { data: 'a', label: 'First' },
15718 * { data: 'b', label: 'Second'},
15719 * { data: 'c', label: 'Third' }
15720 * ]
15721 * } );
15722 * $( 'body' ).append( radioSelectInput.$element );
15723 *
15724 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
15725 *
15726 * @class
15727 * @extends OO.ui.InputWidget
15728 *
15729 * @constructor
15730 * @param {Object} [config] Configuration options
15731 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
15732 */
15733 OO.ui.RadioSelectInputWidget = function OoUiRadioSelectInputWidget( config ) {
15734 // Configuration initialization
15735 config = config || {};
15736
15737 // Properties (must be done before parent constructor which calls #setDisabled)
15738 this.radioSelectWidget = new OO.ui.RadioSelectWidget();
15739
15740 // Parent constructor
15741 OO.ui.RadioSelectInputWidget.parent.call( this, config );
15742
15743 // Events
15744 this.radioSelectWidget.connect( this, { select: 'onMenuSelect' } );
15745
15746 // Initialization
15747 this.setOptions( config.options || [] );
15748 this.$element
15749 .addClass( 'oo-ui-radioSelectInputWidget' )
15750 .append( this.radioSelectWidget.$element );
15751 };
15752
15753 /* Setup */
15754
15755 OO.inheritClass( OO.ui.RadioSelectInputWidget, OO.ui.InputWidget );
15756
15757 /* Static Properties */
15758
15759 OO.ui.RadioSelectInputWidget.static.supportsSimpleLabel = false;
15760
15761 /* Static Methods */
15762
15763 /**
15764 * @inheritdoc
15765 */
15766 OO.ui.RadioSelectInputWidget.static.gatherPreInfuseState = function ( node, config ) {
15767 var state = OO.ui.RadioSelectInputWidget.parent.static.gatherPreInfuseState( node, config );
15768 state.value = $( node ).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
15769 return state;
15770 };
15771
15772 /* Methods */
15773
15774 /**
15775 * @inheritdoc
15776 * @protected
15777 */
15778 OO.ui.RadioSelectInputWidget.prototype.getInputElement = function () {
15779 return $( '<input type="hidden">' );
15780 };
15781
15782 /**
15783 * Handles menu select events.
15784 *
15785 * @private
15786 * @param {OO.ui.RadioOptionWidget} item Selected menu item
15787 */
15788 OO.ui.RadioSelectInputWidget.prototype.onMenuSelect = function ( item ) {
15789 this.setValue( item.getData() );
15790 };
15791
15792 /**
15793 * @inheritdoc
15794 */
15795 OO.ui.RadioSelectInputWidget.prototype.setValue = function ( value ) {
15796 value = this.cleanUpValue( value );
15797 this.radioSelectWidget.selectItemByData( value );
15798 OO.ui.RadioSelectInputWidget.parent.prototype.setValue.call( this, value );
15799 return this;
15800 };
15801
15802 /**
15803 * @inheritdoc
15804 */
15805 OO.ui.RadioSelectInputWidget.prototype.setDisabled = function ( state ) {
15806 this.radioSelectWidget.setDisabled( state );
15807 OO.ui.RadioSelectInputWidget.parent.prototype.setDisabled.call( this, state );
15808 return this;
15809 };
15810
15811 /**
15812 * Set the options available for this input.
15813 *
15814 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
15815 * @chainable
15816 */
15817 OO.ui.RadioSelectInputWidget.prototype.setOptions = function ( options ) {
15818 var
15819 value = this.getValue(),
15820 widget = this;
15821
15822 // Rebuild the radioSelect menu
15823 this.radioSelectWidget
15824 .clearItems()
15825 .addItems( options.map( function ( opt ) {
15826 var optValue = widget.cleanUpValue( opt.data );
15827 return new OO.ui.RadioOptionWidget( {
15828 data: optValue,
15829 label: opt.label !== undefined ? opt.label : optValue
15830 } );
15831 } ) );
15832
15833 // Restore the previous value, or reset to something sensible
15834 if ( this.radioSelectWidget.getItemFromData( value ) ) {
15835 // Previous value is still available, ensure consistency with the radioSelect
15836 this.setValue( value );
15837 } else {
15838 // No longer valid, reset
15839 if ( options.length ) {
15840 this.setValue( options[ 0 ].data );
15841 }
15842 }
15843
15844 return this;
15845 };
15846
15847 /**
15848 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
15849 * size of the field as well as its presentation. In addition, these widgets can be configured
15850 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
15851 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
15852 * which modifies incoming values rather than validating them.
15853 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
15854 *
15855 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
15856 *
15857 * @example
15858 * // Example of a text input widget
15859 * var textInput = new OO.ui.TextInputWidget( {
15860 * value: 'Text input'
15861 * } )
15862 * $( 'body' ).append( textInput.$element );
15863 *
15864 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
15865 *
15866 * @class
15867 * @extends OO.ui.InputWidget
15868 * @mixins OO.ui.mixin.IconElement
15869 * @mixins OO.ui.mixin.IndicatorElement
15870 * @mixins OO.ui.mixin.PendingElement
15871 * @mixins OO.ui.mixin.LabelElement
15872 *
15873 * @constructor
15874 * @param {Object} [config] Configuration options
15875 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password', 'search',
15876 * 'email' or 'url'. Ignored if `multiline` is true.
15877 *
15878 * Some values of `type` result in additional behaviors:
15879 *
15880 * - `search`: implies `icon: 'search'` and `indicator: 'clear'`; when clicked, the indicator
15881 * empties the text field
15882 * @cfg {string} [placeholder] Placeholder text
15883 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
15884 * instruct the browser to focus this widget.
15885 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
15886 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
15887 * @cfg {boolean} [multiline=false] Allow multiple lines of text
15888 * @cfg {number} [rows] If multiline, number of visible lines in textarea. If used with `autosize`,
15889 * specifies minimum number of rows to display.
15890 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
15891 * Use the #maxRows config to specify a maximum number of displayed rows.
15892 * @cfg {boolean} [maxRows] Maximum number of rows to display when #autosize is set to true.
15893 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
15894 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
15895 * the value or placeholder text: `'before'` or `'after'`
15896 * @cfg {boolean} [required=false] Mark the field as required. Implies `indicator: 'required'`.
15897 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
15898 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
15899 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
15900 * (the value must contain only numbers); when RegExp, a regular expression that must match the
15901 * value for it to be considered valid; when Function, a function receiving the value as parameter
15902 * that must return true, or promise resolving to true, for it to be considered valid.
15903 */
15904 OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
15905 // Configuration initialization
15906 config = $.extend( {
15907 type: 'text',
15908 labelPosition: 'after'
15909 }, config );
15910 if ( config.type === 'search' ) {
15911 if ( config.icon === undefined ) {
15912 config.icon = 'search';
15913 }
15914 // indicator: 'clear' is set dynamically later, depending on value
15915 }
15916 if ( config.required ) {
15917 if ( config.indicator === undefined ) {
15918 config.indicator = 'required';
15919 }
15920 }
15921
15922 // Parent constructor
15923 OO.ui.TextInputWidget.parent.call( this, config );
15924
15925 // Mixin constructors
15926 OO.ui.mixin.IconElement.call( this, config );
15927 OO.ui.mixin.IndicatorElement.call( this, config );
15928 OO.ui.mixin.PendingElement.call( this, $.extend( {}, config, { $pending: this.$input } ) );
15929 OO.ui.mixin.LabelElement.call( this, config );
15930
15931 // Properties
15932 this.type = this.getSaneType( config );
15933 this.readOnly = false;
15934 this.multiline = !!config.multiline;
15935 this.autosize = !!config.autosize;
15936 this.minRows = config.rows !== undefined ? config.rows : '';
15937 this.maxRows = config.maxRows || Math.max( 2 * ( this.minRows || 0 ), 10 );
15938 this.validate = null;
15939 this.styleHeight = null;
15940 this.scrollWidth = null;
15941
15942 // Clone for resizing
15943 if ( this.autosize ) {
15944 this.$clone = this.$input
15945 .clone()
15946 .insertAfter( this.$input )
15947 .attr( 'aria-hidden', 'true' )
15948 .addClass( 'oo-ui-element-hidden' );
15949 }
15950
15951 this.setValidation( config.validate );
15952 this.setLabelPosition( config.labelPosition );
15953
15954 // Events
15955 this.$input.on( {
15956 keypress: this.onKeyPress.bind( this ),
15957 blur: this.onBlur.bind( this )
15958 } );
15959 this.$input.one( {
15960 focus: this.onElementAttach.bind( this )
15961 } );
15962 this.$icon.on( 'mousedown', this.onIconMouseDown.bind( this ) );
15963 this.$indicator.on( 'mousedown', this.onIndicatorMouseDown.bind( this ) );
15964 this.on( 'labelChange', this.updatePosition.bind( this ) );
15965 this.connect( this, {
15966 change: 'onChange',
15967 disable: 'onDisable'
15968 } );
15969
15970 // Initialization
15971 this.$element
15972 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type )
15973 .append( this.$icon, this.$indicator );
15974 this.setReadOnly( !!config.readOnly );
15975 this.updateSearchIndicator();
15976 if ( config.placeholder ) {
15977 this.$input.attr( 'placeholder', config.placeholder );
15978 }
15979 if ( config.maxLength !== undefined ) {
15980 this.$input.attr( 'maxlength', config.maxLength );
15981 }
15982 if ( config.autofocus ) {
15983 this.$input.attr( 'autofocus', 'autofocus' );
15984 }
15985 if ( config.required ) {
15986 this.$input.attr( 'required', 'required' );
15987 this.$input.attr( 'aria-required', 'true' );
15988 }
15989 if ( config.autocomplete === false ) {
15990 this.$input.attr( 'autocomplete', 'off' );
15991 // Turning off autocompletion also disables "form caching" when the user navigates to a
15992 // different page and then clicks "Back". Re-enable it when leaving. Borrowed from jQuery UI.
15993 $( window ).on( {
15994 beforeunload: function () {
15995 this.$input.removeAttr( 'autocomplete' );
15996 }.bind( this ),
15997 pageshow: function () {
15998 // Browsers don't seem to actually fire this event on "Back", they instead just reload the
15999 // whole page... it shouldn't hurt, though.
16000 this.$input.attr( 'autocomplete', 'off' );
16001 }.bind( this )
16002 } );
16003 }
16004 if ( this.multiline && config.rows ) {
16005 this.$input.attr( 'rows', config.rows );
16006 }
16007 if ( this.label || config.autosize ) {
16008 this.installParentChangeDetector();
16009 }
16010 };
16011
16012 /* Setup */
16013
16014 OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
16015 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IconElement );
16016 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IndicatorElement );
16017 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.PendingElement );
16018 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.LabelElement );
16019
16020 /* Static Properties */
16021
16022 OO.ui.TextInputWidget.static.validationPatterns = {
16023 'non-empty': /.+/,
16024 integer: /^\d+$/
16025 };
16026
16027 /* Static Methods */
16028
16029 /**
16030 * @inheritdoc
16031 */
16032 OO.ui.TextInputWidget.static.gatherPreInfuseState = function ( node, config ) {
16033 var state = OO.ui.TextInputWidget.parent.static.gatherPreInfuseState( node, config );
16034 if ( config.multiline ) {
16035 state.scrollTop = config.$input.scrollTop();
16036 }
16037 return state;
16038 };
16039
16040 /* Events */
16041
16042 /**
16043 * An `enter` event is emitted when the user presses 'enter' inside the text box.
16044 *
16045 * Not emitted if the input is multiline.
16046 *
16047 * @event enter
16048 */
16049
16050 /**
16051 * A `resize` event is emitted when autosize is set and the widget resizes
16052 *
16053 * @event resize
16054 */
16055
16056 /* Methods */
16057
16058 /**
16059 * Handle icon mouse down events.
16060 *
16061 * @private
16062 * @param {jQuery.Event} e Mouse down event
16063 * @fires icon
16064 */
16065 OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
16066 if ( e.which === OO.ui.MouseButtons.LEFT ) {
16067 this.$input[ 0 ].focus();
16068 return false;
16069 }
16070 };
16071
16072 /**
16073 * Handle indicator mouse down events.
16074 *
16075 * @private
16076 * @param {jQuery.Event} e Mouse down event
16077 * @fires indicator
16078 */
16079 OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
16080 if ( e.which === OO.ui.MouseButtons.LEFT ) {
16081 if ( this.type === 'search' ) {
16082 // Clear the text field
16083 this.setValue( '' );
16084 }
16085 this.$input[ 0 ].focus();
16086 return false;
16087 }
16088 };
16089
16090 /**
16091 * Handle key press events.
16092 *
16093 * @private
16094 * @param {jQuery.Event} e Key press event
16095 * @fires enter If enter key is pressed and input is not multiline
16096 */
16097 OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
16098 if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) {
16099 this.emit( 'enter', e );
16100 }
16101 };
16102
16103 /**
16104 * Handle blur events.
16105 *
16106 * @private
16107 * @param {jQuery.Event} e Blur event
16108 */
16109 OO.ui.TextInputWidget.prototype.onBlur = function () {
16110 this.setValidityFlag();
16111 };
16112
16113 /**
16114 * Handle element attach events.
16115 *
16116 * @private
16117 * @param {jQuery.Event} e Element attach event
16118 */
16119 OO.ui.TextInputWidget.prototype.onElementAttach = function () {
16120 // Any previously calculated size is now probably invalid if we reattached elsewhere
16121 this.valCache = null;
16122 this.adjustSize();
16123 this.positionLabel();
16124 };
16125
16126 /**
16127 * Handle change events.
16128 *
16129 * @param {string} value
16130 * @private
16131 */
16132 OO.ui.TextInputWidget.prototype.onChange = function () {
16133 this.updateSearchIndicator();
16134 this.setValidityFlag();
16135 this.adjustSize();
16136 };
16137
16138 /**
16139 * Handle disable events.
16140 *
16141 * @param {boolean} disabled Element is disabled
16142 * @private
16143 */
16144 OO.ui.TextInputWidget.prototype.onDisable = function () {
16145 this.updateSearchIndicator();
16146 };
16147
16148 /**
16149 * Check if the input is {@link #readOnly read-only}.
16150 *
16151 * @return {boolean}
16152 */
16153 OO.ui.TextInputWidget.prototype.isReadOnly = function () {
16154 return this.readOnly;
16155 };
16156
16157 /**
16158 * Set the {@link #readOnly read-only} state of the input.
16159 *
16160 * @param {boolean} state Make input read-only
16161 * @chainable
16162 */
16163 OO.ui.TextInputWidget.prototype.setReadOnly = function ( state ) {
16164 this.readOnly = !!state;
16165 this.$input.prop( 'readOnly', this.readOnly );
16166 this.updateSearchIndicator();
16167 return this;
16168 };
16169
16170 /**
16171 * Support function for making #onElementAttach work across browsers.
16172 *
16173 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
16174 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
16175 *
16176 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
16177 * first time that the element gets attached to the documented.
16178 */
16179 OO.ui.TextInputWidget.prototype.installParentChangeDetector = function () {
16180 var mutationObserver, onRemove, topmostNode, fakeParentNode,
16181 MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
16182 widget = this;
16183
16184 if ( MutationObserver ) {
16185 // The new way. If only it wasn't so ugly.
16186
16187 if ( this.$element.closest( 'html' ).length ) {
16188 // Widget is attached already, do nothing. This breaks the functionality of this function when
16189 // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
16190 // would require observation of the whole document, which would hurt performance of other,
16191 // more important code.
16192 return;
16193 }
16194
16195 // Find topmost node in the tree
16196 topmostNode = this.$element[ 0 ];
16197 while ( topmostNode.parentNode ) {
16198 topmostNode = topmostNode.parentNode;
16199 }
16200
16201 // We have no way to detect the $element being attached somewhere without observing the entire
16202 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
16203 // parent node of $element, and instead detect when $element is removed from it (and thus
16204 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
16205 // doesn't get attached, we end up back here and create the parent.
16206
16207 mutationObserver = new MutationObserver( function ( mutations ) {
16208 var i, j, removedNodes;
16209 for ( i = 0; i < mutations.length; i++ ) {
16210 removedNodes = mutations[ i ].removedNodes;
16211 for ( j = 0; j < removedNodes.length; j++ ) {
16212 if ( removedNodes[ j ] === topmostNode ) {
16213 setTimeout( onRemove, 0 );
16214 return;
16215 }
16216 }
16217 }
16218 } );
16219
16220 onRemove = function () {
16221 // If the node was attached somewhere else, report it
16222 if ( widget.$element.closest( 'html' ).length ) {
16223 widget.onElementAttach();
16224 }
16225 mutationObserver.disconnect();
16226 widget.installParentChangeDetector();
16227 };
16228
16229 // Create a fake parent and observe it
16230 fakeParentNode = $( '<div>' ).append( topmostNode )[ 0 ];
16231 mutationObserver.observe( fakeParentNode, { childList: true } );
16232 } else {
16233 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
16234 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
16235 this.$element.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach.bind( this ) );
16236 }
16237 };
16238
16239 /**
16240 * Automatically adjust the size of the text input.
16241 *
16242 * This only affects #multiline inputs that are {@link #autosize autosized}.
16243 *
16244 * @chainable
16245 * @fires resize
16246 */
16247 OO.ui.TextInputWidget.prototype.adjustSize = function () {
16248 var scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError,
16249 idealHeight, newHeight, scrollWidth, property;
16250
16251 if ( this.multiline && this.$input.val() !== this.valCache ) {
16252 if ( this.autosize ) {
16253 this.$clone
16254 .val( this.$input.val() )
16255 .attr( 'rows', this.minRows )
16256 // Set inline height property to 0 to measure scroll height
16257 .css( 'height', 0 );
16258
16259 this.$clone.removeClass( 'oo-ui-element-hidden' );
16260
16261 this.valCache = this.$input.val();
16262
16263 scrollHeight = this.$clone[ 0 ].scrollHeight;
16264
16265 // Remove inline height property to measure natural heights
16266 this.$clone.css( 'height', '' );
16267 innerHeight = this.$clone.innerHeight();
16268 outerHeight = this.$clone.outerHeight();
16269
16270 // Measure max rows height
16271 this.$clone
16272 .attr( 'rows', this.maxRows )
16273 .css( 'height', 'auto' )
16274 .val( '' );
16275 maxInnerHeight = this.$clone.innerHeight();
16276
16277 // Difference between reported innerHeight and scrollHeight with no scrollbars present
16278 // Equals 1 on Blink-based browsers and 0 everywhere else
16279 measurementError = maxInnerHeight - this.$clone[ 0 ].scrollHeight;
16280 idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
16281
16282 this.$clone.addClass( 'oo-ui-element-hidden' );
16283
16284 // Only apply inline height when expansion beyond natural height is needed
16285 // Use the difference between the inner and outer height as a buffer
16286 newHeight = idealHeight > innerHeight ? idealHeight + ( outerHeight - innerHeight ) : '';
16287 if ( newHeight !== this.styleHeight ) {
16288 this.$input.css( 'height', newHeight );
16289 this.styleHeight = newHeight;
16290 this.emit( 'resize' );
16291 }
16292 }
16293 scrollWidth = this.$input[ 0 ].offsetWidth - this.$input[ 0 ].clientWidth;
16294 if ( scrollWidth !== this.scrollWidth ) {
16295 property = this.$element.css( 'direction' ) === 'rtl' ? 'left' : 'right';
16296 // Reset
16297 this.$label.css( { right: '', left: '' } );
16298 this.$indicator.css( { right: '', left: '' } );
16299
16300 if ( scrollWidth ) {
16301 this.$indicator.css( property, scrollWidth );
16302 if ( this.labelPosition === 'after' ) {
16303 this.$label.css( property, scrollWidth );
16304 }
16305 }
16306
16307 this.scrollWidth = scrollWidth;
16308 this.positionLabel();
16309 }
16310 }
16311 return this;
16312 };
16313
16314 /**
16315 * @inheritdoc
16316 * @protected
16317 */
16318 OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
16319 return config.multiline ?
16320 $( '<textarea>' ) :
16321 $( '<input type="' + this.getSaneType( config ) + '" />' );
16322 };
16323
16324 /**
16325 * Get sanitized value for 'type' for given config.
16326 *
16327 * @param {Object} config Configuration options
16328 * @return {string|null}
16329 * @private
16330 */
16331 OO.ui.TextInputWidget.prototype.getSaneType = function ( config ) {
16332 var type = [ 'text', 'password', 'search', 'email', 'url' ].indexOf( config.type ) !== -1 ?
16333 config.type :
16334 'text';
16335 return config.multiline ? 'multiline' : type;
16336 };
16337
16338 /**
16339 * Check if the input supports multiple lines.
16340 *
16341 * @return {boolean}
16342 */
16343 OO.ui.TextInputWidget.prototype.isMultiline = function () {
16344 return !!this.multiline;
16345 };
16346
16347 /**
16348 * Check if the input automatically adjusts its size.
16349 *
16350 * @return {boolean}
16351 */
16352 OO.ui.TextInputWidget.prototype.isAutosizing = function () {
16353 return !!this.autosize;
16354 };
16355
16356 /**
16357 * Focus the input and select a specified range within the text.
16358 *
16359 * @param {number} from Select from offset
16360 * @param {number} [to] Select to offset, defaults to from
16361 * @chainable
16362 */
16363 OO.ui.TextInputWidget.prototype.selectRange = function ( from, to ) {
16364 var isBackwards, start, end,
16365 input = this.$input[ 0 ];
16366
16367 to = to || from;
16368
16369 isBackwards = to < from;
16370 start = isBackwards ? to : from;
16371 end = isBackwards ? from : to;
16372
16373 this.focus();
16374
16375 input.setSelectionRange( start, end, isBackwards ? 'backward' : 'forward' );
16376 return this;
16377 };
16378
16379 /**
16380 * Get an object describing the current selection range in a directional manner
16381 *
16382 * @return {Object} Object containing 'from' and 'to' offsets
16383 */
16384 OO.ui.TextInputWidget.prototype.getRange = function () {
16385 var input = this.$input[ 0 ],
16386 start = input.selectionStart,
16387 end = input.selectionEnd,
16388 isBackwards = input.selectionDirection === 'backward';
16389
16390 return {
16391 from: isBackwards ? end : start,
16392 to: isBackwards ? start : end
16393 };
16394 };
16395
16396 /**
16397 * Get the length of the text input value.
16398 *
16399 * This could differ from the length of #getValue if the
16400 * value gets filtered
16401 *
16402 * @return {number} Input length
16403 */
16404 OO.ui.TextInputWidget.prototype.getInputLength = function () {
16405 return this.$input[ 0 ].value.length;
16406 };
16407
16408 /**
16409 * Focus the input and select the entire text.
16410 *
16411 * @chainable
16412 */
16413 OO.ui.TextInputWidget.prototype.select = function () {
16414 return this.selectRange( 0, this.getInputLength() );
16415 };
16416
16417 /**
16418 * Focus the input and move the cursor to the start.
16419 *
16420 * @chainable
16421 */
16422 OO.ui.TextInputWidget.prototype.moveCursorToStart = function () {
16423 return this.selectRange( 0 );
16424 };
16425
16426 /**
16427 * Focus the input and move the cursor to the end.
16428 *
16429 * @chainable
16430 */
16431 OO.ui.TextInputWidget.prototype.moveCursorToEnd = function () {
16432 return this.selectRange( this.getInputLength() );
16433 };
16434
16435 /**
16436 * Insert new content into the input.
16437 *
16438 * @param {string} content Content to be inserted
16439 * @chainable
16440 */
16441 OO.ui.TextInputWidget.prototype.insertContent = function ( content ) {
16442 var start, end,
16443 range = this.getRange(),
16444 value = this.getValue();
16445
16446 start = Math.min( range.from, range.to );
16447 end = Math.max( range.from, range.to );
16448
16449 this.setValue( value.slice( 0, start ) + content + value.slice( end ) );
16450 this.selectRange( start + content.length );
16451 return this;
16452 };
16453
16454 /**
16455 * Insert new content either side of a selection.
16456 *
16457 * @param {string} pre Content to be inserted before the selection
16458 * @param {string} post Content to be inserted after the selection
16459 * @chainable
16460 */
16461 OO.ui.TextInputWidget.prototype.encapsulateContent = function ( pre, post ) {
16462 var start, end,
16463 range = this.getRange(),
16464 offset = pre.length;
16465
16466 start = Math.min( range.from, range.to );
16467 end = Math.max( range.from, range.to );
16468
16469 this.selectRange( start ).insertContent( pre );
16470 this.selectRange( offset + end ).insertContent( post );
16471
16472 this.selectRange( offset + start, offset + end );
16473 return this;
16474 };
16475
16476 /**
16477 * Set the validation pattern.
16478 *
16479 * The validation pattern is either a regular expression, a function, or the symbolic name of a
16480 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
16481 * value must contain only numbers).
16482 *
16483 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
16484 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
16485 */
16486 OO.ui.TextInputWidget.prototype.setValidation = function ( validate ) {
16487 if ( validate instanceof RegExp || validate instanceof Function ) {
16488 this.validate = validate;
16489 } else {
16490 this.validate = this.constructor.static.validationPatterns[ validate ] || /.*/;
16491 }
16492 };
16493
16494 /**
16495 * Sets the 'invalid' flag appropriately.
16496 *
16497 * @param {boolean} [isValid] Optionally override validation result
16498 */
16499 OO.ui.TextInputWidget.prototype.setValidityFlag = function ( isValid ) {
16500 var widget = this,
16501 setFlag = function ( valid ) {
16502 if ( !valid ) {
16503 widget.$input.attr( 'aria-invalid', 'true' );
16504 } else {
16505 widget.$input.removeAttr( 'aria-invalid' );
16506 }
16507 widget.setFlags( { invalid: !valid } );
16508 };
16509
16510 if ( isValid !== undefined ) {
16511 setFlag( isValid );
16512 } else {
16513 this.getValidity().then( function () {
16514 setFlag( true );
16515 }, function () {
16516 setFlag( false );
16517 } );
16518 }
16519 };
16520
16521 /**
16522 * Check if a value is valid.
16523 *
16524 * This method returns a promise that resolves with a boolean `true` if the current value is
16525 * considered valid according to the supplied {@link #validate validation pattern}.
16526 *
16527 * @deprecated
16528 * @return {jQuery.Promise} A promise that resolves to a boolean `true` if the value is valid.
16529 */
16530 OO.ui.TextInputWidget.prototype.isValid = function () {
16531 var result;
16532
16533 if ( this.validate instanceof Function ) {
16534 result = this.validate( this.getValue() );
16535 if ( result && $.isFunction( result.promise ) ) {
16536 return result.promise();
16537 } else {
16538 return $.Deferred().resolve( !!result ).promise();
16539 }
16540 } else {
16541 return $.Deferred().resolve( !!this.getValue().match( this.validate ) ).promise();
16542 }
16543 };
16544
16545 /**
16546 * Get the validity of current value.
16547 *
16548 * This method returns a promise that resolves if the value is valid and rejects if
16549 * it isn't. Uses the {@link #validate validation pattern} to check for validity.
16550 *
16551 * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
16552 */
16553 OO.ui.TextInputWidget.prototype.getValidity = function () {
16554 var result;
16555
16556 function rejectOrResolve( valid ) {
16557 if ( valid ) {
16558 return $.Deferred().resolve().promise();
16559 } else {
16560 return $.Deferred().reject().promise();
16561 }
16562 }
16563
16564 if ( this.validate instanceof Function ) {
16565 result = this.validate( this.getValue() );
16566 if ( result && $.isFunction( result.promise ) ) {
16567 return result.promise().then( function ( valid ) {
16568 return rejectOrResolve( valid );
16569 } );
16570 } else {
16571 return rejectOrResolve( result );
16572 }
16573 } else {
16574 return rejectOrResolve( this.getValue().match( this.validate ) );
16575 }
16576 };
16577
16578 /**
16579 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
16580 *
16581 * @param {string} labelPosition Label position, 'before' or 'after'
16582 * @chainable
16583 */
16584 OO.ui.TextInputWidget.prototype.setLabelPosition = function ( labelPosition ) {
16585 this.labelPosition = labelPosition;
16586 this.updatePosition();
16587 return this;
16588 };
16589
16590 /**
16591 * Update the position of the inline label.
16592 *
16593 * This method is called by #setLabelPosition, and can also be called on its own if
16594 * something causes the label to be mispositioned.
16595 *
16596 * @chainable
16597 */
16598 OO.ui.TextInputWidget.prototype.updatePosition = function () {
16599 var after = this.labelPosition === 'after';
16600
16601 this.$element
16602 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label && after )
16603 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label && !after );
16604
16605 this.valCache = null;
16606 this.scrollWidth = null;
16607 this.adjustSize();
16608 this.positionLabel();
16609
16610 return this;
16611 };
16612
16613 /**
16614 * Update the 'clear' indicator displayed on type: 'search' text fields, hiding it when the field is
16615 * already empty or when it's not editable.
16616 */
16617 OO.ui.TextInputWidget.prototype.updateSearchIndicator = function () {
16618 if ( this.type === 'search' ) {
16619 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
16620 this.setIndicator( null );
16621 } else {
16622 this.setIndicator( 'clear' );
16623 }
16624 }
16625 };
16626
16627 /**
16628 * Position the label by setting the correct padding on the input.
16629 *
16630 * @private
16631 * @chainable
16632 */
16633 OO.ui.TextInputWidget.prototype.positionLabel = function () {
16634 var after, rtl, property;
16635 // Clear old values
16636 this.$input
16637 // Clear old values if present
16638 .css( {
16639 'padding-right': '',
16640 'padding-left': ''
16641 } );
16642
16643 if ( this.label ) {
16644 this.$element.append( this.$label );
16645 } else {
16646 this.$label.detach();
16647 return;
16648 }
16649
16650 after = this.labelPosition === 'after';
16651 rtl = this.$element.css( 'direction' ) === 'rtl';
16652 property = after === rtl ? 'padding-left' : 'padding-right';
16653
16654 this.$input.css( property, this.$label.outerWidth( true ) + ( after ? this.scrollWidth : 0 ) );
16655
16656 return this;
16657 };
16658
16659 /**
16660 * @inheritdoc
16661 */
16662 OO.ui.TextInputWidget.prototype.restorePreInfuseState = function ( state ) {
16663 OO.ui.TextInputWidget.parent.prototype.restorePreInfuseState.call( this, state );
16664 if ( state.scrollTop !== undefined ) {
16665 this.$input.scrollTop( state.scrollTop );
16666 }
16667 };
16668
16669 /**
16670 * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
16671 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
16672 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
16673 *
16674 * - by typing a value in the text input field. If the value exactly matches the value of a menu
16675 * option, that option will appear to be selected.
16676 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
16677 * input field.
16678 *
16679 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
16680 *
16681 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
16682 *
16683 * @example
16684 * // Example: A ComboBoxInputWidget.
16685 * var comboBox = new OO.ui.ComboBoxInputWidget( {
16686 * label: 'ComboBoxInputWidget',
16687 * value: 'Option 1',
16688 * menu: {
16689 * items: [
16690 * new OO.ui.MenuOptionWidget( {
16691 * data: 'Option 1',
16692 * label: 'Option One'
16693 * } ),
16694 * new OO.ui.MenuOptionWidget( {
16695 * data: 'Option 2',
16696 * label: 'Option Two'
16697 * } ),
16698 * new OO.ui.MenuOptionWidget( {
16699 * data: 'Option 3',
16700 * label: 'Option Three'
16701 * } ),
16702 * new OO.ui.MenuOptionWidget( {
16703 * data: 'Option 4',
16704 * label: 'Option Four'
16705 * } ),
16706 * new OO.ui.MenuOptionWidget( {
16707 * data: 'Option 5',
16708 * label: 'Option Five'
16709 * } )
16710 * ]
16711 * }
16712 * } );
16713 * $( 'body' ).append( comboBox.$element );
16714 *
16715 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
16716 *
16717 * @class
16718 * @extends OO.ui.TextInputWidget
16719 *
16720 * @constructor
16721 * @param {Object} [config] Configuration options
16722 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
16723 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.FloatingMenuSelectWidget menu select widget}.
16724 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
16725 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
16726 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
16727 */
16728 OO.ui.ComboBoxInputWidget = function OoUiComboBoxInputWidget( config ) {
16729 // Configuration initialization
16730 config = $.extend( {
16731 indicator: 'down'
16732 }, config );
16733 // For backwards-compatibility with ComboBoxWidget config
16734 $.extend( config, config.input );
16735
16736 // Parent constructor
16737 OO.ui.ComboBoxInputWidget.parent.call( this, config );
16738
16739 // Properties
16740 this.$overlay = config.$overlay || this.$element;
16741 this.menu = new OO.ui.FloatingMenuSelectWidget( $.extend(
16742 {
16743 widget: this,
16744 input: this,
16745 $container: this.$element,
16746 disabled: this.isDisabled()
16747 },
16748 config.menu
16749 ) );
16750 // For backwards-compatibility with ComboBoxWidget
16751 this.input = this;
16752
16753 // Events
16754 this.$indicator.on( {
16755 click: this.onIndicatorClick.bind( this ),
16756 keypress: this.onIndicatorKeyPress.bind( this )
16757 } );
16758 this.connect( this, {
16759 change: 'onInputChange',
16760 enter: 'onInputEnter'
16761 } );
16762 this.menu.connect( this, {
16763 choose: 'onMenuChoose',
16764 add: 'onMenuItemsChange',
16765 remove: 'onMenuItemsChange'
16766 } );
16767
16768 // Initialization
16769 this.$input.attr( {
16770 role: 'combobox',
16771 'aria-autocomplete': 'list'
16772 } );
16773 // Do not override options set via config.menu.items
16774 if ( config.options !== undefined ) {
16775 this.setOptions( config.options );
16776 }
16777 // Extra class for backwards-compatibility with ComboBoxWidget
16778 this.$element.addClass( 'oo-ui-comboBoxInputWidget oo-ui-comboBoxWidget' );
16779 this.$overlay.append( this.menu.$element );
16780 this.onMenuItemsChange();
16781 };
16782
16783 /* Setup */
16784
16785 OO.inheritClass( OO.ui.ComboBoxInputWidget, OO.ui.TextInputWidget );
16786
16787 /* Methods */
16788
16789 /**
16790 * Get the combobox's menu.
16791 * @return {OO.ui.FloatingMenuSelectWidget} Menu widget
16792 */
16793 OO.ui.ComboBoxInputWidget.prototype.getMenu = function () {
16794 return this.menu;
16795 };
16796
16797 /**
16798 * Get the combobox's text input widget.
16799 * @return {OO.ui.TextInputWidget} Text input widget
16800 */
16801 OO.ui.ComboBoxInputWidget.prototype.getInput = function () {
16802 return this;
16803 };
16804
16805 /**
16806 * Handle input change events.
16807 *
16808 * @private
16809 * @param {string} value New value
16810 */
16811 OO.ui.ComboBoxInputWidget.prototype.onInputChange = function ( value ) {
16812 var match = this.menu.getItemFromData( value );
16813
16814 this.menu.selectItem( match );
16815 if ( this.menu.getHighlightedItem() ) {
16816 this.menu.highlightItem( match );
16817 }
16818
16819 if ( !this.isDisabled() ) {
16820 this.menu.toggle( true );
16821 }
16822 };
16823
16824 /**
16825 * Handle mouse click events.
16826 *
16827 * @private
16828 * @param {jQuery.Event} e Mouse click event
16829 */
16830 OO.ui.ComboBoxInputWidget.prototype.onIndicatorClick = function ( e ) {
16831 if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
16832 this.menu.toggle();
16833 this.$input[ 0 ].focus();
16834 }
16835 return false;
16836 };
16837
16838 /**
16839 * Handle key press events.
16840 *
16841 * @private
16842 * @param {jQuery.Event} e Key press event
16843 */
16844 OO.ui.ComboBoxInputWidget.prototype.onIndicatorKeyPress = function ( e ) {
16845 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
16846 this.menu.toggle();
16847 this.$input[ 0 ].focus();
16848 return false;
16849 }
16850 };
16851
16852 /**
16853 * Handle input enter events.
16854 *
16855 * @private
16856 */
16857 OO.ui.ComboBoxInputWidget.prototype.onInputEnter = function () {
16858 if ( !this.isDisabled() ) {
16859 this.menu.toggle( false );
16860 }
16861 };
16862
16863 /**
16864 * Handle menu choose events.
16865 *
16866 * @private
16867 * @param {OO.ui.OptionWidget} item Chosen item
16868 */
16869 OO.ui.ComboBoxInputWidget.prototype.onMenuChoose = function ( item ) {
16870 this.setValue( item.getData() );
16871 };
16872
16873 /**
16874 * Handle menu item change events.
16875 *
16876 * @private
16877 */
16878 OO.ui.ComboBoxInputWidget.prototype.onMenuItemsChange = function () {
16879 var match = this.menu.getItemFromData( this.getValue() );
16880 this.menu.selectItem( match );
16881 if ( this.menu.getHighlightedItem() ) {
16882 this.menu.highlightItem( match );
16883 }
16884 this.$element.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu.isEmpty() );
16885 };
16886
16887 /**
16888 * @inheritdoc
16889 */
16890 OO.ui.ComboBoxInputWidget.prototype.setDisabled = function ( disabled ) {
16891 // Parent method
16892 OO.ui.ComboBoxInputWidget.parent.prototype.setDisabled.call( this, disabled );
16893
16894 if ( this.menu ) {
16895 this.menu.setDisabled( this.isDisabled() );
16896 }
16897
16898 return this;
16899 };
16900
16901 /**
16902 * Set the options available for this input.
16903 *
16904 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
16905 * @chainable
16906 */
16907 OO.ui.ComboBoxInputWidget.prototype.setOptions = function ( options ) {
16908 this.getMenu()
16909 .clearItems()
16910 .addItems( options.map( function ( opt ) {
16911 return new OO.ui.MenuOptionWidget( {
16912 data: opt.data,
16913 label: opt.label !== undefined ? opt.label : opt.data
16914 } );
16915 } ) );
16916
16917 return this;
16918 };
16919
16920 /**
16921 * @class
16922 * @deprecated Use OO.ui.ComboBoxInputWidget instead.
16923 */
16924 OO.ui.ComboBoxWidget = OO.ui.ComboBoxInputWidget;
16925
16926 /**
16927 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
16928 * be configured with a `label` option that is set to a string, a label node, or a function:
16929 *
16930 * - String: a plaintext string
16931 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
16932 * label that includes a link or special styling, such as a gray color or additional graphical elements.
16933 * - Function: a function that will produce a string in the future. Functions are used
16934 * in cases where the value of the label is not currently defined.
16935 *
16936 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
16937 * will come into focus when the label is clicked.
16938 *
16939 * @example
16940 * // Examples of LabelWidgets
16941 * var label1 = new OO.ui.LabelWidget( {
16942 * label: 'plaintext label'
16943 * } );
16944 * var label2 = new OO.ui.LabelWidget( {
16945 * label: $( '<a href="default.html">jQuery label</a>' )
16946 * } );
16947 * // Create a fieldset layout with fields for each example
16948 * var fieldset = new OO.ui.FieldsetLayout();
16949 * fieldset.addItems( [
16950 * new OO.ui.FieldLayout( label1 ),
16951 * new OO.ui.FieldLayout( label2 )
16952 * ] );
16953 * $( 'body' ).append( fieldset.$element );
16954 *
16955 * @class
16956 * @extends OO.ui.Widget
16957 * @mixins OO.ui.mixin.LabelElement
16958 *
16959 * @constructor
16960 * @param {Object} [config] Configuration options
16961 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
16962 * Clicking the label will focus the specified input field.
16963 */
16964 OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
16965 // Configuration initialization
16966 config = config || {};
16967
16968 // Parent constructor
16969 OO.ui.LabelWidget.parent.call( this, config );
16970
16971 // Mixin constructors
16972 OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, { $label: this.$element } ) );
16973 OO.ui.mixin.TitledElement.call( this, config );
16974
16975 // Properties
16976 this.input = config.input;
16977
16978 // Events
16979 if ( this.input instanceof OO.ui.InputWidget ) {
16980 this.$element.on( 'click', this.onClick.bind( this ) );
16981 }
16982
16983 // Initialization
16984 this.$element.addClass( 'oo-ui-labelWidget' );
16985 };
16986
16987 /* Setup */
16988
16989 OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
16990 OO.mixinClass( OO.ui.LabelWidget, OO.ui.mixin.LabelElement );
16991 OO.mixinClass( OO.ui.LabelWidget, OO.ui.mixin.TitledElement );
16992
16993 /* Static Properties */
16994
16995 OO.ui.LabelWidget.static.tagName = 'span';
16996
16997 /* Methods */
16998
16999 /**
17000 * Handles label mouse click events.
17001 *
17002 * @private
17003 * @param {jQuery.Event} e Mouse click event
17004 */
17005 OO.ui.LabelWidget.prototype.onClick = function () {
17006 this.input.simulateLabelClick();
17007 return false;
17008 };
17009
17010 /**
17011 * OptionWidgets are special elements that can be selected and configured with data. The
17012 * data is often unique for each option, but it does not have to be. OptionWidgets are used
17013 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
17014 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
17015 *
17016 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
17017 *
17018 * @class
17019 * @extends OO.ui.Widget
17020 * @mixins OO.ui.mixin.LabelElement
17021 * @mixins OO.ui.mixin.FlaggedElement
17022 *
17023 * @constructor
17024 * @param {Object} [config] Configuration options
17025 */
17026 OO.ui.OptionWidget = function OoUiOptionWidget( config ) {
17027 // Configuration initialization
17028 config = config || {};
17029
17030 // Parent constructor
17031 OO.ui.OptionWidget.parent.call( this, config );
17032
17033 // Mixin constructors
17034 OO.ui.mixin.ItemWidget.call( this );
17035 OO.ui.mixin.LabelElement.call( this, config );
17036 OO.ui.mixin.FlaggedElement.call( this, config );
17037
17038 // Properties
17039 this.selected = false;
17040 this.highlighted = false;
17041 this.pressed = false;
17042
17043 // Initialization
17044 this.$element
17045 .data( 'oo-ui-optionWidget', this )
17046 .attr( 'role', 'option' )
17047 .attr( 'aria-selected', 'false' )
17048 .addClass( 'oo-ui-optionWidget' )
17049 .append( this.$label );
17050 };
17051
17052 /* Setup */
17053
17054 OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
17055 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.ItemWidget );
17056 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.LabelElement );
17057 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.FlaggedElement );
17058
17059 /* Static Properties */
17060
17061 OO.ui.OptionWidget.static.selectable = true;
17062
17063 OO.ui.OptionWidget.static.highlightable = true;
17064
17065 OO.ui.OptionWidget.static.pressable = true;
17066
17067 OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
17068
17069 /* Methods */
17070
17071 /**
17072 * Check if the option can be selected.
17073 *
17074 * @return {boolean} Item is selectable
17075 */
17076 OO.ui.OptionWidget.prototype.isSelectable = function () {
17077 return this.constructor.static.selectable && !this.isDisabled() && this.isVisible();
17078 };
17079
17080 /**
17081 * Check if the option can be highlighted. A highlight indicates that the option
17082 * may be selected when a user presses enter or clicks. Disabled items cannot
17083 * be highlighted.
17084 *
17085 * @return {boolean} Item is highlightable
17086 */
17087 OO.ui.OptionWidget.prototype.isHighlightable = function () {
17088 return this.constructor.static.highlightable && !this.isDisabled() && this.isVisible();
17089 };
17090
17091 /**
17092 * Check if the option can be pressed. The pressed state occurs when a user mouses
17093 * down on an item, but has not yet let go of the mouse.
17094 *
17095 * @return {boolean} Item is pressable
17096 */
17097 OO.ui.OptionWidget.prototype.isPressable = function () {
17098 return this.constructor.static.pressable && !this.isDisabled() && this.isVisible();
17099 };
17100
17101 /**
17102 * Check if the option is selected.
17103 *
17104 * @return {boolean} Item is selected
17105 */
17106 OO.ui.OptionWidget.prototype.isSelected = function () {
17107 return this.selected;
17108 };
17109
17110 /**
17111 * Check if the option is highlighted. A highlight indicates that the
17112 * item may be selected when a user presses enter or clicks.
17113 *
17114 * @return {boolean} Item is highlighted
17115 */
17116 OO.ui.OptionWidget.prototype.isHighlighted = function () {
17117 return this.highlighted;
17118 };
17119
17120 /**
17121 * Check if the option is pressed. The pressed state occurs when a user mouses
17122 * down on an item, but has not yet let go of the mouse. The item may appear
17123 * selected, but it will not be selected until the user releases the mouse.
17124 *
17125 * @return {boolean} Item is pressed
17126 */
17127 OO.ui.OptionWidget.prototype.isPressed = function () {
17128 return this.pressed;
17129 };
17130
17131 /**
17132 * Set the option’s selected state. In general, all modifications to the selection
17133 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
17134 * method instead of this method.
17135 *
17136 * @param {boolean} [state=false] Select option
17137 * @chainable
17138 */
17139 OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
17140 if ( this.constructor.static.selectable ) {
17141 this.selected = !!state;
17142 this.$element
17143 .toggleClass( 'oo-ui-optionWidget-selected', state )
17144 .attr( 'aria-selected', state.toString() );
17145 if ( state && this.constructor.static.scrollIntoViewOnSelect ) {
17146 this.scrollElementIntoView();
17147 }
17148 this.updateThemeClasses();
17149 }
17150 return this;
17151 };
17152
17153 /**
17154 * Set the option’s highlighted state. In general, all programmatic
17155 * modifications to the highlight should be handled by the
17156 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
17157 * method instead of this method.
17158 *
17159 * @param {boolean} [state=false] Highlight option
17160 * @chainable
17161 */
17162 OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
17163 if ( this.constructor.static.highlightable ) {
17164 this.highlighted = !!state;
17165 this.$element.toggleClass( 'oo-ui-optionWidget-highlighted', state );
17166 this.updateThemeClasses();
17167 }
17168 return this;
17169 };
17170
17171 /**
17172 * Set the option’s pressed state. In general, all
17173 * programmatic modifications to the pressed state should be handled by the
17174 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
17175 * method instead of this method.
17176 *
17177 * @param {boolean} [state=false] Press option
17178 * @chainable
17179 */
17180 OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
17181 if ( this.constructor.static.pressable ) {
17182 this.pressed = !!state;
17183 this.$element.toggleClass( 'oo-ui-optionWidget-pressed', state );
17184 this.updateThemeClasses();
17185 }
17186 return this;
17187 };
17188
17189 /**
17190 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
17191 * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
17192 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
17193 * options. For more information about options and selects, please see the
17194 * [OOjs UI documentation on MediaWiki][1].
17195 *
17196 * @example
17197 * // Decorated options in a select widget
17198 * var select = new OO.ui.SelectWidget( {
17199 * items: [
17200 * new OO.ui.DecoratedOptionWidget( {
17201 * data: 'a',
17202 * label: 'Option with icon',
17203 * icon: 'help'
17204 * } ),
17205 * new OO.ui.DecoratedOptionWidget( {
17206 * data: 'b',
17207 * label: 'Option with indicator',
17208 * indicator: 'next'
17209 * } )
17210 * ]
17211 * } );
17212 * $( 'body' ).append( select.$element );
17213 *
17214 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
17215 *
17216 * @class
17217 * @extends OO.ui.OptionWidget
17218 * @mixins OO.ui.mixin.IconElement
17219 * @mixins OO.ui.mixin.IndicatorElement
17220 *
17221 * @constructor
17222 * @param {Object} [config] Configuration options
17223 */
17224 OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( config ) {
17225 // Parent constructor
17226 OO.ui.DecoratedOptionWidget.parent.call( this, config );
17227
17228 // Mixin constructors
17229 OO.ui.mixin.IconElement.call( this, config );
17230 OO.ui.mixin.IndicatorElement.call( this, config );
17231
17232 // Initialization
17233 this.$element
17234 .addClass( 'oo-ui-decoratedOptionWidget' )
17235 .prepend( this.$icon )
17236 .append( this.$indicator );
17237 };
17238
17239 /* Setup */
17240
17241 OO.inheritClass( OO.ui.DecoratedOptionWidget, OO.ui.OptionWidget );
17242 OO.mixinClass( OO.ui.DecoratedOptionWidget, OO.ui.mixin.IconElement );
17243 OO.mixinClass( OO.ui.DecoratedOptionWidget, OO.ui.mixin.IndicatorElement );
17244
17245 /**
17246 * ButtonOptionWidget is a special type of {@link OO.ui.mixin.ButtonElement button element} that
17247 * can be selected and configured with data. The class is
17248 * used with OO.ui.ButtonSelectWidget to create a selection of button options. Please see the
17249 * [OOjs UI documentation on MediaWiki] [1] for more information.
17250 *
17251 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_options
17252 *
17253 * @class
17254 * @extends OO.ui.DecoratedOptionWidget
17255 * @mixins OO.ui.mixin.ButtonElement
17256 * @mixins OO.ui.mixin.TabIndexedElement
17257 * @mixins OO.ui.mixin.TitledElement
17258 *
17259 * @constructor
17260 * @param {Object} [config] Configuration options
17261 */
17262 OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( config ) {
17263 // Configuration initialization
17264 config = config || {};
17265
17266 // Parent constructor
17267 OO.ui.ButtonOptionWidget.parent.call( this, config );
17268
17269 // Mixin constructors
17270 OO.ui.mixin.ButtonElement.call( this, config );
17271 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
17272 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, {
17273 $tabIndexed: this.$button,
17274 tabIndex: -1
17275 } ) );
17276
17277 // Initialization
17278 this.$element.addClass( 'oo-ui-buttonOptionWidget' );
17279 this.$button.append( this.$element.contents() );
17280 this.$element.append( this.$button );
17281 };
17282
17283 /* Setup */
17284
17285 OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.DecoratedOptionWidget );
17286 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.mixin.ButtonElement );
17287 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.mixin.TitledElement );
17288 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.mixin.TabIndexedElement );
17289
17290 /* Static Properties */
17291
17292 // Allow button mouse down events to pass through so they can be handled by the parent select widget
17293 OO.ui.ButtonOptionWidget.static.cancelButtonMouseDownEvents = false;
17294
17295 OO.ui.ButtonOptionWidget.static.highlightable = false;
17296
17297 /* Methods */
17298
17299 /**
17300 * @inheritdoc
17301 */
17302 OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) {
17303 OO.ui.ButtonOptionWidget.parent.prototype.setSelected.call( this, state );
17304
17305 if ( this.constructor.static.selectable ) {
17306 this.setActive( state );
17307 }
17308
17309 return this;
17310 };
17311
17312 /**
17313 * RadioOptionWidget is an option widget that looks like a radio button.
17314 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
17315 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
17316 *
17317 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
17318 *
17319 * @class
17320 * @extends OO.ui.OptionWidget
17321 *
17322 * @constructor
17323 * @param {Object} [config] Configuration options
17324 */
17325 OO.ui.RadioOptionWidget = function OoUiRadioOptionWidget( config ) {
17326 // Configuration initialization
17327 config = config || {};
17328
17329 // Properties (must be done before parent constructor which calls #setDisabled)
17330 this.radio = new OO.ui.RadioInputWidget( { value: config.data, tabIndex: -1 } );
17331
17332 // Parent constructor
17333 OO.ui.RadioOptionWidget.parent.call( this, config );
17334
17335 // Events
17336 this.radio.$input.on( 'focus', this.onInputFocus.bind( this ) );
17337
17338 // Initialization
17339 // Remove implicit role, we're handling it ourselves
17340 this.radio.$input.attr( 'role', 'presentation' );
17341 this.$element
17342 .addClass( 'oo-ui-radioOptionWidget' )
17343 .attr( 'role', 'radio' )
17344 .attr( 'aria-checked', 'false' )
17345 .removeAttr( 'aria-selected' )
17346 .prepend( this.radio.$element );
17347 };
17348
17349 /* Setup */
17350
17351 OO.inheritClass( OO.ui.RadioOptionWidget, OO.ui.OptionWidget );
17352
17353 /* Static Properties */
17354
17355 OO.ui.RadioOptionWidget.static.highlightable = false;
17356
17357 OO.ui.RadioOptionWidget.static.scrollIntoViewOnSelect = true;
17358
17359 OO.ui.RadioOptionWidget.static.pressable = false;
17360
17361 OO.ui.RadioOptionWidget.static.tagName = 'label';
17362
17363 /* Methods */
17364
17365 /**
17366 * @param {jQuery.Event} e Focus event
17367 * @private
17368 */
17369 OO.ui.RadioOptionWidget.prototype.onInputFocus = function () {
17370 this.radio.$input.blur();
17371 this.$element.parent().focus();
17372 };
17373
17374 /**
17375 * @inheritdoc
17376 */
17377 OO.ui.RadioOptionWidget.prototype.setSelected = function ( state ) {
17378 OO.ui.RadioOptionWidget.parent.prototype.setSelected.call( this, state );
17379
17380 this.radio.setSelected( state );
17381 this.$element
17382 .attr( 'aria-checked', state.toString() )
17383 .removeAttr( 'aria-selected' );
17384
17385 return this;
17386 };
17387
17388 /**
17389 * @inheritdoc
17390 */
17391 OO.ui.RadioOptionWidget.prototype.setDisabled = function ( disabled ) {
17392 OO.ui.RadioOptionWidget.parent.prototype.setDisabled.call( this, disabled );
17393
17394 this.radio.setDisabled( this.isDisabled() );
17395
17396 return this;
17397 };
17398
17399 /**
17400 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
17401 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
17402 * the [OOjs UI documentation on MediaWiki] [1] for more information.
17403 *
17404 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
17405 *
17406 * @class
17407 * @extends OO.ui.DecoratedOptionWidget
17408 *
17409 * @constructor
17410 * @param {Object} [config] Configuration options
17411 */
17412 OO.ui.MenuOptionWidget = function OoUiMenuOptionWidget( config ) {
17413 // Configuration initialization
17414 config = $.extend( { icon: 'check' }, config );
17415
17416 // Parent constructor
17417 OO.ui.MenuOptionWidget.parent.call( this, config );
17418
17419 // Initialization
17420 this.$element
17421 .attr( 'role', 'menuitem' )
17422 .addClass( 'oo-ui-menuOptionWidget' );
17423 };
17424
17425 /* Setup */
17426
17427 OO.inheritClass( OO.ui.MenuOptionWidget, OO.ui.DecoratedOptionWidget );
17428
17429 /* Static Properties */
17430
17431 OO.ui.MenuOptionWidget.static.scrollIntoViewOnSelect = true;
17432
17433 /**
17434 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
17435 * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
17436 *
17437 * @example
17438 * var myDropdown = new OO.ui.DropdownWidget( {
17439 * menu: {
17440 * items: [
17441 * new OO.ui.MenuSectionOptionWidget( {
17442 * label: 'Dogs'
17443 * } ),
17444 * new OO.ui.MenuOptionWidget( {
17445 * data: 'corgi',
17446 * label: 'Welsh Corgi'
17447 * } ),
17448 * new OO.ui.MenuOptionWidget( {
17449 * data: 'poodle',
17450 * label: 'Standard Poodle'
17451 * } ),
17452 * new OO.ui.MenuSectionOptionWidget( {
17453 * label: 'Cats'
17454 * } ),
17455 * new OO.ui.MenuOptionWidget( {
17456 * data: 'lion',
17457 * label: 'Lion'
17458 * } )
17459 * ]
17460 * }
17461 * } );
17462 * $( 'body' ).append( myDropdown.$element );
17463 *
17464 * @class
17465 * @extends OO.ui.DecoratedOptionWidget
17466 *
17467 * @constructor
17468 * @param {Object} [config] Configuration options
17469 */
17470 OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( config ) {
17471 // Parent constructor
17472 OO.ui.MenuSectionOptionWidget.parent.call( this, config );
17473
17474 // Initialization
17475 this.$element.addClass( 'oo-ui-menuSectionOptionWidget' );
17476 };
17477
17478 /* Setup */
17479
17480 OO.inheritClass( OO.ui.MenuSectionOptionWidget, OO.ui.DecoratedOptionWidget );
17481
17482 /* Static Properties */
17483
17484 OO.ui.MenuSectionOptionWidget.static.selectable = false;
17485
17486 OO.ui.MenuSectionOptionWidget.static.highlightable = false;
17487
17488 /**
17489 * OutlineOptionWidget is an item in an {@link OO.ui.OutlineSelectWidget OutlineSelectWidget}.
17490 *
17491 * Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}, which contain
17492 * {@link OO.ui.PageLayout page layouts}. See {@link OO.ui.BookletLayout BookletLayout}
17493 * for an example.
17494 *
17495 * @class
17496 * @extends OO.ui.DecoratedOptionWidget
17497 *
17498 * @constructor
17499 * @param {Object} [config] Configuration options
17500 * @cfg {number} [level] Indentation level
17501 * @cfg {boolean} [movable] Allow modification from {@link OO.ui.OutlineControlsWidget outline controls}.
17502 */
17503 OO.ui.OutlineOptionWidget = function OoUiOutlineOptionWidget( config ) {
17504 // Configuration initialization
17505 config = config || {};
17506
17507 // Parent constructor
17508 OO.ui.OutlineOptionWidget.parent.call( this, config );
17509
17510 // Properties
17511 this.level = 0;
17512 this.movable = !!config.movable;
17513 this.removable = !!config.removable;
17514
17515 // Initialization
17516 this.$element.addClass( 'oo-ui-outlineOptionWidget' );
17517 this.setLevel( config.level );
17518 };
17519
17520 /* Setup */
17521
17522 OO.inheritClass( OO.ui.OutlineOptionWidget, OO.ui.DecoratedOptionWidget );
17523
17524 /* Static Properties */
17525
17526 OO.ui.OutlineOptionWidget.static.highlightable = false;
17527
17528 OO.ui.OutlineOptionWidget.static.scrollIntoViewOnSelect = true;
17529
17530 OO.ui.OutlineOptionWidget.static.levelClass = 'oo-ui-outlineOptionWidget-level-';
17531
17532 OO.ui.OutlineOptionWidget.static.levels = 3;
17533
17534 /* Methods */
17535
17536 /**
17537 * Check if item is movable.
17538 *
17539 * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
17540 *
17541 * @return {boolean} Item is movable
17542 */
17543 OO.ui.OutlineOptionWidget.prototype.isMovable = function () {
17544 return this.movable;
17545 };
17546
17547 /**
17548 * Check if item is removable.
17549 *
17550 * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
17551 *
17552 * @return {boolean} Item is removable
17553 */
17554 OO.ui.OutlineOptionWidget.prototype.isRemovable = function () {
17555 return this.removable;
17556 };
17557
17558 /**
17559 * Get indentation level.
17560 *
17561 * @return {number} Indentation level
17562 */
17563 OO.ui.OutlineOptionWidget.prototype.getLevel = function () {
17564 return this.level;
17565 };
17566
17567 /**
17568 * Set movability.
17569 *
17570 * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
17571 *
17572 * @param {boolean} movable Item is movable
17573 * @chainable
17574 */
17575 OO.ui.OutlineOptionWidget.prototype.setMovable = function ( movable ) {
17576 this.movable = !!movable;
17577 this.updateThemeClasses();
17578 return this;
17579 };
17580
17581 /**
17582 * Set removability.
17583 *
17584 * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
17585 *
17586 * @param {boolean} removable Item is removable
17587 * @chainable
17588 */
17589 OO.ui.OutlineOptionWidget.prototype.setRemovable = function ( removable ) {
17590 this.removable = !!removable;
17591 this.updateThemeClasses();
17592 return this;
17593 };
17594
17595 /**
17596 * Set indentation level.
17597 *
17598 * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
17599 * @chainable
17600 */
17601 OO.ui.OutlineOptionWidget.prototype.setLevel = function ( level ) {
17602 var levels = this.constructor.static.levels,
17603 levelClass = this.constructor.static.levelClass,
17604 i = levels;
17605
17606 this.level = level ? Math.max( 0, Math.min( levels - 1, level ) ) : 0;
17607 while ( i-- ) {
17608 if ( this.level === i ) {
17609 this.$element.addClass( levelClass + i );
17610 } else {
17611 this.$element.removeClass( levelClass + i );
17612 }
17613 }
17614 this.updateThemeClasses();
17615
17616 return this;
17617 };
17618
17619 /**
17620 * TabOptionWidget is an item in a {@link OO.ui.TabSelectWidget TabSelectWidget}.
17621 *
17622 * Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}, which contain
17623 * {@link OO.ui.CardLayout card layouts}. See {@link OO.ui.IndexLayout IndexLayout}
17624 * for an example.
17625 *
17626 * @class
17627 * @extends OO.ui.OptionWidget
17628 *
17629 * @constructor
17630 * @param {Object} [config] Configuration options
17631 */
17632 OO.ui.TabOptionWidget = function OoUiTabOptionWidget( config ) {
17633 // Configuration initialization
17634 config = config || {};
17635
17636 // Parent constructor
17637 OO.ui.TabOptionWidget.parent.call( this, config );
17638
17639 // Initialization
17640 this.$element.addClass( 'oo-ui-tabOptionWidget' );
17641 };
17642
17643 /* Setup */
17644
17645 OO.inheritClass( OO.ui.TabOptionWidget, OO.ui.OptionWidget );
17646
17647 /* Static Properties */
17648
17649 OO.ui.TabOptionWidget.static.highlightable = false;
17650
17651 /**
17652 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
17653 * By default, each popup has an anchor that points toward its origin.
17654 * Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples.
17655 *
17656 * @example
17657 * // A popup widget.
17658 * var popup = new OO.ui.PopupWidget( {
17659 * $content: $( '<p>Hi there!</p>' ),
17660 * padded: true,
17661 * width: 300
17662 * } );
17663 *
17664 * $( 'body' ).append( popup.$element );
17665 * // To display the popup, toggle the visibility to 'true'.
17666 * popup.toggle( true );
17667 *
17668 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups
17669 *
17670 * @class
17671 * @extends OO.ui.Widget
17672 * @mixins OO.ui.mixin.LabelElement
17673 * @mixins OO.ui.mixin.ClippableElement
17674 *
17675 * @constructor
17676 * @param {Object} [config] Configuration options
17677 * @cfg {number} [width=320] Width of popup in pixels
17678 * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
17679 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
17680 * @cfg {string} [align='center'] Alignment of the popup: `center`, `force-left`, `force-right`, `backwards` or `forwards`.
17681 * If the popup is forced-left the popup body is leaning towards the left. For force-right alignment, the body of the
17682 * popup is leaning towards the right of the screen.
17683 * Using 'backwards' is a logical direction which will result in the popup leaning towards the beginning of the sentence
17684 * in the given language, which means it will flip to the correct positioning in right-to-left languages.
17685 * Using 'forward' will also result in a logical alignment where the body of the popup leans towards the end of the
17686 * sentence in the given language.
17687 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
17688 * See the [OOjs UI docs on MediaWiki][3] for an example.
17689 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
17690 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
17691 * @cfg {jQuery} [$content] Content to append to the popup's body
17692 * @cfg {jQuery} [$footer] Content to append to the popup's footer
17693 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
17694 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
17695 * This config option is only relevant if #autoClose is set to `true`. See the [OOjs UI docs on MediaWiki][2]
17696 * for an example.
17697 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#autocloseExample
17698 * @cfg {boolean} [head] Show a popup header that contains a #label (if specified) and close
17699 * button.
17700 * @cfg {boolean} [padded] Add padding to the popup's body
17701 */
17702 OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
17703 // Configuration initialization
17704 config = config || {};
17705
17706 // Parent constructor
17707 OO.ui.PopupWidget.parent.call( this, config );
17708
17709 // Properties (must be set before ClippableElement constructor call)
17710 this.$body = $( '<div>' );
17711 this.$popup = $( '<div>' );
17712
17713 // Mixin constructors
17714 OO.ui.mixin.LabelElement.call( this, config );
17715 OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, {
17716 $clippable: this.$body,
17717 $clippableContainer: this.$popup
17718 } ) );
17719
17720 // Properties
17721 this.$head = $( '<div>' );
17722 this.$footer = $( '<div>' );
17723 this.$anchor = $( '<div>' );
17724 // If undefined, will be computed lazily in updateDimensions()
17725 this.$container = config.$container;
17726 this.containerPadding = config.containerPadding !== undefined ? config.containerPadding : 10;
17727 this.autoClose = !!config.autoClose;
17728 this.$autoCloseIgnore = config.$autoCloseIgnore;
17729 this.transitionTimeout = null;
17730 this.anchor = null;
17731 this.width = config.width !== undefined ? config.width : 320;
17732 this.height = config.height !== undefined ? config.height : null;
17733 this.setAlignment( config.align );
17734 this.closeButton = new OO.ui.ButtonWidget( { framed: false, icon: 'close' } );
17735 this.onMouseDownHandler = this.onMouseDown.bind( this );
17736 this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
17737
17738 // Events
17739 this.closeButton.connect( this, { click: 'onCloseButtonClick' } );
17740
17741 // Initialization
17742 this.toggleAnchor( config.anchor === undefined || config.anchor );
17743 this.$body.addClass( 'oo-ui-popupWidget-body' );
17744 this.$anchor.addClass( 'oo-ui-popupWidget-anchor' );
17745 this.$head
17746 .addClass( 'oo-ui-popupWidget-head' )
17747 .append( this.$label, this.closeButton.$element );
17748 this.$footer.addClass( 'oo-ui-popupWidget-footer' );
17749 if ( !config.head ) {
17750 this.$head.addClass( 'oo-ui-element-hidden' );
17751 }
17752 if ( !config.$footer ) {
17753 this.$footer.addClass( 'oo-ui-element-hidden' );
17754 }
17755 this.$popup
17756 .addClass( 'oo-ui-popupWidget-popup' )
17757 .append( this.$head, this.$body, this.$footer );
17758 this.$element
17759 .addClass( 'oo-ui-popupWidget' )
17760 .append( this.$popup, this.$anchor );
17761 // Move content, which was added to #$element by OO.ui.Widget, to the body
17762 if ( config.$content instanceof jQuery ) {
17763 this.$body.append( config.$content );
17764 }
17765 if ( config.$footer instanceof jQuery ) {
17766 this.$footer.append( config.$footer );
17767 }
17768 if ( config.padded ) {
17769 this.$body.addClass( 'oo-ui-popupWidget-body-padded' );
17770 }
17771
17772 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
17773 // that reference properties not initialized at that time of parent class construction
17774 // TODO: Find a better way to handle post-constructor setup
17775 this.visible = false;
17776 this.$element.addClass( 'oo-ui-element-hidden' );
17777 };
17778
17779 /* Setup */
17780
17781 OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
17782 OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.LabelElement );
17783 OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.ClippableElement );
17784
17785 /* Methods */
17786
17787 /**
17788 * Handles mouse down events.
17789 *
17790 * @private
17791 * @param {MouseEvent} e Mouse down event
17792 */
17793 OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
17794 if (
17795 this.isVisible() &&
17796 !$.contains( this.$element[ 0 ], e.target ) &&
17797 ( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length )
17798 ) {
17799 this.toggle( false );
17800 }
17801 };
17802
17803 /**
17804 * Bind mouse down listener.
17805 *
17806 * @private
17807 */
17808 OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
17809 // Capture clicks outside popup
17810 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
17811 };
17812
17813 /**
17814 * Handles close button click events.
17815 *
17816 * @private
17817 */
17818 OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
17819 if ( this.isVisible() ) {
17820 this.toggle( false );
17821 }
17822 };
17823
17824 /**
17825 * Unbind mouse down listener.
17826 *
17827 * @private
17828 */
17829 OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
17830 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
17831 };
17832
17833 /**
17834 * Handles key down events.
17835 *
17836 * @private
17837 * @param {KeyboardEvent} e Key down event
17838 */
17839 OO.ui.PopupWidget.prototype.onDocumentKeyDown = function ( e ) {
17840 if (
17841 e.which === OO.ui.Keys.ESCAPE &&
17842 this.isVisible()
17843 ) {
17844 this.toggle( false );
17845 e.preventDefault();
17846 e.stopPropagation();
17847 }
17848 };
17849
17850 /**
17851 * Bind key down listener.
17852 *
17853 * @private
17854 */
17855 OO.ui.PopupWidget.prototype.bindKeyDownListener = function () {
17856 this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
17857 };
17858
17859 /**
17860 * Unbind key down listener.
17861 *
17862 * @private
17863 */
17864 OO.ui.PopupWidget.prototype.unbindKeyDownListener = function () {
17865 this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
17866 };
17867
17868 /**
17869 * Show, hide, or toggle the visibility of the anchor.
17870 *
17871 * @param {boolean} [show] Show anchor, omit to toggle
17872 */
17873 OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) {
17874 show = show === undefined ? !this.anchored : !!show;
17875
17876 if ( this.anchored !== show ) {
17877 if ( show ) {
17878 this.$element.addClass( 'oo-ui-popupWidget-anchored' );
17879 } else {
17880 this.$element.removeClass( 'oo-ui-popupWidget-anchored' );
17881 }
17882 this.anchored = show;
17883 }
17884 };
17885
17886 /**
17887 * Check if the anchor is visible.
17888 *
17889 * @return {boolean} Anchor is visible
17890 */
17891 OO.ui.PopupWidget.prototype.hasAnchor = function () {
17892 return this.anchor;
17893 };
17894
17895 /**
17896 * @inheritdoc
17897 */
17898 OO.ui.PopupWidget.prototype.toggle = function ( show ) {
17899 var change;
17900 show = show === undefined ? !this.isVisible() : !!show;
17901
17902 change = show !== this.isVisible();
17903
17904 // Parent method
17905 OO.ui.PopupWidget.parent.prototype.toggle.call( this, show );
17906
17907 if ( change ) {
17908 if ( show ) {
17909 if ( this.autoClose ) {
17910 this.bindMouseDownListener();
17911 this.bindKeyDownListener();
17912 }
17913 this.updateDimensions();
17914 this.toggleClipping( true );
17915 } else {
17916 this.toggleClipping( false );
17917 if ( this.autoClose ) {
17918 this.unbindMouseDownListener();
17919 this.unbindKeyDownListener();
17920 }
17921 }
17922 }
17923
17924 return this;
17925 };
17926
17927 /**
17928 * Set the size of the popup.
17929 *
17930 * Changing the size may also change the popup's position depending on the alignment.
17931 *
17932 * @param {number} width Width in pixels
17933 * @param {number} height Height in pixels
17934 * @param {boolean} [transition=false] Use a smooth transition
17935 * @chainable
17936 */
17937 OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) {
17938 this.width = width;
17939 this.height = height !== undefined ? height : null;
17940 if ( this.isVisible() ) {
17941 this.updateDimensions( transition );
17942 }
17943 };
17944
17945 /**
17946 * Update the size and position.
17947 *
17948 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
17949 * be called automatically.
17950 *
17951 * @param {boolean} [transition=false] Use a smooth transition
17952 * @chainable
17953 */
17954 OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
17955 var popupOffset, originOffset, containerLeft, containerWidth, containerRight,
17956 popupLeft, popupRight, overlapLeft, overlapRight, anchorWidth,
17957 align = this.align,
17958 widget = this;
17959
17960 if ( !this.$container ) {
17961 // Lazy-initialize $container if not specified in constructor
17962 this.$container = $( this.getClosestScrollableElementContainer() );
17963 }
17964
17965 // Set height and width before measuring things, since it might cause our measurements
17966 // to change (e.g. due to scrollbars appearing or disappearing)
17967 this.$popup.css( {
17968 width: this.width,
17969 height: this.height !== null ? this.height : 'auto'
17970 } );
17971
17972 // If we are in RTL, we need to flip the alignment, unless it is center
17973 if ( align === 'forwards' || align === 'backwards' ) {
17974 if ( this.$container.css( 'direction' ) === 'rtl' ) {
17975 align = ( { forwards: 'force-left', backwards: 'force-right' } )[ this.align ];
17976 } else {
17977 align = ( { forwards: 'force-right', backwards: 'force-left' } )[ this.align ];
17978 }
17979
17980 }
17981
17982 // Compute initial popupOffset based on alignment
17983 popupOffset = this.width * ( { 'force-left': -1, center: -0.5, 'force-right': 0 } )[ align ];
17984
17985 // Figure out if this will cause the popup to go beyond the edge of the container
17986 originOffset = this.$element.offset().left;
17987 containerLeft = this.$container.offset().left;
17988 containerWidth = this.$container.innerWidth();
17989 containerRight = containerLeft + containerWidth;
17990 popupLeft = popupOffset - this.containerPadding;
17991 popupRight = popupOffset + this.containerPadding + this.width + this.containerPadding;
17992 overlapLeft = ( originOffset + popupLeft ) - containerLeft;
17993 overlapRight = containerRight - ( originOffset + popupRight );
17994
17995 // Adjust offset to make the popup not go beyond the edge, if needed
17996 if ( overlapRight < 0 ) {
17997 popupOffset += overlapRight;
17998 } else if ( overlapLeft < 0 ) {
17999 popupOffset -= overlapLeft;
18000 }
18001
18002 // Adjust offset to avoid anchor being rendered too close to the edge
18003 // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
18004 // TODO: Find a measurement that works for CSS anchors and image anchors
18005 anchorWidth = this.$anchor[ 0 ].scrollWidth * 2;
18006 if ( popupOffset + this.width < anchorWidth ) {
18007 popupOffset = anchorWidth - this.width;
18008 } else if ( -popupOffset < anchorWidth ) {
18009 popupOffset = -anchorWidth;
18010 }
18011
18012 // Prevent transition from being interrupted
18013 clearTimeout( this.transitionTimeout );
18014 if ( transition ) {
18015 // Enable transition
18016 this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
18017 }
18018
18019 // Position body relative to anchor
18020 this.$popup.css( 'margin-left', popupOffset );
18021
18022 if ( transition ) {
18023 // Prevent transitioning after transition is complete
18024 this.transitionTimeout = setTimeout( function () {
18025 widget.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
18026 }, 200 );
18027 } else {
18028 // Prevent transitioning immediately
18029 this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
18030 }
18031
18032 // Reevaluate clipping state since we've relocated and resized the popup
18033 this.clip();
18034
18035 return this;
18036 };
18037
18038 /**
18039 * Set popup alignment
18040 * @param {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
18041 * `backwards` or `forwards`.
18042 */
18043 OO.ui.PopupWidget.prototype.setAlignment = function ( align ) {
18044 // Validate alignment and transform deprecated values
18045 if ( [ 'left', 'right', 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align ) > -1 ) {
18046 this.align = { left: 'force-right', right: 'force-left' }[ align ] || align;
18047 } else {
18048 this.align = 'center';
18049 }
18050 };
18051
18052 /**
18053 * Get popup alignment
18054 * @return {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
18055 * `backwards` or `forwards`.
18056 */
18057 OO.ui.PopupWidget.prototype.getAlignment = function () {
18058 return this.align;
18059 };
18060
18061 /**
18062 * Progress bars visually display the status of an operation, such as a download,
18063 * and can be either determinate or indeterminate:
18064 *
18065 * - **determinate** process bars show the percent of an operation that is complete.
18066 *
18067 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
18068 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
18069 * not use percentages.
18070 *
18071 * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
18072 *
18073 * @example
18074 * // Examples of determinate and indeterminate progress bars.
18075 * var progressBar1 = new OO.ui.ProgressBarWidget( {
18076 * progress: 33
18077 * } );
18078 * var progressBar2 = new OO.ui.ProgressBarWidget();
18079 *
18080 * // Create a FieldsetLayout to layout progress bars
18081 * var fieldset = new OO.ui.FieldsetLayout;
18082 * fieldset.addItems( [
18083 * new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
18084 * new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
18085 * ] );
18086 * $( 'body' ).append( fieldset.$element );
18087 *
18088 * @class
18089 * @extends OO.ui.Widget
18090 *
18091 * @constructor
18092 * @param {Object} [config] Configuration options
18093 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
18094 * To create a determinate progress bar, specify a number that reflects the initial percent complete.
18095 * By default, the progress bar is indeterminate.
18096 */
18097 OO.ui.ProgressBarWidget = function OoUiProgressBarWidget( config ) {
18098 // Configuration initialization
18099 config = config || {};
18100
18101 // Parent constructor
18102 OO.ui.ProgressBarWidget.parent.call( this, config );
18103
18104 // Properties
18105 this.$bar = $( '<div>' );
18106 this.progress = null;
18107
18108 // Initialization
18109 this.setProgress( config.progress !== undefined ? config.progress : false );
18110 this.$bar.addClass( 'oo-ui-progressBarWidget-bar' );
18111 this.$element
18112 .attr( {
18113 role: 'progressbar',
18114 'aria-valuemin': 0,
18115 'aria-valuemax': 100
18116 } )
18117 .addClass( 'oo-ui-progressBarWidget' )
18118 .append( this.$bar );
18119 };
18120
18121 /* Setup */
18122
18123 OO.inheritClass( OO.ui.ProgressBarWidget, OO.ui.Widget );
18124
18125 /* Static Properties */
18126
18127 OO.ui.ProgressBarWidget.static.tagName = 'div';
18128
18129 /* Methods */
18130
18131 /**
18132 * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
18133 *
18134 * @return {number|boolean} Progress percent
18135 */
18136 OO.ui.ProgressBarWidget.prototype.getProgress = function () {
18137 return this.progress;
18138 };
18139
18140 /**
18141 * Set the percent of the process completed or `false` for an indeterminate process.
18142 *
18143 * @param {number|boolean} progress Progress percent or `false` for indeterminate
18144 */
18145 OO.ui.ProgressBarWidget.prototype.setProgress = function ( progress ) {
18146 this.progress = progress;
18147
18148 if ( progress !== false ) {
18149 this.$bar.css( 'width', this.progress + '%' );
18150 this.$element.attr( 'aria-valuenow', this.progress );
18151 } else {
18152 this.$bar.css( 'width', '' );
18153 this.$element.removeAttr( 'aria-valuenow' );
18154 }
18155 this.$element.toggleClass( 'oo-ui-progressBarWidget-indeterminate', !progress );
18156 };
18157
18158 /**
18159 * SearchWidgets combine a {@link OO.ui.TextInputWidget text input field}, where users can type a search query,
18160 * and a menu of search results, which is displayed beneath the query
18161 * field. Unlike {@link OO.ui.mixin.LookupElement lookup menus}, search result menus are always visible to the user.
18162 * Users can choose an item from the menu or type a query into the text field to search for a matching result item.
18163 * In general, search widgets are used inside a separate {@link OO.ui.Dialog dialog} window.
18164 *
18165 * Each time the query is changed, the search result menu is cleared and repopulated. Please see
18166 * the [OOjs UI demos][1] for an example.
18167 *
18168 * [1]: https://tools.wmflabs.org/oojs-ui/oojs-ui/demos/#dialogs-mediawiki-vector-ltr
18169 *
18170 * @class
18171 * @extends OO.ui.Widget
18172 *
18173 * @constructor
18174 * @param {Object} [config] Configuration options
18175 * @cfg {string|jQuery} [placeholder] Placeholder text for query input
18176 * @cfg {string} [value] Initial query value
18177 */
18178 OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
18179 // Configuration initialization
18180 config = config || {};
18181
18182 // Parent constructor
18183 OO.ui.SearchWidget.parent.call( this, config );
18184
18185 // Properties
18186 this.query = new OO.ui.TextInputWidget( {
18187 icon: 'search',
18188 placeholder: config.placeholder,
18189 value: config.value
18190 } );
18191 this.results = new OO.ui.SelectWidget();
18192 this.$query = $( '<div>' );
18193 this.$results = $( '<div>' );
18194
18195 // Events
18196 this.query.connect( this, {
18197 change: 'onQueryChange',
18198 enter: 'onQueryEnter'
18199 } );
18200 this.query.$input.on( 'keydown', this.onQueryKeydown.bind( this ) );
18201
18202 // Initialization
18203 this.$query
18204 .addClass( 'oo-ui-searchWidget-query' )
18205 .append( this.query.$element );
18206 this.$results
18207 .addClass( 'oo-ui-searchWidget-results' )
18208 .append( this.results.$element );
18209 this.$element
18210 .addClass( 'oo-ui-searchWidget' )
18211 .append( this.$results, this.$query );
18212 };
18213
18214 /* Setup */
18215
18216 OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget );
18217
18218 /* Methods */
18219
18220 /**
18221 * Handle query key down events.
18222 *
18223 * @private
18224 * @param {jQuery.Event} e Key down event
18225 */
18226 OO.ui.SearchWidget.prototype.onQueryKeydown = function ( e ) {
18227 var highlightedItem, nextItem,
18228 dir = e.which === OO.ui.Keys.DOWN ? 1 : ( e.which === OO.ui.Keys.UP ? -1 : 0 );
18229
18230 if ( dir ) {
18231 highlightedItem = this.results.getHighlightedItem();
18232 if ( !highlightedItem ) {
18233 highlightedItem = this.results.getSelectedItem();
18234 }
18235 nextItem = this.results.getRelativeSelectableItem( highlightedItem, dir );
18236 this.results.highlightItem( nextItem );
18237 nextItem.scrollElementIntoView();
18238 }
18239 };
18240
18241 /**
18242 * Handle select widget select events.
18243 *
18244 * Clears existing results. Subclasses should repopulate items according to new query.
18245 *
18246 * @private
18247 * @param {string} value New value
18248 */
18249 OO.ui.SearchWidget.prototype.onQueryChange = function () {
18250 // Reset
18251 this.results.clearItems();
18252 };
18253
18254 /**
18255 * Handle select widget enter key events.
18256 *
18257 * Chooses highlighted item.
18258 *
18259 * @private
18260 * @param {string} value New value
18261 */
18262 OO.ui.SearchWidget.prototype.onQueryEnter = function () {
18263 var highlightedItem = this.results.getHighlightedItem();
18264 if ( highlightedItem ) {
18265 this.results.chooseItem( highlightedItem );
18266 }
18267 };
18268
18269 /**
18270 * Get the query input.
18271 *
18272 * @return {OO.ui.TextInputWidget} Query input
18273 */
18274 OO.ui.SearchWidget.prototype.getQuery = function () {
18275 return this.query;
18276 };
18277
18278 /**
18279 * Get the search results menu.
18280 *
18281 * @return {OO.ui.SelectWidget} Menu of search results
18282 */
18283 OO.ui.SearchWidget.prototype.getResults = function () {
18284 return this.results;
18285 };
18286
18287 /**
18288 * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
18289 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
18290 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
18291 * menu selects}.
18292 *
18293 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
18294 * information, please see the [OOjs UI documentation on MediaWiki][1].
18295 *
18296 * @example
18297 * // Example of a select widget with three options
18298 * var select = new OO.ui.SelectWidget( {
18299 * items: [
18300 * new OO.ui.OptionWidget( {
18301 * data: 'a',
18302 * label: 'Option One',
18303 * } ),
18304 * new OO.ui.OptionWidget( {
18305 * data: 'b',
18306 * label: 'Option Two',
18307 * } ),
18308 * new OO.ui.OptionWidget( {
18309 * data: 'c',
18310 * label: 'Option Three',
18311 * } )
18312 * ]
18313 * } );
18314 * $( 'body' ).append( select.$element );
18315 *
18316 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
18317 *
18318 * @abstract
18319 * @class
18320 * @extends OO.ui.Widget
18321 * @mixins OO.ui.mixin.GroupWidget
18322 *
18323 * @constructor
18324 * @param {Object} [config] Configuration options
18325 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
18326 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
18327 * the [OOjs UI documentation on MediaWiki] [2] for examples.
18328 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
18329 */
18330 OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
18331 // Configuration initialization
18332 config = config || {};
18333
18334 // Parent constructor
18335 OO.ui.SelectWidget.parent.call( this, config );
18336
18337 // Mixin constructors
18338 OO.ui.mixin.GroupWidget.call( this, $.extend( {}, config, { $group: this.$element } ) );
18339
18340 // Properties
18341 this.pressed = false;
18342 this.selecting = null;
18343 this.onMouseUpHandler = this.onMouseUp.bind( this );
18344 this.onMouseMoveHandler = this.onMouseMove.bind( this );
18345 this.onKeyDownHandler = this.onKeyDown.bind( this );
18346 this.onKeyPressHandler = this.onKeyPress.bind( this );
18347 this.keyPressBuffer = '';
18348 this.keyPressBufferTimer = null;
18349
18350 // Events
18351 this.connect( this, {
18352 toggle: 'onToggle'
18353 } );
18354 this.$element.on( {
18355 mousedown: this.onMouseDown.bind( this ),
18356 mouseover: this.onMouseOver.bind( this ),
18357 mouseleave: this.onMouseLeave.bind( this )
18358 } );
18359
18360 // Initialization
18361 this.$element
18362 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
18363 .attr( 'role', 'listbox' );
18364 if ( Array.isArray( config.items ) ) {
18365 this.addItems( config.items );
18366 }
18367 };
18368
18369 /* Setup */
18370
18371 OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
18372
18373 // Need to mixin base class as well
18374 OO.mixinClass( OO.ui.SelectWidget, OO.ui.mixin.GroupElement );
18375 OO.mixinClass( OO.ui.SelectWidget, OO.ui.mixin.GroupWidget );
18376
18377 /* Static */
18378 OO.ui.SelectWidget.static.passAllFilter = function () {
18379 return true;
18380 };
18381
18382 /* Events */
18383
18384 /**
18385 * @event highlight
18386 *
18387 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
18388 *
18389 * @param {OO.ui.OptionWidget|null} item Highlighted item
18390 */
18391
18392 /**
18393 * @event press
18394 *
18395 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
18396 * pressed state of an option.
18397 *
18398 * @param {OO.ui.OptionWidget|null} item Pressed item
18399 */
18400
18401 /**
18402 * @event select
18403 *
18404 * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
18405 *
18406 * @param {OO.ui.OptionWidget|null} item Selected item
18407 */
18408
18409 /**
18410 * @event choose
18411 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
18412 * @param {OO.ui.OptionWidget} item Chosen item
18413 */
18414
18415 /**
18416 * @event add
18417 *
18418 * An `add` event is emitted when options are added to the select with the #addItems method.
18419 *
18420 * @param {OO.ui.OptionWidget[]} items Added items
18421 * @param {number} index Index of insertion point
18422 */
18423
18424 /**
18425 * @event remove
18426 *
18427 * A `remove` event is emitted when options are removed from the select with the #clearItems
18428 * or #removeItems methods.
18429 *
18430 * @param {OO.ui.OptionWidget[]} items Removed items
18431 */
18432
18433 /* Methods */
18434
18435 /**
18436 * Handle mouse down events.
18437 *
18438 * @private
18439 * @param {jQuery.Event} e Mouse down event
18440 */
18441 OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
18442 var item;
18443
18444 if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
18445 this.togglePressed( true );
18446 item = this.getTargetItem( e );
18447 if ( item && item.isSelectable() ) {
18448 this.pressItem( item );
18449 this.selecting = item;
18450 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
18451 this.getElementDocument().addEventListener( 'mousemove', this.onMouseMoveHandler, true );
18452 }
18453 }
18454 return false;
18455 };
18456
18457 /**
18458 * Handle mouse up events.
18459 *
18460 * @private
18461 * @param {jQuery.Event} e Mouse up event
18462 */
18463 OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
18464 var item;
18465
18466 this.togglePressed( false );
18467 if ( !this.selecting ) {
18468 item = this.getTargetItem( e );
18469 if ( item && item.isSelectable() ) {
18470 this.selecting = item;
18471 }
18472 }
18473 if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT && this.selecting ) {
18474 this.pressItem( null );
18475 this.chooseItem( this.selecting );
18476 this.selecting = null;
18477 }
18478
18479 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
18480 this.getElementDocument().removeEventListener( 'mousemove', this.onMouseMoveHandler, true );
18481
18482 return false;
18483 };
18484
18485 /**
18486 * Handle mouse move events.
18487 *
18488 * @private
18489 * @param {jQuery.Event} e Mouse move event
18490 */
18491 OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
18492 var item;
18493
18494 if ( !this.isDisabled() && this.pressed ) {
18495 item = this.getTargetItem( e );
18496 if ( item && item !== this.selecting && item.isSelectable() ) {
18497 this.pressItem( item );
18498 this.selecting = item;
18499 }
18500 }
18501 return false;
18502 };
18503
18504 /**
18505 * Handle mouse over events.
18506 *
18507 * @private
18508 * @param {jQuery.Event} e Mouse over event
18509 */
18510 OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
18511 var item;
18512
18513 if ( !this.isDisabled() ) {
18514 item = this.getTargetItem( e );
18515 this.highlightItem( item && item.isHighlightable() ? item : null );
18516 }
18517 return false;
18518 };
18519
18520 /**
18521 * Handle mouse leave events.
18522 *
18523 * @private
18524 * @param {jQuery.Event} e Mouse over event
18525 */
18526 OO.ui.SelectWidget.prototype.onMouseLeave = function () {
18527 if ( !this.isDisabled() ) {
18528 this.highlightItem( null );
18529 }
18530 return false;
18531 };
18532
18533 /**
18534 * Handle key down events.
18535 *
18536 * @protected
18537 * @param {jQuery.Event} e Key down event
18538 */
18539 OO.ui.SelectWidget.prototype.onKeyDown = function ( e ) {
18540 var nextItem,
18541 handled = false,
18542 currentItem = this.getHighlightedItem() || this.getSelectedItem();
18543
18544 if ( !this.isDisabled() && this.isVisible() ) {
18545 switch ( e.keyCode ) {
18546 case OO.ui.Keys.ENTER:
18547 if ( currentItem && currentItem.constructor.static.highlightable ) {
18548 // Was only highlighted, now let's select it. No-op if already selected.
18549 this.chooseItem( currentItem );
18550 handled = true;
18551 }
18552 break;
18553 case OO.ui.Keys.UP:
18554 case OO.ui.Keys.LEFT:
18555 this.clearKeyPressBuffer();
18556 nextItem = this.getRelativeSelectableItem( currentItem, -1 );
18557 handled = true;
18558 break;
18559 case OO.ui.Keys.DOWN:
18560 case OO.ui.Keys.RIGHT:
18561 this.clearKeyPressBuffer();
18562 nextItem = this.getRelativeSelectableItem( currentItem, 1 );
18563 handled = true;
18564 break;
18565 case OO.ui.Keys.ESCAPE:
18566 case OO.ui.Keys.TAB:
18567 if ( currentItem && currentItem.constructor.static.highlightable ) {
18568 currentItem.setHighlighted( false );
18569 }
18570 this.unbindKeyDownListener();
18571 this.unbindKeyPressListener();
18572 // Don't prevent tabbing away / defocusing
18573 handled = false;
18574 break;
18575 }
18576
18577 if ( nextItem ) {
18578 if ( nextItem.constructor.static.highlightable ) {
18579 this.highlightItem( nextItem );
18580 } else {
18581 this.chooseItem( nextItem );
18582 }
18583 nextItem.scrollElementIntoView();
18584 }
18585
18586 if ( handled ) {
18587 // Can't just return false, because e is not always a jQuery event
18588 e.preventDefault();
18589 e.stopPropagation();
18590 }
18591 }
18592 };
18593
18594 /**
18595 * Bind key down listener.
18596 *
18597 * @protected
18598 */
18599 OO.ui.SelectWidget.prototype.bindKeyDownListener = function () {
18600 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
18601 };
18602
18603 /**
18604 * Unbind key down listener.
18605 *
18606 * @protected
18607 */
18608 OO.ui.SelectWidget.prototype.unbindKeyDownListener = function () {
18609 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
18610 };
18611
18612 /**
18613 * Clear the key-press buffer
18614 *
18615 * @protected
18616 */
18617 OO.ui.SelectWidget.prototype.clearKeyPressBuffer = function () {
18618 if ( this.keyPressBufferTimer ) {
18619 clearTimeout( this.keyPressBufferTimer );
18620 this.keyPressBufferTimer = null;
18621 }
18622 this.keyPressBuffer = '';
18623 };
18624
18625 /**
18626 * Handle key press events.
18627 *
18628 * @protected
18629 * @param {jQuery.Event} e Key press event
18630 */
18631 OO.ui.SelectWidget.prototype.onKeyPress = function ( e ) {
18632 var c, filter, item;
18633
18634 if ( !e.charCode ) {
18635 if ( e.keyCode === OO.ui.Keys.BACKSPACE && this.keyPressBuffer !== '' ) {
18636 this.keyPressBuffer = this.keyPressBuffer.substr( 0, this.keyPressBuffer.length - 1 );
18637 return false;
18638 }
18639 return;
18640 }
18641 if ( String.fromCodePoint ) {
18642 c = String.fromCodePoint( e.charCode );
18643 } else {
18644 c = String.fromCharCode( e.charCode );
18645 }
18646
18647 if ( this.keyPressBufferTimer ) {
18648 clearTimeout( this.keyPressBufferTimer );
18649 }
18650 this.keyPressBufferTimer = setTimeout( this.clearKeyPressBuffer.bind( this ), 1500 );
18651
18652 item = this.getHighlightedItem() || this.getSelectedItem();
18653
18654 if ( this.keyPressBuffer === c ) {
18655 // Common (if weird) special case: typing "xxxx" will cycle through all
18656 // the items beginning with "x".
18657 if ( item ) {
18658 item = this.getRelativeSelectableItem( item, 1 );
18659 }
18660 } else {
18661 this.keyPressBuffer += c;
18662 }
18663
18664 filter = this.getItemMatcher( this.keyPressBuffer, false );
18665 if ( !item || !filter( item ) ) {
18666 item = this.getRelativeSelectableItem( item, 1, filter );
18667 }
18668 if ( item ) {
18669 if ( item.constructor.static.highlightable ) {
18670 this.highlightItem( item );
18671 } else {
18672 this.chooseItem( item );
18673 }
18674 item.scrollElementIntoView();
18675 }
18676
18677 return false;
18678 };
18679
18680 /**
18681 * Get a matcher for the specific string
18682 *
18683 * @protected
18684 * @param {string} s String to match against items
18685 * @param {boolean} [exact=false] Only accept exact matches
18686 * @return {Function} function ( OO.ui.OptionItem ) => boolean
18687 */
18688 OO.ui.SelectWidget.prototype.getItemMatcher = function ( s, exact ) {
18689 var re;
18690
18691 if ( s.normalize ) {
18692 s = s.normalize();
18693 }
18694 s = exact ? s.trim() : s.replace( /^\s+/, '' );
18695 re = '^\\s*' + s.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' );
18696 if ( exact ) {
18697 re += '\\s*$';
18698 }
18699 re = new RegExp( re, 'i' );
18700 return function ( item ) {
18701 var l = item.getLabel();
18702 if ( typeof l !== 'string' ) {
18703 l = item.$label.text();
18704 }
18705 if ( l.normalize ) {
18706 l = l.normalize();
18707 }
18708 return re.test( l );
18709 };
18710 };
18711
18712 /**
18713 * Bind key press listener.
18714 *
18715 * @protected
18716 */
18717 OO.ui.SelectWidget.prototype.bindKeyPressListener = function () {
18718 this.getElementWindow().addEventListener( 'keypress', this.onKeyPressHandler, true );
18719 };
18720
18721 /**
18722 * Unbind key down listener.
18723 *
18724 * If you override this, be sure to call this.clearKeyPressBuffer() from your
18725 * implementation.
18726 *
18727 * @protected
18728 */
18729 OO.ui.SelectWidget.prototype.unbindKeyPressListener = function () {
18730 this.getElementWindow().removeEventListener( 'keypress', this.onKeyPressHandler, true );
18731 this.clearKeyPressBuffer();
18732 };
18733
18734 /**
18735 * Visibility change handler
18736 *
18737 * @protected
18738 * @param {boolean} visible
18739 */
18740 OO.ui.SelectWidget.prototype.onToggle = function ( visible ) {
18741 if ( !visible ) {
18742 this.clearKeyPressBuffer();
18743 }
18744 };
18745
18746 /**
18747 * Get the closest item to a jQuery.Event.
18748 *
18749 * @private
18750 * @param {jQuery.Event} e
18751 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
18752 */
18753 OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) {
18754 return $( e.target ).closest( '.oo-ui-optionWidget' ).data( 'oo-ui-optionWidget' ) || null;
18755 };
18756
18757 /**
18758 * Get selected item.
18759 *
18760 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
18761 */
18762 OO.ui.SelectWidget.prototype.getSelectedItem = function () {
18763 var i, len;
18764
18765 for ( i = 0, len = this.items.length; i < len; i++ ) {
18766 if ( this.items[ i ].isSelected() ) {
18767 return this.items[ i ];
18768 }
18769 }
18770 return null;
18771 };
18772
18773 /**
18774 * Get highlighted item.
18775 *
18776 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
18777 */
18778 OO.ui.SelectWidget.prototype.getHighlightedItem = function () {
18779 var i, len;
18780
18781 for ( i = 0, len = this.items.length; i < len; i++ ) {
18782 if ( this.items[ i ].isHighlighted() ) {
18783 return this.items[ i ];
18784 }
18785 }
18786 return null;
18787 };
18788
18789 /**
18790 * Toggle pressed state.
18791 *
18792 * Press is a state that occurs when a user mouses down on an item, but
18793 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
18794 * until the user releases the mouse.
18795 *
18796 * @param {boolean} pressed An option is being pressed
18797 */
18798 OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) {
18799 if ( pressed === undefined ) {
18800 pressed = !this.pressed;
18801 }
18802 if ( pressed !== this.pressed ) {
18803 this.$element
18804 .toggleClass( 'oo-ui-selectWidget-pressed', pressed )
18805 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed );
18806 this.pressed = pressed;
18807 }
18808 };
18809
18810 /**
18811 * Highlight an option. If the `item` param is omitted, no options will be highlighted
18812 * and any existing highlight will be removed. The highlight is mutually exclusive.
18813 *
18814 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
18815 * @fires highlight
18816 * @chainable
18817 */
18818 OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
18819 var i, len, highlighted,
18820 changed = false;
18821
18822 for ( i = 0, len = this.items.length; i < len; i++ ) {
18823 highlighted = this.items[ i ] === item;
18824 if ( this.items[ i ].isHighlighted() !== highlighted ) {
18825 this.items[ i ].setHighlighted( highlighted );
18826 changed = true;
18827 }
18828 }
18829 if ( changed ) {
18830 this.emit( 'highlight', item );
18831 }
18832
18833 return this;
18834 };
18835
18836 /**
18837 * Fetch an item by its label.
18838 *
18839 * @param {string} label Label of the item to select.
18840 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
18841 * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
18842 */
18843 OO.ui.SelectWidget.prototype.getItemFromLabel = function ( label, prefix ) {
18844 var i, item, found,
18845 len = this.items.length,
18846 filter = this.getItemMatcher( label, true );
18847
18848 for ( i = 0; i < len; i++ ) {
18849 item = this.items[ i ];
18850 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() && filter( item ) ) {
18851 return item;
18852 }
18853 }
18854
18855 if ( prefix ) {
18856 found = null;
18857 filter = this.getItemMatcher( label, false );
18858 for ( i = 0; i < len; i++ ) {
18859 item = this.items[ i ];
18860 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() && filter( item ) ) {
18861 if ( found ) {
18862 return null;
18863 }
18864 found = item;
18865 }
18866 }
18867 if ( found ) {
18868 return found;
18869 }
18870 }
18871
18872 return null;
18873 };
18874
18875 /**
18876 * Programmatically select an option by its label. If the item does not exist,
18877 * all options will be deselected.
18878 *
18879 * @param {string} [label] Label of the item to select.
18880 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
18881 * @fires select
18882 * @chainable
18883 */
18884 OO.ui.SelectWidget.prototype.selectItemByLabel = function ( label, prefix ) {
18885 var itemFromLabel = this.getItemFromLabel( label, !!prefix );
18886 if ( label === undefined || !itemFromLabel ) {
18887 return this.selectItem();
18888 }
18889 return this.selectItem( itemFromLabel );
18890 };
18891
18892 /**
18893 * Programmatically select an option by its data. If the `data` parameter is omitted,
18894 * or if the item does not exist, all options will be deselected.
18895 *
18896 * @param {Object|string} [data] Value of the item to select, omit to deselect all
18897 * @fires select
18898 * @chainable
18899 */
18900 OO.ui.SelectWidget.prototype.selectItemByData = function ( data ) {
18901 var itemFromData = this.getItemFromData( data );
18902 if ( data === undefined || !itemFromData ) {
18903 return this.selectItem();
18904 }
18905 return this.selectItem( itemFromData );
18906 };
18907
18908 /**
18909 * Programmatically select an option by its reference. If the `item` parameter is omitted,
18910 * all options will be deselected.
18911 *
18912 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
18913 * @fires select
18914 * @chainable
18915 */
18916 OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
18917 var i, len, selected,
18918 changed = false;
18919
18920 for ( i = 0, len = this.items.length; i < len; i++ ) {
18921 selected = this.items[ i ] === item;
18922 if ( this.items[ i ].isSelected() !== selected ) {
18923 this.items[ i ].setSelected( selected );
18924 changed = true;
18925 }
18926 }
18927 if ( changed ) {
18928 this.emit( 'select', item );
18929 }
18930
18931 return this;
18932 };
18933
18934 /**
18935 * Press an item.
18936 *
18937 * Press is a state that occurs when a user mouses down on an item, but has not
18938 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
18939 * releases the mouse.
18940 *
18941 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
18942 * @fires press
18943 * @chainable
18944 */
18945 OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
18946 var i, len, pressed,
18947 changed = false;
18948
18949 for ( i = 0, len = this.items.length; i < len; i++ ) {
18950 pressed = this.items[ i ] === item;
18951 if ( this.items[ i ].isPressed() !== pressed ) {
18952 this.items[ i ].setPressed( pressed );
18953 changed = true;
18954 }
18955 }
18956 if ( changed ) {
18957 this.emit( 'press', item );
18958 }
18959
18960 return this;
18961 };
18962
18963 /**
18964 * Choose an item.
18965 *
18966 * Note that ‘choose’ should never be modified programmatically. A user can choose
18967 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
18968 * use the #selectItem method.
18969 *
18970 * This method is identical to #selectItem, but may vary in subclasses that take additional action
18971 * when users choose an item with the keyboard or mouse.
18972 *
18973 * @param {OO.ui.OptionWidget} item Item to choose
18974 * @fires choose
18975 * @chainable
18976 */
18977 OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
18978 if ( item ) {
18979 this.selectItem( item );
18980 this.emit( 'choose', item );
18981 }
18982
18983 return this;
18984 };
18985
18986 /**
18987 * Get an option by its position relative to the specified item (or to the start of the option array,
18988 * if item is `null`). The direction in which to search through the option array is specified with a
18989 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
18990 * `null` if there are no options in the array.
18991 *
18992 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
18993 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
18994 * @param {Function} filter Only consider items for which this function returns
18995 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
18996 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
18997 */
18998 OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction, filter ) {
18999 var currentIndex, nextIndex, i,
19000 increase = direction > 0 ? 1 : -1,
19001 len = this.items.length;
19002
19003 if ( !$.isFunction( filter ) ) {
19004 filter = OO.ui.SelectWidget.static.passAllFilter;
19005 }
19006
19007 if ( item instanceof OO.ui.OptionWidget ) {
19008 currentIndex = this.items.indexOf( item );
19009 nextIndex = ( currentIndex + increase + len ) % len;
19010 } else {
19011 // If no item is selected and moving forward, start at the beginning.
19012 // If moving backward, start at the end.
19013 nextIndex = direction > 0 ? 0 : len - 1;
19014 }
19015
19016 for ( i = 0; i < len; i++ ) {
19017 item = this.items[ nextIndex ];
19018 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() && filter( item ) ) {
19019 return item;
19020 }
19021 nextIndex = ( nextIndex + increase + len ) % len;
19022 }
19023 return null;
19024 };
19025
19026 /**
19027 * Get the next selectable item or `null` if there are no selectable items.
19028 * Disabled options and menu-section markers and breaks are not selectable.
19029 *
19030 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
19031 */
19032 OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
19033 var i, len, item;
19034
19035 for ( i = 0, len = this.items.length; i < len; i++ ) {
19036 item = this.items[ i ];
19037 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
19038 return item;
19039 }
19040 }
19041
19042 return null;
19043 };
19044
19045 /**
19046 * Add an array of options to the select. Optionally, an index number can be used to
19047 * specify an insertion point.
19048 *
19049 * @param {OO.ui.OptionWidget[]} items Items to add
19050 * @param {number} [index] Index to insert items after
19051 * @fires add
19052 * @chainable
19053 */
19054 OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
19055 // Mixin method
19056 OO.ui.mixin.GroupWidget.prototype.addItems.call( this, items, index );
19057
19058 // Always provide an index, even if it was omitted
19059 this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index );
19060
19061 return this;
19062 };
19063
19064 /**
19065 * Remove the specified array of options from the select. Options will be detached
19066 * from the DOM, not removed, so they can be reused later. To remove all options from
19067 * the select, you may wish to use the #clearItems method instead.
19068 *
19069 * @param {OO.ui.OptionWidget[]} items Items to remove
19070 * @fires remove
19071 * @chainable
19072 */
19073 OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
19074 var i, len, item;
19075
19076 // Deselect items being removed
19077 for ( i = 0, len = items.length; i < len; i++ ) {
19078 item = items[ i ];
19079 if ( item.isSelected() ) {
19080 this.selectItem( null );
19081 }
19082 }
19083
19084 // Mixin method
19085 OO.ui.mixin.GroupWidget.prototype.removeItems.call( this, items );
19086
19087 this.emit( 'remove', items );
19088
19089 return this;
19090 };
19091
19092 /**
19093 * Clear all options from the select. Options will be detached from the DOM, not removed,
19094 * so that they can be reused later. To remove a subset of options from the select, use
19095 * the #removeItems method.
19096 *
19097 * @fires remove
19098 * @chainable
19099 */
19100 OO.ui.SelectWidget.prototype.clearItems = function () {
19101 var items = this.items.slice();
19102
19103 // Mixin method
19104 OO.ui.mixin.GroupWidget.prototype.clearItems.call( this );
19105
19106 // Clear selection
19107 this.selectItem( null );
19108
19109 this.emit( 'remove', items );
19110
19111 return this;
19112 };
19113
19114 /**
19115 * ButtonSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains
19116 * button options and is used together with
19117 * OO.ui.ButtonOptionWidget. The ButtonSelectWidget provides an interface for
19118 * highlighting, choosing, and selecting mutually exclusive options. Please see
19119 * the [OOjs UI documentation on MediaWiki] [1] for more information.
19120 *
19121 * @example
19122 * // Example: A ButtonSelectWidget that contains three ButtonOptionWidgets
19123 * var option1 = new OO.ui.ButtonOptionWidget( {
19124 * data: 1,
19125 * label: 'Option 1',
19126 * title: 'Button option 1'
19127 * } );
19128 *
19129 * var option2 = new OO.ui.ButtonOptionWidget( {
19130 * data: 2,
19131 * label: 'Option 2',
19132 * title: 'Button option 2'
19133 * } );
19134 *
19135 * var option3 = new OO.ui.ButtonOptionWidget( {
19136 * data: 3,
19137 * label: 'Option 3',
19138 * title: 'Button option 3'
19139 * } );
19140 *
19141 * var buttonSelect=new OO.ui.ButtonSelectWidget( {
19142 * items: [ option1, option2, option3 ]
19143 * } );
19144 * $( 'body' ).append( buttonSelect.$element );
19145 *
19146 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
19147 *
19148 * @class
19149 * @extends OO.ui.SelectWidget
19150 * @mixins OO.ui.mixin.TabIndexedElement
19151 *
19152 * @constructor
19153 * @param {Object} [config] Configuration options
19154 */
19155 OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
19156 // Parent constructor
19157 OO.ui.ButtonSelectWidget.parent.call( this, config );
19158
19159 // Mixin constructors
19160 OO.ui.mixin.TabIndexedElement.call( this, config );
19161
19162 // Events
19163 this.$element.on( {
19164 focus: this.bindKeyDownListener.bind( this ),
19165 blur: this.unbindKeyDownListener.bind( this )
19166 } );
19167
19168 // Initialization
19169 this.$element.addClass( 'oo-ui-buttonSelectWidget' );
19170 };
19171
19172 /* Setup */
19173
19174 OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
19175 OO.mixinClass( OO.ui.ButtonSelectWidget, OO.ui.mixin.TabIndexedElement );
19176
19177 /**
19178 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
19179 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
19180 * an interface for adding, removing and selecting options.
19181 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
19182 *
19183 * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
19184 * OO.ui.RadioSelectInputWidget instead.
19185 *
19186 * @example
19187 * // A RadioSelectWidget with RadioOptions.
19188 * var option1 = new OO.ui.RadioOptionWidget( {
19189 * data: 'a',
19190 * label: 'Selected radio option'
19191 * } );
19192 *
19193 * var option2 = new OO.ui.RadioOptionWidget( {
19194 * data: 'b',
19195 * label: 'Unselected radio option'
19196 * } );
19197 *
19198 * var radioSelect=new OO.ui.RadioSelectWidget( {
19199 * items: [ option1, option2 ]
19200 * } );
19201 *
19202 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
19203 * radioSelect.selectItem( option1 );
19204 *
19205 * $( 'body' ).append( radioSelect.$element );
19206 *
19207 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
19208
19209 *
19210 * @class
19211 * @extends OO.ui.SelectWidget
19212 * @mixins OO.ui.mixin.TabIndexedElement
19213 *
19214 * @constructor
19215 * @param {Object} [config] Configuration options
19216 */
19217 OO.ui.RadioSelectWidget = function OoUiRadioSelectWidget( config ) {
19218 // Parent constructor
19219 OO.ui.RadioSelectWidget.parent.call( this, config );
19220
19221 // Mixin constructors
19222 OO.ui.mixin.TabIndexedElement.call( this, config );
19223
19224 // Events
19225 this.$element.on( {
19226 focus: this.bindKeyDownListener.bind( this ),
19227 blur: this.unbindKeyDownListener.bind( this )
19228 } );
19229
19230 // Initialization
19231 this.$element
19232 .addClass( 'oo-ui-radioSelectWidget' )
19233 .attr( 'role', 'radiogroup' );
19234 };
19235
19236 /* Setup */
19237
19238 OO.inheritClass( OO.ui.RadioSelectWidget, OO.ui.SelectWidget );
19239 OO.mixinClass( OO.ui.RadioSelectWidget, OO.ui.mixin.TabIndexedElement );
19240
19241 /**
19242 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
19243 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
19244 * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget},
19245 * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
19246 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
19247 * and customized to be opened, closed, and displayed as needed.
19248 *
19249 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
19250 * mouse outside the menu.
19251 *
19252 * Menus also have support for keyboard interaction:
19253 *
19254 * - Enter/Return key: choose and select a menu option
19255 * - Up-arrow key: highlight the previous menu option
19256 * - Down-arrow key: highlight the next menu option
19257 * - Esc key: hide the menu
19258 *
19259 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
19260 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
19261 *
19262 * @class
19263 * @extends OO.ui.SelectWidget
19264 * @mixins OO.ui.mixin.ClippableElement
19265 *
19266 * @constructor
19267 * @param {Object} [config] Configuration options
19268 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
19269 * the text the user types. This config is used by {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}
19270 * and {@link OO.ui.mixin.LookupElement LookupElement}
19271 * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
19272 * the text the user types. This config is used by {@link OO.ui.CapsuleMultiSelectWidget CapsuleMultiSelectWidget}
19273 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks the mouse
19274 * anywhere on the page outside of this widget, the menu is hidden. For example, if there is a button
19275 * that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
19276 * that button, unless the button (or its parent widget) is passed in here.
19277 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
19278 * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
19279 */
19280 OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) {
19281 // Configuration initialization
19282 config = config || {};
19283
19284 // Parent constructor
19285 OO.ui.MenuSelectWidget.parent.call( this, config );
19286
19287 // Mixin constructors
19288 OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
19289
19290 // Properties
19291 this.newItems = null;
19292 this.autoHide = config.autoHide === undefined || !!config.autoHide;
19293 this.filterFromInput = !!config.filterFromInput;
19294 this.$input = config.$input ? config.$input : config.input ? config.input.$input : null;
19295 this.$widget = config.widget ? config.widget.$element : null;
19296 this.onDocumentMouseDownHandler = this.onDocumentMouseDown.bind( this );
19297 this.onInputEditHandler = OO.ui.debounce( this.updateItemVisibility.bind( this ), 100 );
19298
19299 // Initialization
19300 this.$element
19301 .addClass( 'oo-ui-menuSelectWidget' )
19302 .attr( 'role', 'menu' );
19303
19304 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
19305 // that reference properties not initialized at that time of parent class construction
19306 // TODO: Find a better way to handle post-constructor setup
19307 this.visible = false;
19308 this.$element.addClass( 'oo-ui-element-hidden' );
19309 };
19310
19311 /* Setup */
19312
19313 OO.inheritClass( OO.ui.MenuSelectWidget, OO.ui.SelectWidget );
19314 OO.mixinClass( OO.ui.MenuSelectWidget, OO.ui.mixin.ClippableElement );
19315
19316 /* Methods */
19317
19318 /**
19319 * Handles document mouse down events.
19320 *
19321 * @protected
19322 * @param {jQuery.Event} e Key down event
19323 */
19324 OO.ui.MenuSelectWidget.prototype.onDocumentMouseDown = function ( e ) {
19325 if (
19326 !OO.ui.contains( this.$element[ 0 ], e.target, true ) &&
19327 ( !this.$widget || !OO.ui.contains( this.$widget[ 0 ], e.target, true ) )
19328 ) {
19329 this.toggle( false );
19330 }
19331 };
19332
19333 /**
19334 * @inheritdoc
19335 */
19336 OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) {
19337 var currentItem = this.getHighlightedItem() || this.getSelectedItem();
19338
19339 if ( !this.isDisabled() && this.isVisible() ) {
19340 switch ( e.keyCode ) {
19341 case OO.ui.Keys.LEFT:
19342 case OO.ui.Keys.RIGHT:
19343 // Do nothing if a text field is associated, arrow keys will be handled natively
19344 if ( !this.$input ) {
19345 OO.ui.MenuSelectWidget.parent.prototype.onKeyDown.call( this, e );
19346 }
19347 break;
19348 case OO.ui.Keys.ESCAPE:
19349 case OO.ui.Keys.TAB:
19350 if ( currentItem ) {
19351 currentItem.setHighlighted( false );
19352 }
19353 this.toggle( false );
19354 // Don't prevent tabbing away, prevent defocusing
19355 if ( e.keyCode === OO.ui.Keys.ESCAPE ) {
19356 e.preventDefault();
19357 e.stopPropagation();
19358 }
19359 break;
19360 default:
19361 OO.ui.MenuSelectWidget.parent.prototype.onKeyDown.call( this, e );
19362 return;
19363 }
19364 }
19365 };
19366
19367 /**
19368 * Update menu item visibility after input changes.
19369 * @protected
19370 */
19371 OO.ui.MenuSelectWidget.prototype.updateItemVisibility = function () {
19372 var i, item,
19373 len = this.items.length,
19374 showAll = !this.isVisible(),
19375 filter = showAll ? null : this.getItemMatcher( this.$input.val() );
19376
19377 for ( i = 0; i < len; i++ ) {
19378 item = this.items[ i ];
19379 if ( item instanceof OO.ui.OptionWidget ) {
19380 item.toggle( showAll || filter( item ) );
19381 }
19382 }
19383
19384 // Reevaluate clipping
19385 this.clip();
19386 };
19387
19388 /**
19389 * @inheritdoc
19390 */
19391 OO.ui.MenuSelectWidget.prototype.bindKeyDownListener = function () {
19392 if ( this.$input ) {
19393 this.$input.on( 'keydown', this.onKeyDownHandler );
19394 } else {
19395 OO.ui.MenuSelectWidget.parent.prototype.bindKeyDownListener.call( this );
19396 }
19397 };
19398
19399 /**
19400 * @inheritdoc
19401 */
19402 OO.ui.MenuSelectWidget.prototype.unbindKeyDownListener = function () {
19403 if ( this.$input ) {
19404 this.$input.off( 'keydown', this.onKeyDownHandler );
19405 } else {
19406 OO.ui.MenuSelectWidget.parent.prototype.unbindKeyDownListener.call( this );
19407 }
19408 };
19409
19410 /**
19411 * @inheritdoc
19412 */
19413 OO.ui.MenuSelectWidget.prototype.bindKeyPressListener = function () {
19414 if ( this.$input ) {
19415 if ( this.filterFromInput ) {
19416 this.$input.on( 'keydown mouseup cut paste change input select', this.onInputEditHandler );
19417 }
19418 } else {
19419 OO.ui.MenuSelectWidget.parent.prototype.bindKeyPressListener.call( this );
19420 }
19421 };
19422
19423 /**
19424 * @inheritdoc
19425 */
19426 OO.ui.MenuSelectWidget.prototype.unbindKeyPressListener = function () {
19427 if ( this.$input ) {
19428 if ( this.filterFromInput ) {
19429 this.$input.off( 'keydown mouseup cut paste change input select', this.onInputEditHandler );
19430 this.updateItemVisibility();
19431 }
19432 } else {
19433 OO.ui.MenuSelectWidget.parent.prototype.unbindKeyPressListener.call( this );
19434 }
19435 };
19436
19437 /**
19438 * Choose an item.
19439 *
19440 * When a user chooses an item, the menu is closed.
19441 *
19442 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
19443 * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
19444 * @param {OO.ui.OptionWidget} item Item to choose
19445 * @chainable
19446 */
19447 OO.ui.MenuSelectWidget.prototype.chooseItem = function ( item ) {
19448 OO.ui.MenuSelectWidget.parent.prototype.chooseItem.call( this, item );
19449 this.toggle( false );
19450 return this;
19451 };
19452
19453 /**
19454 * @inheritdoc
19455 */
19456 OO.ui.MenuSelectWidget.prototype.addItems = function ( items, index ) {
19457 var i, len, item;
19458
19459 // Parent method
19460 OO.ui.MenuSelectWidget.parent.prototype.addItems.call( this, items, index );
19461
19462 // Auto-initialize
19463 if ( !this.newItems ) {
19464 this.newItems = [];
19465 }
19466
19467 for ( i = 0, len = items.length; i < len; i++ ) {
19468 item = items[ i ];
19469 if ( this.isVisible() ) {
19470 // Defer fitting label until item has been attached
19471 item.fitLabel();
19472 } else {
19473 this.newItems.push( item );
19474 }
19475 }
19476
19477 // Reevaluate clipping
19478 this.clip();
19479
19480 return this;
19481 };
19482
19483 /**
19484 * @inheritdoc
19485 */
19486 OO.ui.MenuSelectWidget.prototype.removeItems = function ( items ) {
19487 // Parent method
19488 OO.ui.MenuSelectWidget.parent.prototype.removeItems.call( this, items );
19489
19490 // Reevaluate clipping
19491 this.clip();
19492
19493 return this;
19494 };
19495
19496 /**
19497 * @inheritdoc
19498 */
19499 OO.ui.MenuSelectWidget.prototype.clearItems = function () {
19500 // Parent method
19501 OO.ui.MenuSelectWidget.parent.prototype.clearItems.call( this );
19502
19503 // Reevaluate clipping
19504 this.clip();
19505
19506 return this;
19507 };
19508
19509 /**
19510 * @inheritdoc
19511 */
19512 OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
19513 var i, len, change;
19514
19515 visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
19516 change = visible !== this.isVisible();
19517
19518 // Parent method
19519 OO.ui.MenuSelectWidget.parent.prototype.toggle.call( this, visible );
19520
19521 if ( change ) {
19522 if ( visible ) {
19523 this.bindKeyDownListener();
19524 this.bindKeyPressListener();
19525
19526 if ( this.newItems && this.newItems.length ) {
19527 for ( i = 0, len = this.newItems.length; i < len; i++ ) {
19528 this.newItems[ i ].fitLabel();
19529 }
19530 this.newItems = null;
19531 }
19532 this.toggleClipping( true );
19533
19534 // Auto-hide
19535 if ( this.autoHide ) {
19536 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler, true );
19537 }
19538 } else {
19539 this.unbindKeyDownListener();
19540 this.unbindKeyPressListener();
19541 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler, true );
19542 this.toggleClipping( false );
19543 }
19544 }
19545
19546 return this;
19547 };
19548
19549 /**
19550 * FloatingMenuSelectWidget is a menu that will stick under a specified
19551 * container, even when it is inserted elsewhere in the document (for example,
19552 * in a OO.ui.Window's $overlay). This is sometimes necessary to prevent the
19553 * menu from being clipped too aggresively.
19554 *
19555 * The menu's position is automatically calculated and maintained when the menu
19556 * is toggled or the window is resized.
19557 *
19558 * See OO.ui.ComboBoxInputWidget for an example of a widget that uses this class.
19559 *
19560 * @class
19561 * @extends OO.ui.MenuSelectWidget
19562 * @mixins OO.ui.mixin.FloatableElement
19563 *
19564 * @constructor
19565 * @param {OO.ui.Widget} [inputWidget] Widget to provide the menu for.
19566 * Deprecated, omit this parameter and specify `$container` instead.
19567 * @param {Object} [config] Configuration options
19568 * @cfg {jQuery} [$container=inputWidget.$element] Element to render menu under
19569 */
19570 OO.ui.FloatingMenuSelectWidget = function OoUiFloatingMenuSelectWidget( inputWidget, config ) {
19571 // Allow 'inputWidget' parameter and config for backwards compatibility
19572 if ( OO.isPlainObject( inputWidget ) && config === undefined ) {
19573 config = inputWidget;
19574 inputWidget = config.inputWidget;
19575 }
19576
19577 // Configuration initialization
19578 config = config || {};
19579
19580 // Parent constructor
19581 OO.ui.FloatingMenuSelectWidget.parent.call( this, config );
19582
19583 // Properties (must be set before mixin constructors)
19584 this.inputWidget = inputWidget; // For backwards compatibility
19585 this.$container = config.$container || this.inputWidget.$element;
19586
19587 // Mixins constructors
19588 OO.ui.mixin.FloatableElement.call( this, $.extend( {}, config, { $floatableContainer: this.$container } ) );
19589
19590 // Initialization
19591 this.$element.addClass( 'oo-ui-floatingMenuSelectWidget' );
19592 // For backwards compatibility
19593 this.$element.addClass( 'oo-ui-textInputMenuSelectWidget' );
19594 };
19595
19596 /* Setup */
19597
19598 OO.inheritClass( OO.ui.FloatingMenuSelectWidget, OO.ui.MenuSelectWidget );
19599 OO.mixinClass( OO.ui.FloatingMenuSelectWidget, OO.ui.mixin.FloatableElement );
19600
19601 // For backwards compatibility
19602 OO.ui.TextInputMenuSelectWidget = OO.ui.FloatingMenuSelectWidget;
19603
19604 /* Methods */
19605
19606 /**
19607 * @inheritdoc
19608 */
19609 OO.ui.FloatingMenuSelectWidget.prototype.toggle = function ( visible ) {
19610 var change;
19611 visible = visible === undefined ? !this.isVisible() : !!visible;
19612 change = visible !== this.isVisible();
19613
19614 if ( change && visible ) {
19615 // Make sure the width is set before the parent method runs.
19616 this.setIdealSize( this.$container.width() );
19617 }
19618
19619 // Parent method
19620 // This will call this.clip(), which is nonsensical since we're not positioned yet...
19621 OO.ui.FloatingMenuSelectWidget.parent.prototype.toggle.call( this, visible );
19622
19623 if ( change ) {
19624 this.togglePositioning( this.isVisible() );
19625 }
19626
19627 return this;
19628 };
19629
19630 /**
19631 * OutlineSelectWidget is a structured list that contains {@link OO.ui.OutlineOptionWidget outline options}
19632 * A set of controls can be provided with an {@link OO.ui.OutlineControlsWidget outline controls} widget.
19633 *
19634 * **Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.**
19635 *
19636 * @class
19637 * @extends OO.ui.SelectWidget
19638 * @mixins OO.ui.mixin.TabIndexedElement
19639 *
19640 * @constructor
19641 * @param {Object} [config] Configuration options
19642 */
19643 OO.ui.OutlineSelectWidget = function OoUiOutlineSelectWidget( config ) {
19644 // Parent constructor
19645 OO.ui.OutlineSelectWidget.parent.call( this, config );
19646
19647 // Mixin constructors
19648 OO.ui.mixin.TabIndexedElement.call( this, config );
19649
19650 // Events
19651 this.$element.on( {
19652 focus: this.bindKeyDownListener.bind( this ),
19653 blur: this.unbindKeyDownListener.bind( this )
19654 } );
19655
19656 // Initialization
19657 this.$element.addClass( 'oo-ui-outlineSelectWidget' );
19658 };
19659
19660 /* Setup */
19661
19662 OO.inheritClass( OO.ui.OutlineSelectWidget, OO.ui.SelectWidget );
19663 OO.mixinClass( OO.ui.OutlineSelectWidget, OO.ui.mixin.TabIndexedElement );
19664
19665 /**
19666 * TabSelectWidget is a list that contains {@link OO.ui.TabOptionWidget tab options}
19667 *
19668 * **Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}.**
19669 *
19670 * @class
19671 * @extends OO.ui.SelectWidget
19672 * @mixins OO.ui.mixin.TabIndexedElement
19673 *
19674 * @constructor
19675 * @param {Object} [config] Configuration options
19676 */
19677 OO.ui.TabSelectWidget = function OoUiTabSelectWidget( config ) {
19678 // Parent constructor
19679 OO.ui.TabSelectWidget.parent.call( this, config );
19680
19681 // Mixin constructors
19682 OO.ui.mixin.TabIndexedElement.call( this, config );
19683
19684 // Events
19685 this.$element.on( {
19686 focus: this.bindKeyDownListener.bind( this ),
19687 blur: this.unbindKeyDownListener.bind( this )
19688 } );
19689
19690 // Initialization
19691 this.$element.addClass( 'oo-ui-tabSelectWidget' );
19692 };
19693
19694 /* Setup */
19695
19696 OO.inheritClass( OO.ui.TabSelectWidget, OO.ui.SelectWidget );
19697 OO.mixinClass( OO.ui.TabSelectWidget, OO.ui.mixin.TabIndexedElement );
19698
19699 /**
19700 * NumberInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
19701 * can be entered manually) and two {@link OO.ui.ButtonWidget button widgets}
19702 * (to adjust the value in increments) to allow the user to enter a number.
19703 *
19704 * @example
19705 * // Example: A NumberInputWidget.
19706 * var numberInput = new OO.ui.NumberInputWidget( {
19707 * label: 'NumberInputWidget',
19708 * input: { value: 5, min: 1, max: 10 }
19709 * } );
19710 * $( 'body' ).append( numberInput.$element );
19711 *
19712 * @class
19713 * @extends OO.ui.Widget
19714 *
19715 * @constructor
19716 * @param {Object} [config] Configuration options
19717 * @cfg {Object} [input] Configuration options to pass to the {@link OO.ui.TextInputWidget text input widget}.
19718 * @cfg {Object} [minusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget decrementing button widget}.
19719 * @cfg {Object} [plusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget incrementing button widget}.
19720 * @cfg {boolean} [isInteger=false] Whether the field accepts only integer values.
19721 * @cfg {number} [min=-Infinity] Minimum allowed value
19722 * @cfg {number} [max=Infinity] Maximum allowed value
19723 * @cfg {number} [step=1] Delta when using the buttons or up/down arrow keys
19724 * @cfg {number|null} [pageStep] Delta when using the page-up/page-down keys. Defaults to 10 times #step.
19725 */
19726 OO.ui.NumberInputWidget = function OoUiNumberInputWidget( config ) {
19727 // Configuration initialization
19728 config = $.extend( {
19729 isInteger: false,
19730 min: -Infinity,
19731 max: Infinity,
19732 step: 1,
19733 pageStep: null
19734 }, config );
19735
19736 // Parent constructor
19737 OO.ui.NumberInputWidget.parent.call( this, config );
19738
19739 // Properties
19740 this.input = new OO.ui.TextInputWidget( $.extend(
19741 {
19742 disabled: this.isDisabled()
19743 },
19744 config.input
19745 ) );
19746 this.minusButton = new OO.ui.ButtonWidget( $.extend(
19747 {
19748 disabled: this.isDisabled(),
19749 tabIndex: -1
19750 },
19751 config.minusButton,
19752 {
19753 classes: [ 'oo-ui-numberInputWidget-minusButton' ],
19754 label: '−'
19755 }
19756 ) );
19757 this.plusButton = new OO.ui.ButtonWidget( $.extend(
19758 {
19759 disabled: this.isDisabled(),
19760 tabIndex: -1
19761 },
19762 config.plusButton,
19763 {
19764 classes: [ 'oo-ui-numberInputWidget-plusButton' ],
19765 label: '+'
19766 }
19767 ) );
19768
19769 // Events
19770 this.input.connect( this, {
19771 change: this.emit.bind( this, 'change' ),
19772 enter: this.emit.bind( this, 'enter' )
19773 } );
19774 this.input.$input.on( {
19775 keydown: this.onKeyDown.bind( this ),
19776 'wheel mousewheel DOMMouseScroll': this.onWheel.bind( this )
19777 } );
19778 this.plusButton.connect( this, {
19779 click: [ 'onButtonClick', +1 ]
19780 } );
19781 this.minusButton.connect( this, {
19782 click: [ 'onButtonClick', -1 ]
19783 } );
19784
19785 // Initialization
19786 this.setIsInteger( !!config.isInteger );
19787 this.setRange( config.min, config.max );
19788 this.setStep( config.step, config.pageStep );
19789
19790 this.$field = $( '<div>' ).addClass( 'oo-ui-numberInputWidget-field' )
19791 .append(
19792 this.minusButton.$element,
19793 this.input.$element,
19794 this.plusButton.$element
19795 );
19796 this.$element.addClass( 'oo-ui-numberInputWidget' ).append( this.$field );
19797 this.input.setValidation( this.validateNumber.bind( this ) );
19798 };
19799
19800 /* Setup */
19801
19802 OO.inheritClass( OO.ui.NumberInputWidget, OO.ui.Widget );
19803
19804 /* Events */
19805
19806 /**
19807 * A `change` event is emitted when the value of the input changes.
19808 *
19809 * @event change
19810 */
19811
19812 /**
19813 * An `enter` event is emitted when the user presses 'enter' inside the text box.
19814 *
19815 * @event enter
19816 */
19817
19818 /* Methods */
19819
19820 /**
19821 * Set whether only integers are allowed
19822 * @param {boolean} flag
19823 */
19824 OO.ui.NumberInputWidget.prototype.setIsInteger = function ( flag ) {
19825 this.isInteger = !!flag;
19826 this.input.setValidityFlag();
19827 };
19828
19829 /**
19830 * Get whether only integers are allowed
19831 * @return {boolean} Flag value
19832 */
19833 OO.ui.NumberInputWidget.prototype.getIsInteger = function () {
19834 return this.isInteger;
19835 };
19836
19837 /**
19838 * Set the range of allowed values
19839 * @param {number} min Minimum allowed value
19840 * @param {number} max Maximum allowed value
19841 */
19842 OO.ui.NumberInputWidget.prototype.setRange = function ( min, max ) {
19843 if ( min > max ) {
19844 throw new Error( 'Minimum (' + min + ') must not be greater than maximum (' + max + ')' );
19845 }
19846 this.min = min;
19847 this.max = max;
19848 this.input.setValidityFlag();
19849 };
19850
19851 /**
19852 * Get the current range
19853 * @return {number[]} Minimum and maximum values
19854 */
19855 OO.ui.NumberInputWidget.prototype.getRange = function () {
19856 return [ this.min, this.max ];
19857 };
19858
19859 /**
19860 * Set the stepping deltas
19861 * @param {number} step Normal step
19862 * @param {number|null} pageStep Page step. If null, 10 * step will be used.
19863 */
19864 OO.ui.NumberInputWidget.prototype.setStep = function ( step, pageStep ) {
19865 if ( step <= 0 ) {
19866 throw new Error( 'Step value must be positive' );
19867 }
19868 if ( pageStep === null ) {
19869 pageStep = step * 10;
19870 } else if ( pageStep <= 0 ) {
19871 throw new Error( 'Page step value must be positive' );
19872 }
19873 this.step = step;
19874 this.pageStep = pageStep;
19875 };
19876
19877 /**
19878 * Get the current stepping values
19879 * @return {number[]} Step and page step
19880 */
19881 OO.ui.NumberInputWidget.prototype.getStep = function () {
19882 return [ this.step, this.pageStep ];
19883 };
19884
19885 /**
19886 * Get the current value of the widget
19887 * @return {string}
19888 */
19889 OO.ui.NumberInputWidget.prototype.getValue = function () {
19890 return this.input.getValue();
19891 };
19892
19893 /**
19894 * Get the current value of the widget as a number
19895 * @return {number} May be NaN, or an invalid number
19896 */
19897 OO.ui.NumberInputWidget.prototype.getNumericValue = function () {
19898 return +this.input.getValue();
19899 };
19900
19901 /**
19902 * Set the value of the widget
19903 * @param {string} value Invalid values are allowed
19904 */
19905 OO.ui.NumberInputWidget.prototype.setValue = function ( value ) {
19906 this.input.setValue( value );
19907 };
19908
19909 /**
19910 * Adjust the value of the widget
19911 * @param {number} delta Adjustment amount
19912 */
19913 OO.ui.NumberInputWidget.prototype.adjustValue = function ( delta ) {
19914 var n, v = this.getNumericValue();
19915
19916 delta = +delta;
19917 if ( isNaN( delta ) || !isFinite( delta ) ) {
19918 throw new Error( 'Delta must be a finite number' );
19919 }
19920
19921 if ( isNaN( v ) ) {
19922 n = 0;
19923 } else {
19924 n = v + delta;
19925 n = Math.max( Math.min( n, this.max ), this.min );
19926 if ( this.isInteger ) {
19927 n = Math.round( n );
19928 }
19929 }
19930
19931 if ( n !== v ) {
19932 this.setValue( n );
19933 }
19934 };
19935
19936 /**
19937 * Validate input
19938 * @private
19939 * @param {string} value Field value
19940 * @return {boolean}
19941 */
19942 OO.ui.NumberInputWidget.prototype.validateNumber = function ( value ) {
19943 var n = +value;
19944 if ( isNaN( n ) || !isFinite( n ) ) {
19945 return false;
19946 }
19947
19948 /*jshint bitwise: false */
19949 if ( this.isInteger && ( n | 0 ) !== n ) {
19950 return false;
19951 }
19952 /*jshint bitwise: true */
19953
19954 if ( n < this.min || n > this.max ) {
19955 return false;
19956 }
19957
19958 return true;
19959 };
19960
19961 /**
19962 * Handle mouse click events.
19963 *
19964 * @private
19965 * @param {number} dir +1 or -1
19966 */
19967 OO.ui.NumberInputWidget.prototype.onButtonClick = function ( dir ) {
19968 this.adjustValue( dir * this.step );
19969 };
19970
19971 /**
19972 * Handle mouse wheel events.
19973 *
19974 * @private
19975 * @param {jQuery.Event} event
19976 */
19977 OO.ui.NumberInputWidget.prototype.onWheel = function ( event ) {
19978 var delta = 0;
19979
19980 // Standard 'wheel' event
19981 if ( event.originalEvent.deltaMode !== undefined ) {
19982 this.sawWheelEvent = true;
19983 }
19984 if ( event.originalEvent.deltaY ) {
19985 delta = -event.originalEvent.deltaY;
19986 } else if ( event.originalEvent.deltaX ) {
19987 delta = event.originalEvent.deltaX;
19988 }
19989
19990 // Non-standard events
19991 if ( !this.sawWheelEvent ) {
19992 if ( event.originalEvent.wheelDeltaX ) {
19993 delta = -event.originalEvent.wheelDeltaX;
19994 } else if ( event.originalEvent.wheelDeltaY ) {
19995 delta = event.originalEvent.wheelDeltaY;
19996 } else if ( event.originalEvent.wheelDelta ) {
19997 delta = event.originalEvent.wheelDelta;
19998 } else if ( event.originalEvent.detail ) {
19999 delta = -event.originalEvent.detail;
20000 }
20001 }
20002
20003 if ( delta ) {
20004 delta = delta < 0 ? -1 : 1;
20005 this.adjustValue( delta * this.step );
20006 }
20007
20008 return false;
20009 };
20010
20011 /**
20012 * Handle key down events.
20013 *
20014 * @private
20015 * @param {jQuery.Event} e Key down event
20016 */
20017 OO.ui.NumberInputWidget.prototype.onKeyDown = function ( e ) {
20018 if ( !this.isDisabled() ) {
20019 switch ( e.which ) {
20020 case OO.ui.Keys.UP:
20021 this.adjustValue( this.step );
20022 return false;
20023 case OO.ui.Keys.DOWN:
20024 this.adjustValue( -this.step );
20025 return false;
20026 case OO.ui.Keys.PAGEUP:
20027 this.adjustValue( this.pageStep );
20028 return false;
20029 case OO.ui.Keys.PAGEDOWN:
20030 this.adjustValue( -this.pageStep );
20031 return false;
20032 }
20033 }
20034 };
20035
20036 /**
20037 * @inheritdoc
20038 */
20039 OO.ui.NumberInputWidget.prototype.setDisabled = function ( disabled ) {
20040 // Parent method
20041 OO.ui.NumberInputWidget.parent.prototype.setDisabled.call( this, disabled );
20042
20043 if ( this.input ) {
20044 this.input.setDisabled( this.isDisabled() );
20045 }
20046 if ( this.minusButton ) {
20047 this.minusButton.setDisabled( this.isDisabled() );
20048 }
20049 if ( this.plusButton ) {
20050 this.plusButton.setDisabled( this.isDisabled() );
20051 }
20052
20053 return this;
20054 };
20055
20056 /**
20057 * ToggleSwitches are switches that slide on and off. Their state is represented by a Boolean
20058 * value (`true` for ‘on’, and `false` otherwise, the default). The ‘off’ state is represented
20059 * visually by a slider in the leftmost position.
20060 *
20061 * @example
20062 * // Toggle switches in the 'off' and 'on' position.
20063 * var toggleSwitch1 = new OO.ui.ToggleSwitchWidget();
20064 * var toggleSwitch2 = new OO.ui.ToggleSwitchWidget( {
20065 * value: true
20066 * } );
20067 *
20068 * // Create a FieldsetLayout to layout and label switches
20069 * var fieldset = new OO.ui.FieldsetLayout( {
20070 * label: 'Toggle switches'
20071 * } );
20072 * fieldset.addItems( [
20073 * new OO.ui.FieldLayout( toggleSwitch1, { label: 'Off', align: 'top' } ),
20074 * new OO.ui.FieldLayout( toggleSwitch2, { label: 'On', align: 'top' } )
20075 * ] );
20076 * $( 'body' ).append( fieldset.$element );
20077 *
20078 * @class
20079 * @extends OO.ui.ToggleWidget
20080 * @mixins OO.ui.mixin.TabIndexedElement
20081 *
20082 * @constructor
20083 * @param {Object} [config] Configuration options
20084 * @cfg {boolean} [value=false] The toggle switch’s initial on/off state.
20085 * By default, the toggle switch is in the 'off' position.
20086 */
20087 OO.ui.ToggleSwitchWidget = function OoUiToggleSwitchWidget( config ) {
20088 // Parent constructor
20089 OO.ui.ToggleSwitchWidget.parent.call( this, config );
20090
20091 // Mixin constructors
20092 OO.ui.mixin.TabIndexedElement.call( this, config );
20093
20094 // Properties
20095 this.dragging = false;
20096 this.dragStart = null;
20097 this.sliding = false;
20098 this.$glow = $( '<span>' );
20099 this.$grip = $( '<span>' );
20100
20101 // Events
20102 this.$element.on( {
20103 click: this.onClick.bind( this ),
20104 keypress: this.onKeyPress.bind( this )
20105 } );
20106
20107 // Initialization
20108 this.$glow.addClass( 'oo-ui-toggleSwitchWidget-glow' );
20109 this.$grip.addClass( 'oo-ui-toggleSwitchWidget-grip' );
20110 this.$element
20111 .addClass( 'oo-ui-toggleSwitchWidget' )
20112 .attr( 'role', 'checkbox' )
20113 .append( this.$glow, this.$grip );
20114 };
20115
20116 /* Setup */
20117
20118 OO.inheritClass( OO.ui.ToggleSwitchWidget, OO.ui.ToggleWidget );
20119 OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.mixin.TabIndexedElement );
20120
20121 /* Methods */
20122
20123 /**
20124 * Handle mouse click events.
20125 *
20126 * @private
20127 * @param {jQuery.Event} e Mouse click event
20128 */
20129 OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
20130 if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
20131 this.setValue( !this.value );
20132 }
20133 return false;
20134 };
20135
20136 /**
20137 * Handle key press events.
20138 *
20139 * @private
20140 * @param {jQuery.Event} e Key press event
20141 */
20142 OO.ui.ToggleSwitchWidget.prototype.onKeyPress = function ( e ) {
20143 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
20144 this.setValue( !this.value );
20145 return false;
20146 }
20147 };
20148
20149 }( OO ) );