Update OOjs UI to v0.15.2
[lhc/web/wiklou.git] / resources / lib / oojs-ui / oojs-ui.js
1 /*!
2 * OOjs UI v0.15.2
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-02-02T22:07:00Z
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 * Mixin namespace.
449 */
450
451 /**
452 * Namespace for OOjs UI mixins.
453 *
454 * Mixins are named according to the type of object they are intended to
455 * be mixed in to. For example, OO.ui.mixin.GroupElement is intended to be
456 * mixed in to an instance of OO.ui.Element, and OO.ui.mixin.GroupWidget
457 * is intended to be mixed in to an instance of OO.ui.Widget.
458 *
459 * @class
460 * @singleton
461 */
462 OO.ui.mixin = {};
463
464 /**
465 * Each Element represents a rendering in the DOM—a button or an icon, for example, or anything
466 * that is visible to a user. Unlike {@link OO.ui.Widget widgets}, plain elements usually do not have events
467 * connected to them and can't be interacted with.
468 *
469 * @abstract
470 * @class
471 *
472 * @constructor
473 * @param {Object} [config] Configuration options
474 * @cfg {string[]} [classes] The names of the CSS classes to apply to the element. CSS styles are added
475 * to the top level (e.g., the outermost div) of the element. See the [OOjs UI documentation on MediaWiki][2]
476 * for an example.
477 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#cssExample
478 * @cfg {string} [id] The HTML id attribute used in the rendered tag.
479 * @cfg {string} [text] Text to insert
480 * @cfg {Array} [content] An array of content elements to append (after #text).
481 * Strings will be html-escaped; use an OO.ui.HtmlSnippet to append raw HTML.
482 * Instances of OO.ui.Element will have their $element appended.
483 * @cfg {jQuery} [$content] Content elements to append (after #text).
484 * @cfg {jQuery} [$element] Wrapper element. Defaults to a new element with #getTagName.
485 * @cfg {Mixed} [data] Custom data of any type or combination of types (e.g., string, number, array, object).
486 * Data can also be specified with the #setData method.
487 */
488 OO.ui.Element = function OoUiElement( config ) {
489 // Configuration initialization
490 config = config || {};
491
492 // Properties
493 this.$ = $;
494 this.visible = true;
495 this.data = config.data;
496 this.$element = config.$element ||
497 $( document.createElement( this.getTagName() ) );
498 this.elementGroup = null;
499 this.debouncedUpdateThemeClassesHandler = OO.ui.debounce( this.debouncedUpdateThemeClasses );
500
501 // Initialization
502 if ( Array.isArray( config.classes ) ) {
503 this.$element.addClass( config.classes.join( ' ' ) );
504 }
505 if ( config.id ) {
506 this.$element.attr( 'id', config.id );
507 }
508 if ( config.text ) {
509 this.$element.text( config.text );
510 }
511 if ( config.content ) {
512 // The `content` property treats plain strings as text; use an
513 // HtmlSnippet to append HTML content. `OO.ui.Element`s get their
514 // appropriate $element appended.
515 this.$element.append( config.content.map( function ( v ) {
516 if ( typeof v === 'string' ) {
517 // Escape string so it is properly represented in HTML.
518 return document.createTextNode( v );
519 } else if ( v instanceof OO.ui.HtmlSnippet ) {
520 // Bypass escaping.
521 return v.toString();
522 } else if ( v instanceof OO.ui.Element ) {
523 return v.$element;
524 }
525 return v;
526 } ) );
527 }
528 if ( config.$content ) {
529 // The `$content` property treats plain strings as HTML.
530 this.$element.append( config.$content );
531 }
532 };
533
534 /* Setup */
535
536 OO.initClass( OO.ui.Element );
537
538 /* Static Properties */
539
540 /**
541 * The name of the HTML tag used by the element.
542 *
543 * The static value may be ignored if the #getTagName method is overridden.
544 *
545 * @static
546 * @inheritable
547 * @property {string}
548 */
549 OO.ui.Element.static.tagName = 'div';
550
551 /* Static Methods */
552
553 /**
554 * Reconstitute a JavaScript object corresponding to a widget created
555 * by the PHP implementation.
556 *
557 * @param {string|HTMLElement|jQuery} idOrNode
558 * A DOM id (if a string) or node for the widget to infuse.
559 * @return {OO.ui.Element}
560 * The `OO.ui.Element` corresponding to this (infusable) document node.
561 * For `Tag` objects emitted on the HTML side (used occasionally for content)
562 * the value returned is a newly-created Element wrapping around the existing
563 * DOM node.
564 */
565 OO.ui.Element.static.infuse = function ( idOrNode ) {
566 var obj = OO.ui.Element.static.unsafeInfuse( idOrNode, false );
567 // Verify that the type matches up.
568 // FIXME: uncomment after T89721 is fixed (see T90929)
569 /*
570 if ( !( obj instanceof this['class'] ) ) {
571 throw new Error( 'Infusion type mismatch!' );
572 }
573 */
574 return obj;
575 };
576
577 /**
578 * Implementation helper for `infuse`; skips the type check and has an
579 * extra property so that only the top-level invocation touches the DOM.
580 * @private
581 * @param {string|HTMLElement|jQuery} idOrNode
582 * @param {jQuery.Promise|boolean} domPromise A promise that will be resolved
583 * when the top-level widget of this infusion is inserted into DOM,
584 * replacing the original node; or false for top-level invocation.
585 * @return {OO.ui.Element}
586 */
587 OO.ui.Element.static.unsafeInfuse = function ( idOrNode, domPromise ) {
588 // look for a cached result of a previous infusion.
589 var id, $elem, data, cls, parts, parent, obj, top, state;
590 if ( typeof idOrNode === 'string' ) {
591 id = idOrNode;
592 $elem = $( document.getElementById( id ) );
593 } else {
594 $elem = $( idOrNode );
595 id = $elem.attr( 'id' );
596 }
597 if ( !$elem.length ) {
598 throw new Error( 'Widget not found: ' + id );
599 }
600 data = $elem.data( 'ooui-infused' ) || $elem[ 0 ].oouiInfused;
601 if ( data ) {
602 // cached!
603 if ( data === true ) {
604 throw new Error( 'Circular dependency! ' + id );
605 }
606 return data;
607 }
608 data = $elem.attr( 'data-ooui' );
609 if ( !data ) {
610 throw new Error( 'No infusion data found: ' + id );
611 }
612 try {
613 data = $.parseJSON( data );
614 } catch ( _ ) {
615 data = null;
616 }
617 if ( !( data && data._ ) ) {
618 throw new Error( 'No valid infusion data found: ' + id );
619 }
620 if ( data._ === 'Tag' ) {
621 // Special case: this is a raw Tag; wrap existing node, don't rebuild.
622 return new OO.ui.Element( { $element: $elem } );
623 }
624 parts = data._.split( '.' );
625 cls = OO.getProp.apply( OO, [ window ].concat( parts ) );
626 if ( cls === undefined ) {
627 // The PHP output might be old and not including the "OO.ui" prefix
628 // TODO: Remove this back-compat after next major release
629 cls = OO.getProp.apply( OO, [ OO.ui ].concat( parts ) );
630 if ( cls === undefined ) {
631 throw new Error( 'Unknown widget type: id: ' + id + ', class: ' + data._ );
632 }
633 }
634
635 // Verify that we're creating an OO.ui.Element instance
636 parent = cls.parent;
637
638 while ( parent !== undefined ) {
639 if ( parent === OO.ui.Element ) {
640 // Safe
641 break;
642 }
643
644 parent = parent.parent;
645 }
646
647 if ( parent !== OO.ui.Element ) {
648 throw new Error( 'Unknown widget type: id: ' + id + ', class: ' + data._ );
649 }
650
651 if ( domPromise === false ) {
652 top = $.Deferred();
653 domPromise = top.promise();
654 }
655 $elem.data( 'ooui-infused', true ); // prevent loops
656 data.id = id; // implicit
657 data = OO.copy( data, null, function deserialize( value ) {
658 if ( OO.isPlainObject( value ) ) {
659 if ( value.tag ) {
660 return OO.ui.Element.static.unsafeInfuse( value.tag, domPromise );
661 }
662 if ( value.html ) {
663 return new OO.ui.HtmlSnippet( value.html );
664 }
665 }
666 } );
667 // allow widgets to reuse parts of the DOM
668 data = cls.static.reusePreInfuseDOM( $elem[ 0 ], data );
669 // pick up dynamic state, like focus, value of form inputs, scroll position, etc.
670 state = cls.static.gatherPreInfuseState( $elem[ 0 ], data );
671 // rebuild widget
672 // jscs:disable requireCapitalizedConstructors
673 obj = new cls( data );
674 // jscs:enable requireCapitalizedConstructors
675 // now replace old DOM with this new DOM.
676 if ( top ) {
677 // An efficient constructor might be able to reuse the entire DOM tree of the original element,
678 // so only mutate the DOM if we need to.
679 if ( $elem[ 0 ] !== obj.$element[ 0 ] ) {
680 $elem.replaceWith( obj.$element );
681 // This element is now gone from the DOM, but if anyone is holding a reference to it,
682 // let's allow them to OO.ui.infuse() it and do what they expect (T105828).
683 // Do not use jQuery.data(), as using it on detached nodes leaks memory in 1.x line by design.
684 $elem[ 0 ].oouiInfused = obj;
685 }
686 top.resolve();
687 }
688 obj.$element.data( 'ooui-infused', obj );
689 // set the 'data-ooui' attribute so we can identify infused widgets
690 obj.$element.attr( 'data-ooui', '' );
691 // restore dynamic state after the new element is inserted into DOM
692 domPromise.done( obj.restorePreInfuseState.bind( obj, state ) );
693 return obj;
694 };
695
696 /**
697 * Pick out parts of `node`'s DOM to be reused when infusing a widget.
698 *
699 * This method **must not** make any changes to the DOM, only find interesting pieces and add them
700 * to `config` (which should then be returned). Actual DOM juggling should then be done by the
701 * constructor, which will be given the enhanced config.
702 *
703 * @protected
704 * @param {HTMLElement} node
705 * @param {Object} config
706 * @return {Object}
707 */
708 OO.ui.Element.static.reusePreInfuseDOM = function ( node, config ) {
709 return config;
710 };
711
712 /**
713 * Gather the dynamic state (focus, value of form inputs, scroll position, etc.) of a HTML DOM node
714 * (and its children) that represent an Element of the same class and the given configuration,
715 * generated by the PHP implementation.
716 *
717 * This method is called just before `node` is detached from the DOM. The return value of this
718 * function will be passed to #restorePreInfuseState after the newly created widget's #$element
719 * is inserted into DOM to replace `node`.
720 *
721 * @protected
722 * @param {HTMLElement} node
723 * @param {Object} config
724 * @return {Object}
725 */
726 OO.ui.Element.static.gatherPreInfuseState = function () {
727 return {};
728 };
729
730 /**
731 * Get a jQuery function within a specific document.
732 *
733 * @static
734 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
735 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
736 * not in an iframe
737 * @return {Function} Bound jQuery function
738 */
739 OO.ui.Element.static.getJQuery = function ( context, $iframe ) {
740 function wrapper( selector ) {
741 return $( selector, wrapper.context );
742 }
743
744 wrapper.context = this.getDocument( context );
745
746 if ( $iframe ) {
747 wrapper.$iframe = $iframe;
748 }
749
750 return wrapper;
751 };
752
753 /**
754 * Get the document of an element.
755 *
756 * @static
757 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
758 * @return {HTMLDocument|null} Document object
759 */
760 OO.ui.Element.static.getDocument = function ( obj ) {
761 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
762 return ( obj[ 0 ] && obj[ 0 ].ownerDocument ) ||
763 // Empty jQuery selections might have a context
764 obj.context ||
765 // HTMLElement
766 obj.ownerDocument ||
767 // Window
768 obj.document ||
769 // HTMLDocument
770 ( obj.nodeType === 9 && obj ) ||
771 null;
772 };
773
774 /**
775 * Get the window of an element or document.
776 *
777 * @static
778 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
779 * @return {Window} Window object
780 */
781 OO.ui.Element.static.getWindow = function ( obj ) {
782 var doc = this.getDocument( obj );
783 return doc.defaultView;
784 };
785
786 /**
787 * Get the direction of an element or document.
788 *
789 * @static
790 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
791 * @return {string} Text direction, either 'ltr' or 'rtl'
792 */
793 OO.ui.Element.static.getDir = function ( obj ) {
794 var isDoc, isWin;
795
796 if ( obj instanceof jQuery ) {
797 obj = obj[ 0 ];
798 }
799 isDoc = obj.nodeType === 9;
800 isWin = obj.document !== undefined;
801 if ( isDoc || isWin ) {
802 if ( isWin ) {
803 obj = obj.document;
804 }
805 obj = obj.body;
806 }
807 return $( obj ).css( 'direction' );
808 };
809
810 /**
811 * Get the offset between two frames.
812 *
813 * TODO: Make this function not use recursion.
814 *
815 * @static
816 * @param {Window} from Window of the child frame
817 * @param {Window} [to=window] Window of the parent frame
818 * @param {Object} [offset] Offset to start with, used internally
819 * @return {Object} Offset object, containing left and top properties
820 */
821 OO.ui.Element.static.getFrameOffset = function ( from, to, offset ) {
822 var i, len, frames, frame, rect;
823
824 if ( !to ) {
825 to = window;
826 }
827 if ( !offset ) {
828 offset = { top: 0, left: 0 };
829 }
830 if ( from.parent === from ) {
831 return offset;
832 }
833
834 // Get iframe element
835 frames = from.parent.document.getElementsByTagName( 'iframe' );
836 for ( i = 0, len = frames.length; i < len; i++ ) {
837 if ( frames[ i ].contentWindow === from ) {
838 frame = frames[ i ];
839 break;
840 }
841 }
842
843 // Recursively accumulate offset values
844 if ( frame ) {
845 rect = frame.getBoundingClientRect();
846 offset.left += rect.left;
847 offset.top += rect.top;
848 if ( from !== to ) {
849 this.getFrameOffset( from.parent, offset );
850 }
851 }
852 return offset;
853 };
854
855 /**
856 * Get the offset between two elements.
857 *
858 * The two elements may be in a different frame, but in that case the frame $element is in must
859 * be contained in the frame $anchor is in.
860 *
861 * @static
862 * @param {jQuery} $element Element whose position to get
863 * @param {jQuery} $anchor Element to get $element's position relative to
864 * @return {Object} Translated position coordinates, containing top and left properties
865 */
866 OO.ui.Element.static.getRelativePosition = function ( $element, $anchor ) {
867 var iframe, iframePos,
868 pos = $element.offset(),
869 anchorPos = $anchor.offset(),
870 elementDocument = this.getDocument( $element ),
871 anchorDocument = this.getDocument( $anchor );
872
873 // If $element isn't in the same document as $anchor, traverse up
874 while ( elementDocument !== anchorDocument ) {
875 iframe = elementDocument.defaultView.frameElement;
876 if ( !iframe ) {
877 throw new Error( '$element frame is not contained in $anchor frame' );
878 }
879 iframePos = $( iframe ).offset();
880 pos.left += iframePos.left;
881 pos.top += iframePos.top;
882 elementDocument = iframe.ownerDocument;
883 }
884 pos.left -= anchorPos.left;
885 pos.top -= anchorPos.top;
886 return pos;
887 };
888
889 /**
890 * Get element border sizes.
891 *
892 * @static
893 * @param {HTMLElement} el Element to measure
894 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
895 */
896 OO.ui.Element.static.getBorders = function ( el ) {
897 var doc = el.ownerDocument,
898 win = doc.defaultView,
899 style = win.getComputedStyle( el, null ),
900 $el = $( el ),
901 top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0,
902 left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0,
903 bottom = parseFloat( style ? style.borderBottomWidth : $el.css( 'borderBottomWidth' ) ) || 0,
904 right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0;
905
906 return {
907 top: top,
908 left: left,
909 bottom: bottom,
910 right: right
911 };
912 };
913
914 /**
915 * Get dimensions of an element or window.
916 *
917 * @static
918 * @param {HTMLElement|Window} el Element to measure
919 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
920 */
921 OO.ui.Element.static.getDimensions = function ( el ) {
922 var $el, $win,
923 doc = el.ownerDocument || el.document,
924 win = doc.defaultView;
925
926 if ( win === el || el === doc.documentElement ) {
927 $win = $( win );
928 return {
929 borders: { top: 0, left: 0, bottom: 0, right: 0 },
930 scroll: {
931 top: $win.scrollTop(),
932 left: $win.scrollLeft()
933 },
934 scrollbar: { right: 0, bottom: 0 },
935 rect: {
936 top: 0,
937 left: 0,
938 bottom: $win.innerHeight(),
939 right: $win.innerWidth()
940 }
941 };
942 } else {
943 $el = $( el );
944 return {
945 borders: this.getBorders( el ),
946 scroll: {
947 top: $el.scrollTop(),
948 left: $el.scrollLeft()
949 },
950 scrollbar: {
951 right: $el.innerWidth() - el.clientWidth,
952 bottom: $el.innerHeight() - el.clientHeight
953 },
954 rect: el.getBoundingClientRect()
955 };
956 }
957 };
958
959 /**
960 * Get scrollable object parent
961 *
962 * documentElement can't be used to get or set the scrollTop
963 * property on Blink. Changing and testing its value lets us
964 * use 'body' or 'documentElement' based on what is working.
965 *
966 * https://code.google.com/p/chromium/issues/detail?id=303131
967 *
968 * @static
969 * @param {HTMLElement} el Element to find scrollable parent for
970 * @return {HTMLElement} Scrollable parent
971 */
972 OO.ui.Element.static.getRootScrollableElement = function ( el ) {
973 var scrollTop, body;
974
975 if ( OO.ui.scrollableElement === undefined ) {
976 body = el.ownerDocument.body;
977 scrollTop = body.scrollTop;
978 body.scrollTop = 1;
979
980 if ( body.scrollTop === 1 ) {
981 body.scrollTop = scrollTop;
982 OO.ui.scrollableElement = 'body';
983 } else {
984 OO.ui.scrollableElement = 'documentElement';
985 }
986 }
987
988 return el.ownerDocument[ OO.ui.scrollableElement ];
989 };
990
991 /**
992 * Get closest scrollable container.
993 *
994 * Traverses up until either a scrollable element or the root is reached, in which case the window
995 * will be returned.
996 *
997 * @static
998 * @param {HTMLElement} el Element to find scrollable container for
999 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1000 * @return {HTMLElement} Closest scrollable container
1001 */
1002 OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) {
1003 var i, val,
1004 // props = [ 'overflow' ] doesn't work due to https://bugzilla.mozilla.org/show_bug.cgi?id=889091
1005 props = [ 'overflow-x', 'overflow-y' ],
1006 $parent = $( el ).parent();
1007
1008 if ( dimension === 'x' || dimension === 'y' ) {
1009 props = [ 'overflow-' + dimension ];
1010 }
1011
1012 while ( $parent.length ) {
1013 if ( $parent[ 0 ] === this.getRootScrollableElement( el ) ) {
1014 return $parent[ 0 ];
1015 }
1016 i = props.length;
1017 while ( i-- ) {
1018 val = $parent.css( props[ i ] );
1019 if ( val === 'auto' || val === 'scroll' ) {
1020 return $parent[ 0 ];
1021 }
1022 }
1023 $parent = $parent.parent();
1024 }
1025 return this.getDocument( el ).body;
1026 };
1027
1028 /**
1029 * Scroll element into view.
1030 *
1031 * @static
1032 * @param {HTMLElement} el Element to scroll into view
1033 * @param {Object} [config] Configuration options
1034 * @param {string} [config.duration] jQuery animation duration value
1035 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1036 * to scroll in both directions
1037 * @param {Function} [config.complete] Function to call when scrolling completes
1038 */
1039 OO.ui.Element.static.scrollIntoView = function ( el, config ) {
1040 var rel, anim, callback, sc, $sc, eld, scd, $win;
1041
1042 // Configuration initialization
1043 config = config || {};
1044
1045 anim = {};
1046 callback = typeof config.complete === 'function' && config.complete;
1047 sc = this.getClosestScrollableContainer( el, config.direction );
1048 $sc = $( sc );
1049 eld = this.getDimensions( el );
1050 scd = this.getDimensions( sc );
1051 $win = $( this.getWindow( el ) );
1052
1053 // Compute the distances between the edges of el and the edges of the scroll viewport
1054 if ( $sc.is( 'html, body' ) ) {
1055 // If the scrollable container is the root, this is easy
1056 rel = {
1057 top: eld.rect.top,
1058 bottom: $win.innerHeight() - eld.rect.bottom,
1059 left: eld.rect.left,
1060 right: $win.innerWidth() - eld.rect.right
1061 };
1062 } else {
1063 // Otherwise, we have to subtract el's coordinates from sc's coordinates
1064 rel = {
1065 top: eld.rect.top - ( scd.rect.top + scd.borders.top ),
1066 bottom: scd.rect.bottom - scd.borders.bottom - scd.scrollbar.bottom - eld.rect.bottom,
1067 left: eld.rect.left - ( scd.rect.left + scd.borders.left ),
1068 right: scd.rect.right - scd.borders.right - scd.scrollbar.right - eld.rect.right
1069 };
1070 }
1071
1072 if ( !config.direction || config.direction === 'y' ) {
1073 if ( rel.top < 0 ) {
1074 anim.scrollTop = scd.scroll.top + rel.top;
1075 } else if ( rel.top > 0 && rel.bottom < 0 ) {
1076 anim.scrollTop = scd.scroll.top + Math.min( rel.top, -rel.bottom );
1077 }
1078 }
1079 if ( !config.direction || config.direction === 'x' ) {
1080 if ( rel.left < 0 ) {
1081 anim.scrollLeft = scd.scroll.left + rel.left;
1082 } else if ( rel.left > 0 && rel.right < 0 ) {
1083 anim.scrollLeft = scd.scroll.left + Math.min( rel.left, -rel.right );
1084 }
1085 }
1086 if ( !$.isEmptyObject( anim ) ) {
1087 $sc.stop( true ).animate( anim, config.duration || 'fast' );
1088 if ( callback ) {
1089 $sc.queue( function ( next ) {
1090 callback();
1091 next();
1092 } );
1093 }
1094 } else {
1095 if ( callback ) {
1096 callback();
1097 }
1098 }
1099 };
1100
1101 /**
1102 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1103 * and reserve space for them, because it probably doesn't.
1104 *
1105 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1106 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1107 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1108 * and then reattach (or show) them back.
1109 *
1110 * @static
1111 * @param {HTMLElement} el Element to reconsider the scrollbars on
1112 */
1113 OO.ui.Element.static.reconsiderScrollbars = function ( el ) {
1114 var i, len, scrollLeft, scrollTop, nodes = [];
1115 // Save scroll position
1116 scrollLeft = el.scrollLeft;
1117 scrollTop = el.scrollTop;
1118 // Detach all children
1119 while ( el.firstChild ) {
1120 nodes.push( el.firstChild );
1121 el.removeChild( el.firstChild );
1122 }
1123 // Force reflow
1124 void el.offsetHeight;
1125 // Reattach all children
1126 for ( i = 0, len = nodes.length; i < len; i++ ) {
1127 el.appendChild( nodes[ i ] );
1128 }
1129 // Restore scroll position (no-op if scrollbars disappeared)
1130 el.scrollLeft = scrollLeft;
1131 el.scrollTop = scrollTop;
1132 };
1133
1134 /* Methods */
1135
1136 /**
1137 * Toggle visibility of an element.
1138 *
1139 * @param {boolean} [show] Make element visible, omit to toggle visibility
1140 * @fires visible
1141 * @chainable
1142 */
1143 OO.ui.Element.prototype.toggle = function ( show ) {
1144 show = show === undefined ? !this.visible : !!show;
1145
1146 if ( show !== this.isVisible() ) {
1147 this.visible = show;
1148 this.$element.toggleClass( 'oo-ui-element-hidden', !this.visible );
1149 this.emit( 'toggle', show );
1150 }
1151
1152 return this;
1153 };
1154
1155 /**
1156 * Check if element is visible.
1157 *
1158 * @return {boolean} element is visible
1159 */
1160 OO.ui.Element.prototype.isVisible = function () {
1161 return this.visible;
1162 };
1163
1164 /**
1165 * Get element data.
1166 *
1167 * @return {Mixed} Element data
1168 */
1169 OO.ui.Element.prototype.getData = function () {
1170 return this.data;
1171 };
1172
1173 /**
1174 * Set element data.
1175 *
1176 * @param {Mixed} Element data
1177 * @chainable
1178 */
1179 OO.ui.Element.prototype.setData = function ( data ) {
1180 this.data = data;
1181 return this;
1182 };
1183
1184 /**
1185 * Check if element supports one or more methods.
1186 *
1187 * @param {string|string[]} methods Method or list of methods to check
1188 * @return {boolean} All methods are supported
1189 */
1190 OO.ui.Element.prototype.supports = function ( methods ) {
1191 var i, len,
1192 support = 0;
1193
1194 methods = Array.isArray( methods ) ? methods : [ methods ];
1195 for ( i = 0, len = methods.length; i < len; i++ ) {
1196 if ( $.isFunction( this[ methods[ i ] ] ) ) {
1197 support++;
1198 }
1199 }
1200
1201 return methods.length === support;
1202 };
1203
1204 /**
1205 * Update the theme-provided classes.
1206 *
1207 * @localdoc This is called in element mixins and widget classes any time state changes.
1208 * Updating is debounced, minimizing overhead of changing multiple attributes and
1209 * guaranteeing that theme updates do not occur within an element's constructor
1210 */
1211 OO.ui.Element.prototype.updateThemeClasses = function () {
1212 this.debouncedUpdateThemeClassesHandler();
1213 };
1214
1215 /**
1216 * @private
1217 * @localdoc This method is called directly from the QUnit tests instead of #updateThemeClasses, to
1218 * make them synchronous.
1219 */
1220 OO.ui.Element.prototype.debouncedUpdateThemeClasses = function () {
1221 OO.ui.theme.updateElementClasses( this );
1222 };
1223
1224 /**
1225 * Get the HTML tag name.
1226 *
1227 * Override this method to base the result on instance information.
1228 *
1229 * @return {string} HTML tag name
1230 */
1231 OO.ui.Element.prototype.getTagName = function () {
1232 return this.constructor.static.tagName;
1233 };
1234
1235 /**
1236 * Check if the element is attached to the DOM
1237 * @return {boolean} The element is attached to the DOM
1238 */
1239 OO.ui.Element.prototype.isElementAttached = function () {
1240 return $.contains( this.getElementDocument(), this.$element[ 0 ] );
1241 };
1242
1243 /**
1244 * Get the DOM document.
1245 *
1246 * @return {HTMLDocument} Document object
1247 */
1248 OO.ui.Element.prototype.getElementDocument = function () {
1249 // Don't cache this in other ways either because subclasses could can change this.$element
1250 return OO.ui.Element.static.getDocument( this.$element );
1251 };
1252
1253 /**
1254 * Get the DOM window.
1255 *
1256 * @return {Window} Window object
1257 */
1258 OO.ui.Element.prototype.getElementWindow = function () {
1259 return OO.ui.Element.static.getWindow( this.$element );
1260 };
1261
1262 /**
1263 * Get closest scrollable container.
1264 */
1265 OO.ui.Element.prototype.getClosestScrollableElementContainer = function () {
1266 return OO.ui.Element.static.getClosestScrollableContainer( this.$element[ 0 ] );
1267 };
1268
1269 /**
1270 * Get group element is in.
1271 *
1272 * @return {OO.ui.mixin.GroupElement|null} Group element, null if none
1273 */
1274 OO.ui.Element.prototype.getElementGroup = function () {
1275 return this.elementGroup;
1276 };
1277
1278 /**
1279 * Set group element is in.
1280 *
1281 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
1282 * @chainable
1283 */
1284 OO.ui.Element.prototype.setElementGroup = function ( group ) {
1285 this.elementGroup = group;
1286 return this;
1287 };
1288
1289 /**
1290 * Scroll element into view.
1291 *
1292 * @param {Object} [config] Configuration options
1293 */
1294 OO.ui.Element.prototype.scrollElementIntoView = function ( config ) {
1295 return OO.ui.Element.static.scrollIntoView( this.$element[ 0 ], config );
1296 };
1297
1298 /**
1299 * Restore the pre-infusion dynamic state for this widget.
1300 *
1301 * This method is called after #$element has been inserted into DOM. The parameter is the return
1302 * value of #gatherPreInfuseState.
1303 *
1304 * @protected
1305 * @param {Object} state
1306 */
1307 OO.ui.Element.prototype.restorePreInfuseState = function () {
1308 };
1309
1310 /**
1311 * Wraps an HTML snippet for use with configuration values which default
1312 * to strings. This bypasses the default html-escaping done to string
1313 * values.
1314 *
1315 * @class
1316 *
1317 * @constructor
1318 * @param {string} [content] HTML content
1319 */
1320 OO.ui.HtmlSnippet = function OoUiHtmlSnippet( content ) {
1321 // Properties
1322 this.content = content;
1323 };
1324
1325 /* Setup */
1326
1327 OO.initClass( OO.ui.HtmlSnippet );
1328
1329 /* Methods */
1330
1331 /**
1332 * Render into HTML.
1333 *
1334 * @return {string} Unchanged HTML snippet.
1335 */
1336 OO.ui.HtmlSnippet.prototype.toString = function () {
1337 return this.content;
1338 };
1339
1340 /**
1341 * Layouts are containers for elements and are used to arrange other widgets of arbitrary type in a way
1342 * that is centrally controlled and can be updated dynamically. Layouts can be, and usually are, combined.
1343 * See {@link OO.ui.FieldsetLayout FieldsetLayout}, {@link OO.ui.FieldLayout FieldLayout}, {@link OO.ui.FormLayout FormLayout},
1344 * {@link OO.ui.PanelLayout PanelLayout}, {@link OO.ui.StackLayout StackLayout}, {@link OO.ui.PageLayout PageLayout},
1345 * {@link OO.ui.HorizontalLayout HorizontalLayout}, and {@link OO.ui.BookletLayout BookletLayout} for more information and examples.
1346 *
1347 * @abstract
1348 * @class
1349 * @extends OO.ui.Element
1350 * @mixins OO.EventEmitter
1351 *
1352 * @constructor
1353 * @param {Object} [config] Configuration options
1354 */
1355 OO.ui.Layout = function OoUiLayout( config ) {
1356 // Configuration initialization
1357 config = config || {};
1358
1359 // Parent constructor
1360 OO.ui.Layout.parent.call( this, config );
1361
1362 // Mixin constructors
1363 OO.EventEmitter.call( this );
1364
1365 // Initialization
1366 this.$element.addClass( 'oo-ui-layout' );
1367 };
1368
1369 /* Setup */
1370
1371 OO.inheritClass( OO.ui.Layout, OO.ui.Element );
1372 OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
1373
1374 /**
1375 * Widgets are compositions of one or more OOjs UI elements that users can both view
1376 * and interact with. All widgets can be configured and modified via a standard API,
1377 * and their state can change dynamically according to a model.
1378 *
1379 * @abstract
1380 * @class
1381 * @extends OO.ui.Element
1382 * @mixins OO.EventEmitter
1383 *
1384 * @constructor
1385 * @param {Object} [config] Configuration options
1386 * @cfg {boolean} [disabled=false] Disable the widget. Disabled widgets cannot be used and their
1387 * appearance reflects this state.
1388 */
1389 OO.ui.Widget = function OoUiWidget( config ) {
1390 // Initialize config
1391 config = $.extend( { disabled: false }, config );
1392
1393 // Parent constructor
1394 OO.ui.Widget.parent.call( this, config );
1395
1396 // Mixin constructors
1397 OO.EventEmitter.call( this );
1398
1399 // Properties
1400 this.disabled = null;
1401 this.wasDisabled = null;
1402
1403 // Initialization
1404 this.$element.addClass( 'oo-ui-widget' );
1405 this.setDisabled( !!config.disabled );
1406 };
1407
1408 /* Setup */
1409
1410 OO.inheritClass( OO.ui.Widget, OO.ui.Element );
1411 OO.mixinClass( OO.ui.Widget, OO.EventEmitter );
1412
1413 /* Static Properties */
1414
1415 /**
1416 * Whether this widget will behave reasonably when wrapped in a HTML `<label>`. If this is true,
1417 * wrappers such as OO.ui.FieldLayout may use a `<label>` instead of implementing own label click
1418 * handling.
1419 *
1420 * @static
1421 * @inheritable
1422 * @property {boolean}
1423 */
1424 OO.ui.Widget.static.supportsSimpleLabel = false;
1425
1426 /* Events */
1427
1428 /**
1429 * @event disable
1430 *
1431 * A 'disable' event is emitted when the disabled state of the widget changes
1432 * (i.e. on disable **and** enable).
1433 *
1434 * @param {boolean} disabled Widget is disabled
1435 */
1436
1437 /**
1438 * @event toggle
1439 *
1440 * A 'toggle' event is emitted when the visibility of the widget changes.
1441 *
1442 * @param {boolean} visible Widget is visible
1443 */
1444
1445 /* Methods */
1446
1447 /**
1448 * Check if the widget is disabled.
1449 *
1450 * @return {boolean} Widget is disabled
1451 */
1452 OO.ui.Widget.prototype.isDisabled = function () {
1453 return this.disabled;
1454 };
1455
1456 /**
1457 * Set the 'disabled' state of the widget.
1458 *
1459 * When a widget is disabled, it cannot be used and its appearance is updated to reflect this state.
1460 *
1461 * @param {boolean} disabled Disable widget
1462 * @chainable
1463 */
1464 OO.ui.Widget.prototype.setDisabled = function ( disabled ) {
1465 var isDisabled;
1466
1467 this.disabled = !!disabled;
1468 isDisabled = this.isDisabled();
1469 if ( isDisabled !== this.wasDisabled ) {
1470 this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled );
1471 this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled );
1472 this.$element.attr( 'aria-disabled', isDisabled.toString() );
1473 this.emit( 'disable', isDisabled );
1474 this.updateThemeClasses();
1475 }
1476 this.wasDisabled = isDisabled;
1477
1478 return this;
1479 };
1480
1481 /**
1482 * Update the disabled state, in case of changes in parent widget.
1483 *
1484 * @chainable
1485 */
1486 OO.ui.Widget.prototype.updateDisabled = function () {
1487 this.setDisabled( this.disabled );
1488 return this;
1489 };
1490
1491 /**
1492 * Theme logic.
1493 *
1494 * @abstract
1495 * @class
1496 *
1497 * @constructor
1498 * @param {Object} [config] Configuration options
1499 */
1500 OO.ui.Theme = function OoUiTheme( config ) {
1501 // Configuration initialization
1502 config = config || {};
1503 };
1504
1505 /* Setup */
1506
1507 OO.initClass( OO.ui.Theme );
1508
1509 /* Methods */
1510
1511 /**
1512 * Get a list of classes to be applied to a widget.
1513 *
1514 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
1515 * otherwise state transitions will not work properly.
1516 *
1517 * @param {OO.ui.Element} element Element for which to get classes
1518 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1519 */
1520 OO.ui.Theme.prototype.getElementClasses = function () {
1521 return { on: [], off: [] };
1522 };
1523
1524 /**
1525 * Update CSS classes provided by the theme.
1526 *
1527 * For elements with theme logic hooks, this should be called any time there's a state change.
1528 *
1529 * @param {OO.ui.Element} element Element for which to update classes
1530 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
1531 */
1532 OO.ui.Theme.prototype.updateElementClasses = function ( element ) {
1533 var $elements = $( [] ),
1534 classes = this.getElementClasses( element );
1535
1536 if ( element.$icon ) {
1537 $elements = $elements.add( element.$icon );
1538 }
1539 if ( element.$indicator ) {
1540 $elements = $elements.add( element.$indicator );
1541 }
1542
1543 $elements
1544 .removeClass( classes.off.join( ' ' ) )
1545 .addClass( classes.on.join( ' ' ) );
1546 };
1547
1548 /**
1549 * The TabIndexedElement class is an attribute mixin used to add additional functionality to an
1550 * element created by another class. The mixin provides a ‘tabIndex’ property, which specifies the
1551 * order in which users will navigate through the focusable elements via the "tab" key.
1552 *
1553 * @example
1554 * // TabIndexedElement is mixed into the ButtonWidget class
1555 * // to provide a tabIndex property.
1556 * var button1 = new OO.ui.ButtonWidget( {
1557 * label: 'fourth',
1558 * tabIndex: 4
1559 * } );
1560 * var button2 = new OO.ui.ButtonWidget( {
1561 * label: 'second',
1562 * tabIndex: 2
1563 * } );
1564 * var button3 = new OO.ui.ButtonWidget( {
1565 * label: 'third',
1566 * tabIndex: 3
1567 * } );
1568 * var button4 = new OO.ui.ButtonWidget( {
1569 * label: 'first',
1570 * tabIndex: 1
1571 * } );
1572 * $( 'body' ).append( button1.$element, button2.$element, button3.$element, button4.$element );
1573 *
1574 * @abstract
1575 * @class
1576 *
1577 * @constructor
1578 * @param {Object} [config] Configuration options
1579 * @cfg {jQuery} [$tabIndexed] The element that should use the tabindex functionality. By default,
1580 * the functionality is applied to the element created by the class ($element). If a different element is specified, the tabindex
1581 * functionality will be applied to it instead.
1582 * @cfg {number|null} [tabIndex=0] Number that specifies the element’s position in the tab-navigation
1583 * order (e.g., 1 for the first focusable element). Use 0 to use the default navigation order; use -1
1584 * to remove the element from the tab-navigation flow.
1585 */
1586 OO.ui.mixin.TabIndexedElement = function OoUiMixinTabIndexedElement( config ) {
1587 // Configuration initialization
1588 config = $.extend( { tabIndex: 0 }, config );
1589
1590 // Properties
1591 this.$tabIndexed = null;
1592 this.tabIndex = null;
1593
1594 // Events
1595 this.connect( this, { disable: 'onTabIndexedElementDisable' } );
1596
1597 // Initialization
1598 this.setTabIndex( config.tabIndex );
1599 this.setTabIndexedElement( config.$tabIndexed || this.$element );
1600 };
1601
1602 /* Setup */
1603
1604 OO.initClass( OO.ui.mixin.TabIndexedElement );
1605
1606 /* Methods */
1607
1608 /**
1609 * Set the element that should use the tabindex functionality.
1610 *
1611 * This method is used to retarget a tabindex mixin so that its functionality applies
1612 * to the specified element. If an element is currently using the functionality, the mixin’s
1613 * effect on that element is removed before the new element is set up.
1614 *
1615 * @param {jQuery} $tabIndexed Element that should use the tabindex functionality
1616 * @chainable
1617 */
1618 OO.ui.mixin.TabIndexedElement.prototype.setTabIndexedElement = function ( $tabIndexed ) {
1619 var tabIndex = this.tabIndex;
1620 // Remove attributes from old $tabIndexed
1621 this.setTabIndex( null );
1622 // Force update of new $tabIndexed
1623 this.$tabIndexed = $tabIndexed;
1624 this.tabIndex = tabIndex;
1625 return this.updateTabIndex();
1626 };
1627
1628 /**
1629 * Set the value of the tabindex.
1630 *
1631 * @param {number|null} tabIndex Tabindex value, or `null` for no tabindex
1632 * @chainable
1633 */
1634 OO.ui.mixin.TabIndexedElement.prototype.setTabIndex = function ( tabIndex ) {
1635 tabIndex = typeof tabIndex === 'number' ? tabIndex : null;
1636
1637 if ( this.tabIndex !== tabIndex ) {
1638 this.tabIndex = tabIndex;
1639 this.updateTabIndex();
1640 }
1641
1642 return this;
1643 };
1644
1645 /**
1646 * Update the `tabindex` attribute, in case of changes to tab index or
1647 * disabled state.
1648 *
1649 * @private
1650 * @chainable
1651 */
1652 OO.ui.mixin.TabIndexedElement.prototype.updateTabIndex = function () {
1653 if ( this.$tabIndexed ) {
1654 if ( this.tabIndex !== null ) {
1655 // Do not index over disabled elements
1656 this.$tabIndexed.attr( {
1657 tabindex: this.isDisabled() ? -1 : this.tabIndex,
1658 // Support: ChromeVox and NVDA
1659 // These do not seem to inherit aria-disabled from parent elements
1660 'aria-disabled': this.isDisabled().toString()
1661 } );
1662 } else {
1663 this.$tabIndexed.removeAttr( 'tabindex aria-disabled' );
1664 }
1665 }
1666 return this;
1667 };
1668
1669 /**
1670 * Handle disable events.
1671 *
1672 * @private
1673 * @param {boolean} disabled Element is disabled
1674 */
1675 OO.ui.mixin.TabIndexedElement.prototype.onTabIndexedElementDisable = function () {
1676 this.updateTabIndex();
1677 };
1678
1679 /**
1680 * Get the value of the tabindex.
1681 *
1682 * @return {number|null} Tabindex value
1683 */
1684 OO.ui.mixin.TabIndexedElement.prototype.getTabIndex = function () {
1685 return this.tabIndex;
1686 };
1687
1688 /**
1689 * ButtonElement is often mixed into other classes to generate a button, which is a clickable
1690 * interface element that can be configured with access keys for accessibility.
1691 * See the [OOjs UI documentation on MediaWiki] [1] for examples.
1692 *
1693 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Buttons
1694 * @abstract
1695 * @class
1696 *
1697 * @constructor
1698 * @param {Object} [config] Configuration options
1699 * @cfg {jQuery} [$button] The button element created by the class.
1700 * If this configuration is omitted, the button element will use a generated `<a>`.
1701 * @cfg {boolean} [framed=true] Render the button with a frame
1702 */
1703 OO.ui.mixin.ButtonElement = function OoUiMixinButtonElement( config ) {
1704 // Configuration initialization
1705 config = config || {};
1706
1707 // Properties
1708 this.$button = null;
1709 this.framed = null;
1710 this.active = false;
1711 this.onMouseUpHandler = this.onMouseUp.bind( this );
1712 this.onMouseDownHandler = this.onMouseDown.bind( this );
1713 this.onKeyDownHandler = this.onKeyDown.bind( this );
1714 this.onKeyUpHandler = this.onKeyUp.bind( this );
1715 this.onClickHandler = this.onClick.bind( this );
1716 this.onKeyPressHandler = this.onKeyPress.bind( this );
1717
1718 // Initialization
1719 this.$element.addClass( 'oo-ui-buttonElement' );
1720 this.toggleFramed( config.framed === undefined || config.framed );
1721 this.setButtonElement( config.$button || $( '<a>' ) );
1722 };
1723
1724 /* Setup */
1725
1726 OO.initClass( OO.ui.mixin.ButtonElement );
1727
1728 /* Static Properties */
1729
1730 /**
1731 * Cancel mouse down events.
1732 *
1733 * This property is usually set to `true` to prevent the focus from changing when the button is clicked.
1734 * Classes such as {@link OO.ui.mixin.DraggableElement DraggableElement} and {@link OO.ui.ButtonOptionWidget ButtonOptionWidget}
1735 * use a value of `false` so that dragging behavior is possible and mousedown events can be handled by a
1736 * parent widget.
1737 *
1738 * @static
1739 * @inheritable
1740 * @property {boolean}
1741 */
1742 OO.ui.mixin.ButtonElement.static.cancelButtonMouseDownEvents = true;
1743
1744 /* Events */
1745
1746 /**
1747 * A 'click' event is emitted when the button element is clicked.
1748 *
1749 * @event click
1750 */
1751
1752 /* Methods */
1753
1754 /**
1755 * Set the button element.
1756 *
1757 * This method is used to retarget a button mixin so that its functionality applies to
1758 * the specified button element instead of the one created by the class. If a button element
1759 * is already set, the method will remove the mixin’s effect on that element.
1760 *
1761 * @param {jQuery} $button Element to use as button
1762 */
1763 OO.ui.mixin.ButtonElement.prototype.setButtonElement = function ( $button ) {
1764 if ( this.$button ) {
1765 this.$button
1766 .removeClass( 'oo-ui-buttonElement-button' )
1767 .removeAttr( 'role accesskey' )
1768 .off( {
1769 mousedown: this.onMouseDownHandler,
1770 keydown: this.onKeyDownHandler,
1771 click: this.onClickHandler,
1772 keypress: this.onKeyPressHandler
1773 } );
1774 }
1775
1776 this.$button = $button
1777 .addClass( 'oo-ui-buttonElement-button' )
1778 .attr( { role: 'button' } )
1779 .on( {
1780 mousedown: this.onMouseDownHandler,
1781 keydown: this.onKeyDownHandler,
1782 click: this.onClickHandler,
1783 keypress: this.onKeyPressHandler
1784 } );
1785 };
1786
1787 /**
1788 * Handles mouse down events.
1789 *
1790 * @protected
1791 * @param {jQuery.Event} e Mouse down event
1792 */
1793 OO.ui.mixin.ButtonElement.prototype.onMouseDown = function ( e ) {
1794 if ( this.isDisabled() || e.which !== OO.ui.MouseButtons.LEFT ) {
1795 return;
1796 }
1797 this.$element.addClass( 'oo-ui-buttonElement-pressed' );
1798 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
1799 // reliably remove the pressed class
1800 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
1801 // Prevent change of focus unless specifically configured otherwise
1802 if ( this.constructor.static.cancelButtonMouseDownEvents ) {
1803 return false;
1804 }
1805 };
1806
1807 /**
1808 * Handles mouse up events.
1809 *
1810 * @protected
1811 * @param {jQuery.Event} e Mouse up event
1812 */
1813 OO.ui.mixin.ButtonElement.prototype.onMouseUp = function ( e ) {
1814 if ( this.isDisabled() || e.which !== OO.ui.MouseButtons.LEFT ) {
1815 return;
1816 }
1817 this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
1818 // Stop listening for mouseup, since we only needed this once
1819 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
1820 };
1821
1822 /**
1823 * Handles mouse click events.
1824 *
1825 * @protected
1826 * @param {jQuery.Event} e Mouse click event
1827 * @fires click
1828 */
1829 OO.ui.mixin.ButtonElement.prototype.onClick = function ( e ) {
1830 if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
1831 if ( this.emit( 'click' ) ) {
1832 return false;
1833 }
1834 }
1835 };
1836
1837 /**
1838 * Handles key down events.
1839 *
1840 * @protected
1841 * @param {jQuery.Event} e Key down event
1842 */
1843 OO.ui.mixin.ButtonElement.prototype.onKeyDown = function ( e ) {
1844 if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
1845 return;
1846 }
1847 this.$element.addClass( 'oo-ui-buttonElement-pressed' );
1848 // Run the keyup handler no matter where the key is when the button is let go, so we can
1849 // reliably remove the pressed class
1850 this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler, true );
1851 };
1852
1853 /**
1854 * Handles key up events.
1855 *
1856 * @protected
1857 * @param {jQuery.Event} e Key up event
1858 */
1859 OO.ui.mixin.ButtonElement.prototype.onKeyUp = function ( e ) {
1860 if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
1861 return;
1862 }
1863 this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
1864 // Stop listening for keyup, since we only needed this once
1865 this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler, true );
1866 };
1867
1868 /**
1869 * Handles key press events.
1870 *
1871 * @protected
1872 * @param {jQuery.Event} e Key press event
1873 * @fires click
1874 */
1875 OO.ui.mixin.ButtonElement.prototype.onKeyPress = function ( e ) {
1876 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
1877 if ( this.emit( 'click' ) ) {
1878 return false;
1879 }
1880 }
1881 };
1882
1883 /**
1884 * Check if button has a frame.
1885 *
1886 * @return {boolean} Button is framed
1887 */
1888 OO.ui.mixin.ButtonElement.prototype.isFramed = function () {
1889 return this.framed;
1890 };
1891
1892 /**
1893 * Render the button with or without a frame. Omit the `framed` parameter to toggle the button frame on and off.
1894 *
1895 * @param {boolean} [framed] Make button framed, omit to toggle
1896 * @chainable
1897 */
1898 OO.ui.mixin.ButtonElement.prototype.toggleFramed = function ( framed ) {
1899 framed = framed === undefined ? !this.framed : !!framed;
1900 if ( framed !== this.framed ) {
1901 this.framed = framed;
1902 this.$element
1903 .toggleClass( 'oo-ui-buttonElement-frameless', !framed )
1904 .toggleClass( 'oo-ui-buttonElement-framed', framed );
1905 this.updateThemeClasses();
1906 }
1907
1908 return this;
1909 };
1910
1911 /**
1912 * Set the button's active state.
1913 *
1914 * The active state occurs when a {@link OO.ui.ButtonOptionWidget ButtonOptionWidget} or
1915 * a {@link OO.ui.ToggleButtonWidget ToggleButtonWidget} is pressed. This method does nothing
1916 * for other button types.
1917 *
1918 * @param {boolean} value Make button active
1919 * @chainable
1920 */
1921 OO.ui.mixin.ButtonElement.prototype.setActive = function ( value ) {
1922 this.active = !!value;
1923 this.$element.toggleClass( 'oo-ui-buttonElement-active', this.active );
1924 return this;
1925 };
1926
1927 /**
1928 * Check if the button is active
1929 *
1930 * @return {boolean} The button is active
1931 */
1932 OO.ui.mixin.ButtonElement.prototype.isActive = function () {
1933 return this.active;
1934 };
1935
1936 /**
1937 * Any OOjs UI widget that contains other widgets (such as {@link OO.ui.ButtonWidget buttons} or
1938 * {@link OO.ui.OptionWidget options}) mixes in GroupElement. Adding, removing, and clearing
1939 * items from the group is done through the interface the class provides.
1940 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
1941 *
1942 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Groups
1943 *
1944 * @abstract
1945 * @class
1946 *
1947 * @constructor
1948 * @param {Object} [config] Configuration options
1949 * @cfg {jQuery} [$group] The container element created by the class. If this configuration
1950 * is omitted, the group element will use a generated `<div>`.
1951 */
1952 OO.ui.mixin.GroupElement = function OoUiMixinGroupElement( config ) {
1953 // Configuration initialization
1954 config = config || {};
1955
1956 // Properties
1957 this.$group = null;
1958 this.items = [];
1959 this.aggregateItemEvents = {};
1960
1961 // Initialization
1962 this.setGroupElement( config.$group || $( '<div>' ) );
1963 };
1964
1965 /* Methods */
1966
1967 /**
1968 * Set the group element.
1969 *
1970 * If an element is already set, items will be moved to the new element.
1971 *
1972 * @param {jQuery} $group Element to use as group
1973 */
1974 OO.ui.mixin.GroupElement.prototype.setGroupElement = function ( $group ) {
1975 var i, len;
1976
1977 this.$group = $group;
1978 for ( i = 0, len = this.items.length; i < len; i++ ) {
1979 this.$group.append( this.items[ i ].$element );
1980 }
1981 };
1982
1983 /**
1984 * Check if a group contains no items.
1985 *
1986 * @return {boolean} Group is empty
1987 */
1988 OO.ui.mixin.GroupElement.prototype.isEmpty = function () {
1989 return !this.items.length;
1990 };
1991
1992 /**
1993 * Get all items in the group.
1994 *
1995 * The method returns an array of item references (e.g., [button1, button2, button3]) and is useful
1996 * when synchronizing groups of items, or whenever the references are required (e.g., when removing items
1997 * from a group).
1998 *
1999 * @return {OO.ui.Element[]} An array of items.
2000 */
2001 OO.ui.mixin.GroupElement.prototype.getItems = function () {
2002 return this.items.slice( 0 );
2003 };
2004
2005 /**
2006 * Get an item by its data.
2007 *
2008 * Only the first item with matching data will be returned. To return all matching items,
2009 * use the #getItemsFromData method.
2010 *
2011 * @param {Object} data Item data to search for
2012 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
2013 */
2014 OO.ui.mixin.GroupElement.prototype.getItemFromData = function ( data ) {
2015 var i, len, item,
2016 hash = OO.getHash( data );
2017
2018 for ( i = 0, len = this.items.length; i < len; i++ ) {
2019 item = this.items[ i ];
2020 if ( hash === OO.getHash( item.getData() ) ) {
2021 return item;
2022 }
2023 }
2024
2025 return null;
2026 };
2027
2028 /**
2029 * Get items by their data.
2030 *
2031 * All items with matching data will be returned. To return only the first match, use the #getItemFromData method instead.
2032 *
2033 * @param {Object} data Item data to search for
2034 * @return {OO.ui.Element[]} Items with equivalent data
2035 */
2036 OO.ui.mixin.GroupElement.prototype.getItemsFromData = function ( data ) {
2037 var i, len, item,
2038 hash = OO.getHash( data ),
2039 items = [];
2040
2041 for ( i = 0, len = this.items.length; i < len; i++ ) {
2042 item = this.items[ i ];
2043 if ( hash === OO.getHash( item.getData() ) ) {
2044 items.push( item );
2045 }
2046 }
2047
2048 return items;
2049 };
2050
2051 /**
2052 * Aggregate the events emitted by the group.
2053 *
2054 * When events are aggregated, the group will listen to all contained items for the event,
2055 * and then emit the event under a new name. The new event will contain an additional leading
2056 * parameter containing the item that emitted the original event. Other arguments emitted from
2057 * the original event are passed through.
2058 *
2059 * @param {Object.<string,string|null>} events An object keyed by the name of the event that should be
2060 * aggregated (e.g., ‘click’) and the value of the new name to use (e.g., ‘groupClick’).
2061 * A `null` value will remove aggregated events.
2062
2063 * @throws {Error} An error is thrown if aggregation already exists.
2064 */
2065 OO.ui.mixin.GroupElement.prototype.aggregate = function ( events ) {
2066 var i, len, item, add, remove, itemEvent, groupEvent;
2067
2068 for ( itemEvent in events ) {
2069 groupEvent = events[ itemEvent ];
2070
2071 // Remove existing aggregated event
2072 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
2073 // Don't allow duplicate aggregations
2074 if ( groupEvent ) {
2075 throw new Error( 'Duplicate item event aggregation for ' + itemEvent );
2076 }
2077 // Remove event aggregation from existing items
2078 for ( i = 0, len = this.items.length; i < len; i++ ) {
2079 item = this.items[ i ];
2080 if ( item.connect && item.disconnect ) {
2081 remove = {};
2082 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
2083 item.disconnect( this, remove );
2084 }
2085 }
2086 // Prevent future items from aggregating event
2087 delete this.aggregateItemEvents[ itemEvent ];
2088 }
2089
2090 // Add new aggregate event
2091 if ( groupEvent ) {
2092 // Make future items aggregate event
2093 this.aggregateItemEvents[ itemEvent ] = groupEvent;
2094 // Add event aggregation to existing items
2095 for ( i = 0, len = this.items.length; i < len; i++ ) {
2096 item = this.items[ i ];
2097 if ( item.connect && item.disconnect ) {
2098 add = {};
2099 add[ itemEvent ] = [ 'emit', groupEvent, item ];
2100 item.connect( this, add );
2101 }
2102 }
2103 }
2104 }
2105 };
2106
2107 /**
2108 * Add items to the group.
2109 *
2110 * Items will be added to the end of the group array unless the optional `index` parameter specifies
2111 * a different insertion point. Adding an existing item will move it to the end of the array or the point specified by the `index`.
2112 *
2113 * @param {OO.ui.Element[]} items An array of items to add to the group
2114 * @param {number} [index] Index of the insertion point
2115 * @chainable
2116 */
2117 OO.ui.mixin.GroupElement.prototype.addItems = function ( items, index ) {
2118 var i, len, item, event, events, currentIndex,
2119 itemElements = [];
2120
2121 for ( i = 0, len = items.length; i < len; i++ ) {
2122 item = items[ i ];
2123
2124 // Check if item exists then remove it first, effectively "moving" it
2125 currentIndex = this.items.indexOf( item );
2126 if ( currentIndex >= 0 ) {
2127 this.removeItems( [ item ] );
2128 // Adjust index to compensate for removal
2129 if ( currentIndex < index ) {
2130 index--;
2131 }
2132 }
2133 // Add the item
2134 if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
2135 events = {};
2136 for ( event in this.aggregateItemEvents ) {
2137 events[ event ] = [ 'emit', this.aggregateItemEvents[ event ], item ];
2138 }
2139 item.connect( this, events );
2140 }
2141 item.setElementGroup( this );
2142 itemElements.push( item.$element.get( 0 ) );
2143 }
2144
2145 if ( index === undefined || index < 0 || index >= this.items.length ) {
2146 this.$group.append( itemElements );
2147 this.items.push.apply( this.items, items );
2148 } else if ( index === 0 ) {
2149 this.$group.prepend( itemElements );
2150 this.items.unshift.apply( this.items, items );
2151 } else {
2152 this.items[ index ].$element.before( itemElements );
2153 this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
2154 }
2155
2156 return this;
2157 };
2158
2159 /**
2160 * Remove the specified items from a group.
2161 *
2162 * Removed items are detached (not removed) from the DOM so that they may be reused.
2163 * To remove all items from a group, you may wish to use the #clearItems method instead.
2164 *
2165 * @param {OO.ui.Element[]} items An array of items to remove
2166 * @chainable
2167 */
2168 OO.ui.mixin.GroupElement.prototype.removeItems = function ( items ) {
2169 var i, len, item, index, remove, itemEvent;
2170
2171 // Remove specific items
2172 for ( i = 0, len = items.length; i < len; i++ ) {
2173 item = items[ i ];
2174 index = this.items.indexOf( item );
2175 if ( index !== -1 ) {
2176 if (
2177 item.connect && item.disconnect &&
2178 !$.isEmptyObject( this.aggregateItemEvents )
2179 ) {
2180 remove = {};
2181 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
2182 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
2183 }
2184 item.disconnect( this, remove );
2185 }
2186 item.setElementGroup( null );
2187 this.items.splice( index, 1 );
2188 item.$element.detach();
2189 }
2190 }
2191
2192 return this;
2193 };
2194
2195 /**
2196 * Clear all items from the group.
2197 *
2198 * Cleared items are detached from the DOM, not removed, so that they may be reused.
2199 * To remove only a subset of items from a group, use the #removeItems method.
2200 *
2201 * @chainable
2202 */
2203 OO.ui.mixin.GroupElement.prototype.clearItems = function () {
2204 var i, len, item, remove, itemEvent;
2205
2206 // Remove all items
2207 for ( i = 0, len = this.items.length; i < len; i++ ) {
2208 item = this.items[ i ];
2209 if (
2210 item.connect && item.disconnect &&
2211 !$.isEmptyObject( this.aggregateItemEvents )
2212 ) {
2213 remove = {};
2214 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
2215 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
2216 }
2217 item.disconnect( this, remove );
2218 }
2219 item.setElementGroup( null );
2220 item.$element.detach();
2221 }
2222
2223 this.items = [];
2224 return this;
2225 };
2226
2227 /**
2228 * IconElement is often mixed into other classes to generate an icon.
2229 * Icons are graphics, about the size of normal text. They are used to aid the user
2230 * in locating a control or to convey information in a space-efficient way. See the
2231 * [OOjs UI documentation on MediaWiki] [1] for a list of icons
2232 * included in the library.
2233 *
2234 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
2235 *
2236 * @abstract
2237 * @class
2238 *
2239 * @constructor
2240 * @param {Object} [config] Configuration options
2241 * @cfg {jQuery} [$icon] The icon element created by the class. If this configuration is omitted,
2242 * the icon element will use a generated `<span>`. To use a different HTML tag, or to specify that
2243 * the icon element be set to an existing icon instead of the one generated by this class, set a
2244 * value using a jQuery selection. For example:
2245 *
2246 * // Use a <div> tag instead of a <span>
2247 * $icon: $("<div>")
2248 * // Use an existing icon element instead of the one generated by the class
2249 * $icon: this.$element
2250 * // Use an icon element from a child widget
2251 * $icon: this.childwidget.$element
2252 * @cfg {Object|string} [icon=''] The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of
2253 * symbolic names. A map is used for i18n purposes and contains a `default` icon
2254 * name and additional names keyed by language code. The `default` name is used when no icon is keyed
2255 * by the user's language.
2256 *
2257 * Example of an i18n map:
2258 *
2259 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2260 * See the [OOjs UI documentation on MediaWiki] [2] for a list of icons included in the library.
2261 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
2262 * @cfg {string|Function} [iconTitle] A text string used as the icon title, or a function that returns title
2263 * text. The icon title is displayed when users move the mouse over the icon.
2264 */
2265 OO.ui.mixin.IconElement = function OoUiMixinIconElement( config ) {
2266 // Configuration initialization
2267 config = config || {};
2268
2269 // Properties
2270 this.$icon = null;
2271 this.icon = null;
2272 this.iconTitle = null;
2273
2274 // Initialization
2275 this.setIcon( config.icon || this.constructor.static.icon );
2276 this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle );
2277 this.setIconElement( config.$icon || $( '<span>' ) );
2278 };
2279
2280 /* Setup */
2281
2282 OO.initClass( OO.ui.mixin.IconElement );
2283
2284 /* Static Properties */
2285
2286 /**
2287 * The symbolic name of the icon (e.g., ‘remove’ or ‘menu’), or a map of symbolic names. A map is used
2288 * for i18n purposes and contains a `default` icon name and additional names keyed by
2289 * language code. The `default` name is used when no icon is keyed by the user's language.
2290 *
2291 * Example of an i18n map:
2292 *
2293 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
2294 *
2295 * Note: the static property will be overridden if the #icon configuration is used.
2296 *
2297 * @static
2298 * @inheritable
2299 * @property {Object|string}
2300 */
2301 OO.ui.mixin.IconElement.static.icon = null;
2302
2303 /**
2304 * The icon title, displayed when users move the mouse over the icon. The value can be text, a
2305 * function that returns title text, or `null` for no title.
2306 *
2307 * The static property will be overridden if the #iconTitle configuration is used.
2308 *
2309 * @static
2310 * @inheritable
2311 * @property {string|Function|null}
2312 */
2313 OO.ui.mixin.IconElement.static.iconTitle = null;
2314
2315 /* Methods */
2316
2317 /**
2318 * Set the icon element. This method is used to retarget an icon mixin so that its functionality
2319 * applies to the specified icon element instead of the one created by the class. If an icon
2320 * element is already set, the mixin’s effect on that element is removed. Generated CSS classes
2321 * and mixin methods will no longer affect the element.
2322 *
2323 * @param {jQuery} $icon Element to use as icon
2324 */
2325 OO.ui.mixin.IconElement.prototype.setIconElement = function ( $icon ) {
2326 if ( this.$icon ) {
2327 this.$icon
2328 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon )
2329 .removeAttr( 'title' );
2330 }
2331
2332 this.$icon = $icon
2333 .addClass( 'oo-ui-iconElement-icon' )
2334 .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon );
2335 if ( this.iconTitle !== null ) {
2336 this.$icon.attr( 'title', this.iconTitle );
2337 }
2338
2339 this.updateThemeClasses();
2340 };
2341
2342 /**
2343 * Set icon by symbolic name (e.g., ‘remove’ or ‘menu’). Use `null` to remove an icon.
2344 * The icon parameter can also be set to a map of icon names. See the #icon config setting
2345 * for an example.
2346 *
2347 * @param {Object|string|null} icon A symbolic icon name, a {@link #icon map of icon names} keyed
2348 * by language code, or `null` to remove the icon.
2349 * @chainable
2350 */
2351 OO.ui.mixin.IconElement.prototype.setIcon = function ( icon ) {
2352 icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
2353 icon = typeof icon === 'string' && icon.trim().length ? icon.trim() : null;
2354
2355 if ( this.icon !== icon ) {
2356 if ( this.$icon ) {
2357 if ( this.icon !== null ) {
2358 this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
2359 }
2360 if ( icon !== null ) {
2361 this.$icon.addClass( 'oo-ui-icon-' + icon );
2362 }
2363 }
2364 this.icon = icon;
2365 }
2366
2367 this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon );
2368 this.updateThemeClasses();
2369
2370 return this;
2371 };
2372
2373 /**
2374 * Set the icon title. Use `null` to remove the title.
2375 *
2376 * @param {string|Function|null} iconTitle A text string used as the icon title,
2377 * a function that returns title text, or `null` for no title.
2378 * @chainable
2379 */
2380 OO.ui.mixin.IconElement.prototype.setIconTitle = function ( iconTitle ) {
2381 iconTitle = typeof iconTitle === 'function' ||
2382 ( typeof iconTitle === 'string' && iconTitle.length ) ?
2383 OO.ui.resolveMsg( iconTitle ) : null;
2384
2385 if ( this.iconTitle !== iconTitle ) {
2386 this.iconTitle = iconTitle;
2387 if ( this.$icon ) {
2388 if ( this.iconTitle !== null ) {
2389 this.$icon.attr( 'title', iconTitle );
2390 } else {
2391 this.$icon.removeAttr( 'title' );
2392 }
2393 }
2394 }
2395
2396 return this;
2397 };
2398
2399 /**
2400 * Get the symbolic name of the icon.
2401 *
2402 * @return {string} Icon name
2403 */
2404 OO.ui.mixin.IconElement.prototype.getIcon = function () {
2405 return this.icon;
2406 };
2407
2408 /**
2409 * Get the icon title. The title text is displayed when a user moves the mouse over the icon.
2410 *
2411 * @return {string} Icon title text
2412 */
2413 OO.ui.mixin.IconElement.prototype.getIconTitle = function () {
2414 return this.iconTitle;
2415 };
2416
2417 /**
2418 * IndicatorElement is often mixed into other classes to generate an indicator.
2419 * Indicators are small graphics that are generally used in two ways:
2420 *
2421 * - To draw attention to the status of an item. For example, an indicator might be
2422 * used to show that an item in a list has errors that need to be resolved.
2423 * - To clarify the function of a control that acts in an exceptional way (a button
2424 * that opens a menu instead of performing an action directly, for example).
2425 *
2426 * For a list of indicators included in the library, please see the
2427 * [OOjs UI documentation on MediaWiki] [1].
2428 *
2429 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2430 *
2431 * @abstract
2432 * @class
2433 *
2434 * @constructor
2435 * @param {Object} [config] Configuration options
2436 * @cfg {jQuery} [$indicator] The indicator element created by the class. If this
2437 * configuration is omitted, the indicator element will use a generated `<span>`.
2438 * @cfg {string} [indicator] Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
2439 * See the [OOjs UI documentation on MediaWiki][2] for a list of indicators included
2440 * in the library.
2441 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
2442 * @cfg {string|Function} [indicatorTitle] A text string used as the indicator title,
2443 * or a function that returns title text. The indicator title is displayed when users move
2444 * the mouse over the indicator.
2445 */
2446 OO.ui.mixin.IndicatorElement = function OoUiMixinIndicatorElement( config ) {
2447 // Configuration initialization
2448 config = config || {};
2449
2450 // Properties
2451 this.$indicator = null;
2452 this.indicator = null;
2453 this.indicatorTitle = null;
2454
2455 // Initialization
2456 this.setIndicator( config.indicator || this.constructor.static.indicator );
2457 this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle );
2458 this.setIndicatorElement( config.$indicator || $( '<span>' ) );
2459 };
2460
2461 /* Setup */
2462
2463 OO.initClass( OO.ui.mixin.IndicatorElement );
2464
2465 /* Static Properties */
2466
2467 /**
2468 * Symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
2469 * The static property will be overridden if the #indicator configuration is used.
2470 *
2471 * @static
2472 * @inheritable
2473 * @property {string|null}
2474 */
2475 OO.ui.mixin.IndicatorElement.static.indicator = null;
2476
2477 /**
2478 * A text string used as the indicator title, a function that returns title text, or `null`
2479 * for no title. The static property will be overridden if the #indicatorTitle configuration is used.
2480 *
2481 * @static
2482 * @inheritable
2483 * @property {string|Function|null}
2484 */
2485 OO.ui.mixin.IndicatorElement.static.indicatorTitle = null;
2486
2487 /* Methods */
2488
2489 /**
2490 * Set the indicator element.
2491 *
2492 * If an element is already set, it will be cleaned up before setting up the new element.
2493 *
2494 * @param {jQuery} $indicator Element to use as indicator
2495 */
2496 OO.ui.mixin.IndicatorElement.prototype.setIndicatorElement = function ( $indicator ) {
2497 if ( this.$indicator ) {
2498 this.$indicator
2499 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator )
2500 .removeAttr( 'title' );
2501 }
2502
2503 this.$indicator = $indicator
2504 .addClass( 'oo-ui-indicatorElement-indicator' )
2505 .toggleClass( 'oo-ui-indicator-' + this.indicator, !!this.indicator );
2506 if ( this.indicatorTitle !== null ) {
2507 this.$indicator.attr( 'title', this.indicatorTitle );
2508 }
2509
2510 this.updateThemeClasses();
2511 };
2512
2513 /**
2514 * Set the indicator by its symbolic name: ‘alert’, ‘down’, ‘next’, ‘previous’, ‘required’, ‘up’. Use `null` to remove the indicator.
2515 *
2516 * @param {string|null} indicator Symbolic name of indicator, or `null` for no indicator
2517 * @chainable
2518 */
2519 OO.ui.mixin.IndicatorElement.prototype.setIndicator = function ( indicator ) {
2520 indicator = typeof indicator === 'string' && indicator.length ? indicator.trim() : null;
2521
2522 if ( this.indicator !== indicator ) {
2523 if ( this.$indicator ) {
2524 if ( this.indicator !== null ) {
2525 this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
2526 }
2527 if ( indicator !== null ) {
2528 this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
2529 }
2530 }
2531 this.indicator = indicator;
2532 }
2533
2534 this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator );
2535 this.updateThemeClasses();
2536
2537 return this;
2538 };
2539
2540 /**
2541 * Set the indicator title.
2542 *
2543 * The title is displayed when a user moves the mouse over the indicator.
2544 *
2545 * @param {string|Function|null} indicator Indicator title text, a function that returns text, or
2546 * `null` for no indicator title
2547 * @chainable
2548 */
2549 OO.ui.mixin.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
2550 indicatorTitle = typeof indicatorTitle === 'function' ||
2551 ( typeof indicatorTitle === 'string' && indicatorTitle.length ) ?
2552 OO.ui.resolveMsg( indicatorTitle ) : null;
2553
2554 if ( this.indicatorTitle !== indicatorTitle ) {
2555 this.indicatorTitle = indicatorTitle;
2556 if ( this.$indicator ) {
2557 if ( this.indicatorTitle !== null ) {
2558 this.$indicator.attr( 'title', indicatorTitle );
2559 } else {
2560 this.$indicator.removeAttr( 'title' );
2561 }
2562 }
2563 }
2564
2565 return this;
2566 };
2567
2568 /**
2569 * Get the symbolic name of the indicator (e.g., ‘alert’ or ‘down’).
2570 *
2571 * @return {string} Symbolic name of indicator
2572 */
2573 OO.ui.mixin.IndicatorElement.prototype.getIndicator = function () {
2574 return this.indicator;
2575 };
2576
2577 /**
2578 * Get the indicator title.
2579 *
2580 * The title is displayed when a user moves the mouse over the indicator.
2581 *
2582 * @return {string} Indicator title text
2583 */
2584 OO.ui.mixin.IndicatorElement.prototype.getIndicatorTitle = function () {
2585 return this.indicatorTitle;
2586 };
2587
2588 /**
2589 * LabelElement is often mixed into other classes to generate a label, which
2590 * helps identify the function of an interface element.
2591 * See the [OOjs UI documentation on MediaWiki] [1] for more information.
2592 *
2593 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
2594 *
2595 * @abstract
2596 * @class
2597 *
2598 * @constructor
2599 * @param {Object} [config] Configuration options
2600 * @cfg {jQuery} [$label] The label element created by the class. If this
2601 * configuration is omitted, the label element will use a generated `<span>`.
2602 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] The label text. The label can be specified
2603 * as a plaintext string, a jQuery selection of elements, or a function that will produce a string
2604 * in the future. See the [OOjs UI documentation on MediaWiki] [2] for examples.
2605 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Labels
2606 * @cfg {boolean} [autoFitLabel=true] Fit the label to the width of the parent element.
2607 * The label will be truncated to fit if necessary.
2608 */
2609 OO.ui.mixin.LabelElement = function OoUiMixinLabelElement( config ) {
2610 // Configuration initialization
2611 config = config || {};
2612
2613 // Properties
2614 this.$label = null;
2615 this.label = null;
2616 this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel;
2617
2618 // Initialization
2619 this.setLabel( config.label || this.constructor.static.label );
2620 this.setLabelElement( config.$label || $( '<span>' ) );
2621 };
2622
2623 /* Setup */
2624
2625 OO.initClass( OO.ui.mixin.LabelElement );
2626
2627 /* Events */
2628
2629 /**
2630 * @event labelChange
2631 * @param {string} value
2632 */
2633
2634 /* Static Properties */
2635
2636 /**
2637 * The label text. The label can be specified as a plaintext string, a function that will
2638 * produce a string in the future, or `null` for no label. The static value will
2639 * be overridden if a label is specified with the #label config option.
2640 *
2641 * @static
2642 * @inheritable
2643 * @property {string|Function|null}
2644 */
2645 OO.ui.mixin.LabelElement.static.label = null;
2646
2647 /* Methods */
2648
2649 /**
2650 * Set the label element.
2651 *
2652 * If an element is already set, it will be cleaned up before setting up the new element.
2653 *
2654 * @param {jQuery} $label Element to use as label
2655 */
2656 OO.ui.mixin.LabelElement.prototype.setLabelElement = function ( $label ) {
2657 if ( this.$label ) {
2658 this.$label.removeClass( 'oo-ui-labelElement-label' ).empty();
2659 }
2660
2661 this.$label = $label.addClass( 'oo-ui-labelElement-label' );
2662 this.setLabelContent( this.label );
2663 };
2664
2665 /**
2666 * Set the label.
2667 *
2668 * An empty string will result in the label being hidden. A string containing only whitespace will
2669 * be converted to a single `&nbsp;`.
2670 *
2671 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} label Label nodes; text; a function that returns nodes or
2672 * text; or null for no label
2673 * @chainable
2674 */
2675 OO.ui.mixin.LabelElement.prototype.setLabel = function ( label ) {
2676 label = typeof label === 'function' ? OO.ui.resolveMsg( label ) : label;
2677 label = ( ( typeof label === 'string' && label.length ) || label instanceof jQuery || label instanceof OO.ui.HtmlSnippet ) ? label : null;
2678
2679 this.$element.toggleClass( 'oo-ui-labelElement', !!label );
2680
2681 if ( this.label !== label ) {
2682 if ( this.$label ) {
2683 this.setLabelContent( label );
2684 }
2685 this.label = label;
2686 this.emit( 'labelChange' );
2687 }
2688
2689 return this;
2690 };
2691
2692 /**
2693 * Get the label.
2694 *
2695 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
2696 * text; or null for no label
2697 */
2698 OO.ui.mixin.LabelElement.prototype.getLabel = function () {
2699 return this.label;
2700 };
2701
2702 /**
2703 * Fit the label.
2704 *
2705 * @chainable
2706 */
2707 OO.ui.mixin.LabelElement.prototype.fitLabel = function () {
2708 if ( this.$label && this.$label.autoEllipsis && this.autoFitLabel ) {
2709 this.$label.autoEllipsis( { hasSpan: false, tooltip: true } );
2710 }
2711
2712 return this;
2713 };
2714
2715 /**
2716 * Set the content of the label.
2717 *
2718 * Do not call this method until after the label element has been set by #setLabelElement.
2719 *
2720 * @private
2721 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
2722 * text; or null for no label
2723 */
2724 OO.ui.mixin.LabelElement.prototype.setLabelContent = function ( label ) {
2725 if ( typeof label === 'string' ) {
2726 if ( label.match( /^\s*$/ ) ) {
2727 // Convert whitespace only string to a single non-breaking space
2728 this.$label.html( '&nbsp;' );
2729 } else {
2730 this.$label.text( label );
2731 }
2732 } else if ( label instanceof OO.ui.HtmlSnippet ) {
2733 this.$label.html( label.toString() );
2734 } else if ( label instanceof jQuery ) {
2735 this.$label.empty().append( label );
2736 } else {
2737 this.$label.empty();
2738 }
2739 };
2740
2741 /**
2742 * The FlaggedElement class is an attribute mixin, meaning that it is used to add
2743 * additional functionality to an element created by another class. The class provides
2744 * a ‘flags’ property assigned the name (or an array of names) of styling flags,
2745 * which are used to customize the look and feel of a widget to better describe its
2746 * importance and functionality.
2747 *
2748 * The library currently contains the following styling flags for general use:
2749 *
2750 * - **progressive**: Progressive styling is applied to convey that the widget will move the user forward in a process.
2751 * - **destructive**: Destructive styling is applied to convey that the widget will remove something.
2752 * - **constructive**: Constructive styling is applied to convey that the widget will create something.
2753 *
2754 * The flags affect the appearance of the buttons:
2755 *
2756 * @example
2757 * // FlaggedElement is mixed into ButtonWidget to provide styling flags
2758 * var button1 = new OO.ui.ButtonWidget( {
2759 * label: 'Constructive',
2760 * flags: 'constructive'
2761 * } );
2762 * var button2 = new OO.ui.ButtonWidget( {
2763 * label: 'Destructive',
2764 * flags: 'destructive'
2765 * } );
2766 * var button3 = new OO.ui.ButtonWidget( {
2767 * label: 'Progressive',
2768 * flags: 'progressive'
2769 * } );
2770 * $( 'body' ).append( button1.$element, button2.$element, button3.$element );
2771 *
2772 * {@link OO.ui.ActionWidget ActionWidgets}, which are a special kind of button that execute an action, use these flags: **primary** and **safe**.
2773 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
2774 *
2775 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
2776 *
2777 * @abstract
2778 * @class
2779 *
2780 * @constructor
2781 * @param {Object} [config] Configuration options
2782 * @cfg {string|string[]} [flags] The name or names of the flags (e.g., 'constructive' or 'primary') to apply.
2783 * Please see the [OOjs UI documentation on MediaWiki] [2] for more information about available flags.
2784 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Elements/Flagged
2785 * @cfg {jQuery} [$flagged] The flagged element. By default,
2786 * the flagged functionality is applied to the element created by the class ($element).
2787 * If a different element is specified, the flagged functionality will be applied to it instead.
2788 */
2789 OO.ui.mixin.FlaggedElement = function OoUiMixinFlaggedElement( config ) {
2790 // Configuration initialization
2791 config = config || {};
2792
2793 // Properties
2794 this.flags = {};
2795 this.$flagged = null;
2796
2797 // Initialization
2798 this.setFlags( config.flags );
2799 this.setFlaggedElement( config.$flagged || this.$element );
2800 };
2801
2802 /* Events */
2803
2804 /**
2805 * @event flag
2806 * A flag event is emitted when the #clearFlags or #setFlags methods are used. The `changes`
2807 * parameter contains the name of each modified flag and indicates whether it was
2808 * added or removed.
2809 *
2810 * @param {Object.<string,boolean>} changes Object keyed by flag name. A Boolean `true` indicates
2811 * that the flag was added, `false` that the flag was removed.
2812 */
2813
2814 /* Methods */
2815
2816 /**
2817 * Set the flagged element.
2818 *
2819 * This method is used to retarget a flagged mixin so that its functionality applies to the specified element.
2820 * If an element is already set, the method will remove the mixin’s effect on that element.
2821 *
2822 * @param {jQuery} $flagged Element that should be flagged
2823 */
2824 OO.ui.mixin.FlaggedElement.prototype.setFlaggedElement = function ( $flagged ) {
2825 var classNames = Object.keys( this.flags ).map( function ( flag ) {
2826 return 'oo-ui-flaggedElement-' + flag;
2827 } ).join( ' ' );
2828
2829 if ( this.$flagged ) {
2830 this.$flagged.removeClass( classNames );
2831 }
2832
2833 this.$flagged = $flagged.addClass( classNames );
2834 };
2835
2836 /**
2837 * Check if the specified flag is set.
2838 *
2839 * @param {string} flag Name of flag
2840 * @return {boolean} The flag is set
2841 */
2842 OO.ui.mixin.FlaggedElement.prototype.hasFlag = function ( flag ) {
2843 // This may be called before the constructor, thus before this.flags is set
2844 return this.flags && ( flag in this.flags );
2845 };
2846
2847 /**
2848 * Get the names of all flags set.
2849 *
2850 * @return {string[]} Flag names
2851 */
2852 OO.ui.mixin.FlaggedElement.prototype.getFlags = function () {
2853 // This may be called before the constructor, thus before this.flags is set
2854 return Object.keys( this.flags || {} );
2855 };
2856
2857 /**
2858 * Clear all flags.
2859 *
2860 * @chainable
2861 * @fires flag
2862 */
2863 OO.ui.mixin.FlaggedElement.prototype.clearFlags = function () {
2864 var flag, className,
2865 changes = {},
2866 remove = [],
2867 classPrefix = 'oo-ui-flaggedElement-';
2868
2869 for ( flag in this.flags ) {
2870 className = classPrefix + flag;
2871 changes[ flag ] = false;
2872 delete this.flags[ flag ];
2873 remove.push( className );
2874 }
2875
2876 if ( this.$flagged ) {
2877 this.$flagged.removeClass( remove.join( ' ' ) );
2878 }
2879
2880 this.updateThemeClasses();
2881 this.emit( 'flag', changes );
2882
2883 return this;
2884 };
2885
2886 /**
2887 * Add one or more flags.
2888 *
2889 * @param {string|string[]|Object.<string, boolean>} flags A flag name, an array of flag names,
2890 * or an object keyed by flag name with a boolean value that indicates whether the flag should
2891 * be added (`true`) or removed (`false`).
2892 * @chainable
2893 * @fires flag
2894 */
2895 OO.ui.mixin.FlaggedElement.prototype.setFlags = function ( flags ) {
2896 var i, len, flag, className,
2897 changes = {},
2898 add = [],
2899 remove = [],
2900 classPrefix = 'oo-ui-flaggedElement-';
2901
2902 if ( typeof flags === 'string' ) {
2903 className = classPrefix + flags;
2904 // Set
2905 if ( !this.flags[ flags ] ) {
2906 this.flags[ flags ] = true;
2907 add.push( className );
2908 }
2909 } else if ( Array.isArray( flags ) ) {
2910 for ( i = 0, len = flags.length; i < len; i++ ) {
2911 flag = flags[ i ];
2912 className = classPrefix + flag;
2913 // Set
2914 if ( !this.flags[ flag ] ) {
2915 changes[ flag ] = true;
2916 this.flags[ flag ] = true;
2917 add.push( className );
2918 }
2919 }
2920 } else if ( OO.isPlainObject( flags ) ) {
2921 for ( flag in flags ) {
2922 className = classPrefix + flag;
2923 if ( flags[ flag ] ) {
2924 // Set
2925 if ( !this.flags[ flag ] ) {
2926 changes[ flag ] = true;
2927 this.flags[ flag ] = true;
2928 add.push( className );
2929 }
2930 } else {
2931 // Remove
2932 if ( this.flags[ flag ] ) {
2933 changes[ flag ] = false;
2934 delete this.flags[ flag ];
2935 remove.push( className );
2936 }
2937 }
2938 }
2939 }
2940
2941 if ( this.$flagged ) {
2942 this.$flagged
2943 .addClass( add.join( ' ' ) )
2944 .removeClass( remove.join( ' ' ) );
2945 }
2946
2947 this.updateThemeClasses();
2948 this.emit( 'flag', changes );
2949
2950 return this;
2951 };
2952
2953 /**
2954 * TitledElement is mixed into other classes to provide a `title` attribute.
2955 * Titles are rendered by the browser and are made visible when the user moves
2956 * the mouse over the element. Titles are not visible on touch devices.
2957 *
2958 * @example
2959 * // TitledElement provides a 'title' attribute to the
2960 * // ButtonWidget class
2961 * var button = new OO.ui.ButtonWidget( {
2962 * label: 'Button with Title',
2963 * title: 'I am a button'
2964 * } );
2965 * $( 'body' ).append( button.$element );
2966 *
2967 * @abstract
2968 * @class
2969 *
2970 * @constructor
2971 * @param {Object} [config] Configuration options
2972 * @cfg {jQuery} [$titled] The element to which the `title` attribute is applied.
2973 * If this config is omitted, the title functionality is applied to $element, the
2974 * element created by the class.
2975 * @cfg {string|Function} [title] The title text or a function that returns text. If
2976 * this config is omitted, the value of the {@link #static-title static title} property is used.
2977 */
2978 OO.ui.mixin.TitledElement = function OoUiMixinTitledElement( config ) {
2979 // Configuration initialization
2980 config = config || {};
2981
2982 // Properties
2983 this.$titled = null;
2984 this.title = null;
2985
2986 // Initialization
2987 this.setTitle( config.title || this.constructor.static.title );
2988 this.setTitledElement( config.$titled || this.$element );
2989 };
2990
2991 /* Setup */
2992
2993 OO.initClass( OO.ui.mixin.TitledElement );
2994
2995 /* Static Properties */
2996
2997 /**
2998 * The title text, a function that returns text, or `null` for no title. The value of the static property
2999 * is overridden if the #title config option is used.
3000 *
3001 * @static
3002 * @inheritable
3003 * @property {string|Function|null}
3004 */
3005 OO.ui.mixin.TitledElement.static.title = null;
3006
3007 /* Methods */
3008
3009 /**
3010 * Set the titled element.
3011 *
3012 * This method is used to retarget a titledElement mixin so that its functionality applies to the specified element.
3013 * If an element is already set, the mixin’s effect on that element is removed before the new element is set up.
3014 *
3015 * @param {jQuery} $titled Element that should use the 'titled' functionality
3016 */
3017 OO.ui.mixin.TitledElement.prototype.setTitledElement = function ( $titled ) {
3018 if ( this.$titled ) {
3019 this.$titled.removeAttr( 'title' );
3020 }
3021
3022 this.$titled = $titled;
3023 if ( this.title ) {
3024 this.$titled.attr( 'title', this.title );
3025 }
3026 };
3027
3028 /**
3029 * Set title.
3030 *
3031 * @param {string|Function|null} title Title text, a function that returns text, or `null` for no title
3032 * @chainable
3033 */
3034 OO.ui.mixin.TitledElement.prototype.setTitle = function ( title ) {
3035 title = typeof title === 'function' ? OO.ui.resolveMsg( title ) : title;
3036 title = ( typeof title === 'string' && title.length ) ? title : null;
3037
3038 if ( this.title !== title ) {
3039 if ( this.$titled ) {
3040 if ( title !== null ) {
3041 this.$titled.attr( 'title', title );
3042 } else {
3043 this.$titled.removeAttr( 'title' );
3044 }
3045 }
3046 this.title = title;
3047 }
3048
3049 return this;
3050 };
3051
3052 /**
3053 * Get title.
3054 *
3055 * @return {string} Title string
3056 */
3057 OO.ui.mixin.TitledElement.prototype.getTitle = function () {
3058 return this.title;
3059 };
3060
3061 /**
3062 * AccessKeyedElement is mixed into other classes to provide an `accesskey` attribute.
3063 * Accesskeys allow an user to go to a specific element by using
3064 * a shortcut combination of a browser specific keys + the key
3065 * set to the field.
3066 *
3067 * @example
3068 * // AccessKeyedElement provides an 'accesskey' attribute to the
3069 * // ButtonWidget class
3070 * var button = new OO.ui.ButtonWidget( {
3071 * label: 'Button with Accesskey',
3072 * accessKey: 'k'
3073 * } );
3074 * $( 'body' ).append( button.$element );
3075 *
3076 * @abstract
3077 * @class
3078 *
3079 * @constructor
3080 * @param {Object} [config] Configuration options
3081 * @cfg {jQuery} [$accessKeyed] The element to which the `accesskey` attribute is applied.
3082 * If this config is omitted, the accesskey functionality is applied to $element, the
3083 * element created by the class.
3084 * @cfg {string|Function} [accessKey] The key or a function that returns the key. If
3085 * this config is omitted, no accesskey will be added.
3086 */
3087 OO.ui.mixin.AccessKeyedElement = function OoUiMixinAccessKeyedElement( config ) {
3088 // Configuration initialization
3089 config = config || {};
3090
3091 // Properties
3092 this.$accessKeyed = null;
3093 this.accessKey = null;
3094
3095 // Initialization
3096 this.setAccessKey( config.accessKey || null );
3097 this.setAccessKeyedElement( config.$accessKeyed || this.$element );
3098 };
3099
3100 /* Setup */
3101
3102 OO.initClass( OO.ui.mixin.AccessKeyedElement );
3103
3104 /* Static Properties */
3105
3106 /**
3107 * The access key, a function that returns a key, or `null` for no accesskey.
3108 *
3109 * @static
3110 * @inheritable
3111 * @property {string|Function|null}
3112 */
3113 OO.ui.mixin.AccessKeyedElement.static.accessKey = null;
3114
3115 /* Methods */
3116
3117 /**
3118 * Set the accesskeyed element.
3119 *
3120 * This method is used to retarget a AccessKeyedElement mixin so that its functionality applies to the specified element.
3121 * If an element is already set, the mixin's effect on that element is removed before the new element is set up.
3122 *
3123 * @param {jQuery} $accessKeyed Element that should use the 'accesskeyes' functionality
3124 */
3125 OO.ui.mixin.AccessKeyedElement.prototype.setAccessKeyedElement = function ( $accessKeyed ) {
3126 if ( this.$accessKeyed ) {
3127 this.$accessKeyed.removeAttr( 'accesskey' );
3128 }
3129
3130 this.$accessKeyed = $accessKeyed;
3131 if ( this.accessKey ) {
3132 this.$accessKeyed.attr( 'accesskey', this.accessKey );
3133 }
3134 };
3135
3136 /**
3137 * Set accesskey.
3138 *
3139 * @param {string|Function|null} accesskey Key, a function that returns a key, or `null` for no accesskey
3140 * @chainable
3141 */
3142 OO.ui.mixin.AccessKeyedElement.prototype.setAccessKey = function ( accessKey ) {
3143 accessKey = typeof accessKey === 'string' ? OO.ui.resolveMsg( accessKey ) : null;
3144
3145 if ( this.accessKey !== accessKey ) {
3146 if ( this.$accessKeyed ) {
3147 if ( accessKey !== null ) {
3148 this.$accessKeyed.attr( 'accesskey', accessKey );
3149 } else {
3150 this.$accessKeyed.removeAttr( 'accesskey' );
3151 }
3152 }
3153 this.accessKey = accessKey;
3154 }
3155
3156 return this;
3157 };
3158
3159 /**
3160 * Get accesskey.
3161 *
3162 * @return {string} accessKey string
3163 */
3164 OO.ui.mixin.AccessKeyedElement.prototype.getAccessKey = function () {
3165 return this.accessKey;
3166 };
3167
3168 /**
3169 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
3170 * feels, and functionality can be customized via the class’s configuration options
3171 * and methods. Please see the [OOjs UI documentation on MediaWiki] [1] for more information
3172 * and examples.
3173 *
3174 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches
3175 *
3176 * @example
3177 * // A button widget
3178 * var button = new OO.ui.ButtonWidget( {
3179 * label: 'Button with Icon',
3180 * icon: 'remove',
3181 * iconTitle: 'Remove'
3182 * } );
3183 * $( 'body' ).append( button.$element );
3184 *
3185 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
3186 *
3187 * @class
3188 * @extends OO.ui.Widget
3189 * @mixins OO.ui.mixin.ButtonElement
3190 * @mixins OO.ui.mixin.IconElement
3191 * @mixins OO.ui.mixin.IndicatorElement
3192 * @mixins OO.ui.mixin.LabelElement
3193 * @mixins OO.ui.mixin.TitledElement
3194 * @mixins OO.ui.mixin.FlaggedElement
3195 * @mixins OO.ui.mixin.TabIndexedElement
3196 * @mixins OO.ui.mixin.AccessKeyedElement
3197 *
3198 * @constructor
3199 * @param {Object} [config] Configuration options
3200 * @cfg {string} [href] Hyperlink to visit when the button is clicked.
3201 * @cfg {string} [target] The frame or window in which to open the hyperlink.
3202 * @cfg {boolean} [noFollow] Search engine traversal hint (default: true)
3203 */
3204 OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
3205 // Configuration initialization
3206 config = config || {};
3207
3208 // Parent constructor
3209 OO.ui.ButtonWidget.parent.call( this, config );
3210
3211 // Mixin constructors
3212 OO.ui.mixin.ButtonElement.call( this, config );
3213 OO.ui.mixin.IconElement.call( this, config );
3214 OO.ui.mixin.IndicatorElement.call( this, config );
3215 OO.ui.mixin.LabelElement.call( this, config );
3216 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
3217 OO.ui.mixin.FlaggedElement.call( this, config );
3218 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
3219 OO.ui.mixin.AccessKeyedElement.call( this, $.extend( {}, config, { $accessKeyed: this.$button } ) );
3220
3221 // Properties
3222 this.href = null;
3223 this.target = null;
3224 this.noFollow = false;
3225
3226 // Events
3227 this.connect( this, { disable: 'onDisable' } );
3228
3229 // Initialization
3230 this.$button.append( this.$icon, this.$label, this.$indicator );
3231 this.$element
3232 .addClass( 'oo-ui-buttonWidget' )
3233 .append( this.$button );
3234 this.setHref( config.href );
3235 this.setTarget( config.target );
3236 this.setNoFollow( config.noFollow );
3237 };
3238
3239 /* Setup */
3240
3241 OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget );
3242 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.ButtonElement );
3243 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.IconElement );
3244 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.IndicatorElement );
3245 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.LabelElement );
3246 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.TitledElement );
3247 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.FlaggedElement );
3248 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.TabIndexedElement );
3249 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.mixin.AccessKeyedElement );
3250
3251 /* Methods */
3252
3253 /**
3254 * @inheritdoc
3255 */
3256 OO.ui.ButtonWidget.prototype.onMouseDown = function ( e ) {
3257 if ( !this.isDisabled() ) {
3258 // Remove the tab-index while the button is down to prevent the button from stealing focus
3259 this.$button.removeAttr( 'tabindex' );
3260 }
3261
3262 return OO.ui.mixin.ButtonElement.prototype.onMouseDown.call( this, e );
3263 };
3264
3265 /**
3266 * @inheritdoc
3267 */
3268 OO.ui.ButtonWidget.prototype.onMouseUp = function ( e ) {
3269 if ( !this.isDisabled() ) {
3270 // Restore the tab-index after the button is up to restore the button's accessibility
3271 this.$button.attr( 'tabindex', this.tabIndex );
3272 }
3273
3274 return OO.ui.mixin.ButtonElement.prototype.onMouseUp.call( this, e );
3275 };
3276
3277 /**
3278 * Get hyperlink location.
3279 *
3280 * @return {string} Hyperlink location
3281 */
3282 OO.ui.ButtonWidget.prototype.getHref = function () {
3283 return this.href;
3284 };
3285
3286 /**
3287 * Get hyperlink target.
3288 *
3289 * @return {string} Hyperlink target
3290 */
3291 OO.ui.ButtonWidget.prototype.getTarget = function () {
3292 return this.target;
3293 };
3294
3295 /**
3296 * Get search engine traversal hint.
3297 *
3298 * @return {boolean} Whether search engines should avoid traversing this hyperlink
3299 */
3300 OO.ui.ButtonWidget.prototype.getNoFollow = function () {
3301 return this.noFollow;
3302 };
3303
3304 /**
3305 * Set hyperlink location.
3306 *
3307 * @param {string|null} href Hyperlink location, null to remove
3308 */
3309 OO.ui.ButtonWidget.prototype.setHref = function ( href ) {
3310 href = typeof href === 'string' ? href : null;
3311 if ( href !== null && !OO.ui.isSafeUrl( href ) ) {
3312 href = './' + href;
3313 }
3314
3315 if ( href !== this.href ) {
3316 this.href = href;
3317 this.updateHref();
3318 }
3319
3320 return this;
3321 };
3322
3323 /**
3324 * Update the `href` attribute, in case of changes to href or
3325 * disabled state.
3326 *
3327 * @private
3328 * @chainable
3329 */
3330 OO.ui.ButtonWidget.prototype.updateHref = function () {
3331 if ( this.href !== null && !this.isDisabled() ) {
3332 this.$button.attr( 'href', this.href );
3333 } else {
3334 this.$button.removeAttr( 'href' );
3335 }
3336
3337 return this;
3338 };
3339
3340 /**
3341 * Handle disable events.
3342 *
3343 * @private
3344 * @param {boolean} disabled Element is disabled
3345 */
3346 OO.ui.ButtonWidget.prototype.onDisable = function () {
3347 this.updateHref();
3348 };
3349
3350 /**
3351 * Set hyperlink target.
3352 *
3353 * @param {string|null} target Hyperlink target, null to remove
3354 */
3355 OO.ui.ButtonWidget.prototype.setTarget = function ( target ) {
3356 target = typeof target === 'string' ? target : null;
3357
3358 if ( target !== this.target ) {
3359 this.target = target;
3360 if ( target !== null ) {
3361 this.$button.attr( 'target', target );
3362 } else {
3363 this.$button.removeAttr( 'target' );
3364 }
3365 }
3366
3367 return this;
3368 };
3369
3370 /**
3371 * Set search engine traversal hint.
3372 *
3373 * @param {boolean} noFollow True if search engines should avoid traversing this hyperlink
3374 */
3375 OO.ui.ButtonWidget.prototype.setNoFollow = function ( noFollow ) {
3376 noFollow = typeof noFollow === 'boolean' ? noFollow : true;
3377
3378 if ( noFollow !== this.noFollow ) {
3379 this.noFollow = noFollow;
3380 if ( noFollow ) {
3381 this.$button.attr( 'rel', 'nofollow' );
3382 } else {
3383 this.$button.removeAttr( 'rel' );
3384 }
3385 }
3386
3387 return this;
3388 };
3389
3390 /**
3391 * A ButtonGroupWidget groups related buttons and is used together with OO.ui.ButtonWidget and
3392 * its subclasses. Each button in a group is addressed by a unique reference. Buttons can be added,
3393 * removed, and cleared from the group.
3394 *
3395 * @example
3396 * // Example: A ButtonGroupWidget with two buttons
3397 * var button1 = new OO.ui.PopupButtonWidget( {
3398 * label: 'Select a category',
3399 * icon: 'menu',
3400 * popup: {
3401 * $content: $( '<p>List of categories...</p>' ),
3402 * padded: true,
3403 * align: 'left'
3404 * }
3405 * } );
3406 * var button2 = new OO.ui.ButtonWidget( {
3407 * label: 'Add item'
3408 * });
3409 * var buttonGroup = new OO.ui.ButtonGroupWidget( {
3410 * items: [button1, button2]
3411 * } );
3412 * $( 'body' ).append( buttonGroup.$element );
3413 *
3414 * @class
3415 * @extends OO.ui.Widget
3416 * @mixins OO.ui.mixin.GroupElement
3417 *
3418 * @constructor
3419 * @param {Object} [config] Configuration options
3420 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
3421 */
3422 OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
3423 // Configuration initialization
3424 config = config || {};
3425
3426 // Parent constructor
3427 OO.ui.ButtonGroupWidget.parent.call( this, config );
3428
3429 // Mixin constructors
3430 OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
3431
3432 // Initialization
3433 this.$element.addClass( 'oo-ui-buttonGroupWidget' );
3434 if ( Array.isArray( config.items ) ) {
3435 this.addItems( config.items );
3436 }
3437 };
3438
3439 /* Setup */
3440
3441 OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget );
3442 OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.mixin.GroupElement );
3443
3444 /**
3445 * IconWidget is a generic widget for {@link OO.ui.mixin.IconElement icons}. In general, IconWidgets should be used with OO.ui.LabelWidget,
3446 * which creates a label that identifies the icon’s function. See the [OOjs UI documentation on MediaWiki] [1]
3447 * for a list of icons included in the library.
3448 *
3449 * @example
3450 * // An icon widget with a label
3451 * var myIcon = new OO.ui.IconWidget( {
3452 * icon: 'help',
3453 * iconTitle: 'Help'
3454 * } );
3455 * // Create a label.
3456 * var iconLabel = new OO.ui.LabelWidget( {
3457 * label: 'Help'
3458 * } );
3459 * $( 'body' ).append( myIcon.$element, iconLabel.$element );
3460 *
3461 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Icons
3462 *
3463 * @class
3464 * @extends OO.ui.Widget
3465 * @mixins OO.ui.mixin.IconElement
3466 * @mixins OO.ui.mixin.TitledElement
3467 * @mixins OO.ui.mixin.FlaggedElement
3468 *
3469 * @constructor
3470 * @param {Object} [config] Configuration options
3471 */
3472 OO.ui.IconWidget = function OoUiIconWidget( config ) {
3473 // Configuration initialization
3474 config = config || {};
3475
3476 // Parent constructor
3477 OO.ui.IconWidget.parent.call( this, config );
3478
3479 // Mixin constructors
3480 OO.ui.mixin.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) );
3481 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
3482 OO.ui.mixin.FlaggedElement.call( this, $.extend( {}, config, { $flagged: this.$element } ) );
3483
3484 // Initialization
3485 this.$element.addClass( 'oo-ui-iconWidget' );
3486 };
3487
3488 /* Setup */
3489
3490 OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
3491 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.IconElement );
3492 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.TitledElement );
3493 OO.mixinClass( OO.ui.IconWidget, OO.ui.mixin.FlaggedElement );
3494
3495 /* Static Properties */
3496
3497 OO.ui.IconWidget.static.tagName = 'span';
3498
3499 /**
3500 * IndicatorWidgets create indicators, which are small graphics that are generally used to draw
3501 * attention to the status of an item or to clarify the function of a control. For a list of
3502 * indicators included in the library, please see the [OOjs UI documentation on MediaWiki][1].
3503 *
3504 * @example
3505 * // Example of an indicator widget
3506 * var indicator1 = new OO.ui.IndicatorWidget( {
3507 * indicator: 'alert'
3508 * } );
3509 *
3510 * // Create a fieldset layout to add a label
3511 * var fieldset = new OO.ui.FieldsetLayout();
3512 * fieldset.addItems( [
3513 * new OO.ui.FieldLayout( indicator1, { label: 'An alert indicator:' } )
3514 * ] );
3515 * $( 'body' ).append( fieldset.$element );
3516 *
3517 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Icons,_Indicators,_and_Labels#Indicators
3518 *
3519 * @class
3520 * @extends OO.ui.Widget
3521 * @mixins OO.ui.mixin.IndicatorElement
3522 * @mixins OO.ui.mixin.TitledElement
3523 *
3524 * @constructor
3525 * @param {Object} [config] Configuration options
3526 */
3527 OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
3528 // Configuration initialization
3529 config = config || {};
3530
3531 // Parent constructor
3532 OO.ui.IndicatorWidget.parent.call( this, config );
3533
3534 // Mixin constructors
3535 OO.ui.mixin.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) );
3536 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
3537
3538 // Initialization
3539 this.$element.addClass( 'oo-ui-indicatorWidget' );
3540 };
3541
3542 /* Setup */
3543
3544 OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
3545 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.mixin.IndicatorElement );
3546 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.mixin.TitledElement );
3547
3548 /* Static Properties */
3549
3550 OO.ui.IndicatorWidget.static.tagName = 'span';
3551
3552 /**
3553 * LabelWidgets help identify the function of interface elements. Each LabelWidget can
3554 * be configured with a `label` option that is set to a string, a label node, or a function:
3555 *
3556 * - String: a plaintext string
3557 * - jQuery selection: a jQuery selection, used for anything other than a plaintext label, e.g., a
3558 * label that includes a link or special styling, such as a gray color or additional graphical elements.
3559 * - Function: a function that will produce a string in the future. Functions are used
3560 * in cases where the value of the label is not currently defined.
3561 *
3562 * In addition, the LabelWidget can be associated with an {@link OO.ui.InputWidget input widget}, which
3563 * will come into focus when the label is clicked.
3564 *
3565 * @example
3566 * // Examples of LabelWidgets
3567 * var label1 = new OO.ui.LabelWidget( {
3568 * label: 'plaintext label'
3569 * } );
3570 * var label2 = new OO.ui.LabelWidget( {
3571 * label: $( '<a href="default.html">jQuery label</a>' )
3572 * } );
3573 * // Create a fieldset layout with fields for each example
3574 * var fieldset = new OO.ui.FieldsetLayout();
3575 * fieldset.addItems( [
3576 * new OO.ui.FieldLayout( label1 ),
3577 * new OO.ui.FieldLayout( label2 )
3578 * ] );
3579 * $( 'body' ).append( fieldset.$element );
3580 *
3581 * @class
3582 * @extends OO.ui.Widget
3583 * @mixins OO.ui.mixin.LabelElement
3584 *
3585 * @constructor
3586 * @param {Object} [config] Configuration options
3587 * @cfg {OO.ui.InputWidget} [input] {@link OO.ui.InputWidget Input widget} that uses the label.
3588 * Clicking the label will focus the specified input field.
3589 */
3590 OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
3591 // Configuration initialization
3592 config = config || {};
3593
3594 // Parent constructor
3595 OO.ui.LabelWidget.parent.call( this, config );
3596
3597 // Mixin constructors
3598 OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, { $label: this.$element } ) );
3599 OO.ui.mixin.TitledElement.call( this, config );
3600
3601 // Properties
3602 this.input = config.input;
3603
3604 // Events
3605 if ( this.input instanceof OO.ui.InputWidget ) {
3606 this.$element.on( 'click', this.onClick.bind( this ) );
3607 }
3608
3609 // Initialization
3610 this.$element.addClass( 'oo-ui-labelWidget' );
3611 };
3612
3613 /* Setup */
3614
3615 OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
3616 OO.mixinClass( OO.ui.LabelWidget, OO.ui.mixin.LabelElement );
3617 OO.mixinClass( OO.ui.LabelWidget, OO.ui.mixin.TitledElement );
3618
3619 /* Static Properties */
3620
3621 OO.ui.LabelWidget.static.tagName = 'span';
3622
3623 /* Methods */
3624
3625 /**
3626 * Handles label mouse click events.
3627 *
3628 * @private
3629 * @param {jQuery.Event} e Mouse click event
3630 */
3631 OO.ui.LabelWidget.prototype.onClick = function () {
3632 this.input.simulateLabelClick();
3633 return false;
3634 };
3635
3636 /**
3637 * PendingElement is a mixin that is used to create elements that notify users that something is happening
3638 * and that they should wait before proceeding. The pending state is visually represented with a pending
3639 * texture that appears in the head of a pending {@link OO.ui.ProcessDialog process dialog} or in the input
3640 * field of a {@link OO.ui.TextInputWidget text input widget}.
3641 *
3642 * Currently, {@link OO.ui.ActionWidget Action widgets}, which mix in this class, can also be marked as pending, but only when
3643 * used in {@link OO.ui.MessageDialog message dialogs}. The behavior is not currently supported for action widgets used
3644 * in process dialogs.
3645 *
3646 * @example
3647 * function MessageDialog( config ) {
3648 * MessageDialog.parent.call( this, config );
3649 * }
3650 * OO.inheritClass( MessageDialog, OO.ui.MessageDialog );
3651 *
3652 * MessageDialog.static.actions = [
3653 * { action: 'save', label: 'Done', flags: 'primary' },
3654 * { label: 'Cancel', flags: 'safe' }
3655 * ];
3656 *
3657 * MessageDialog.prototype.initialize = function () {
3658 * MessageDialog.parent.prototype.initialize.apply( this, arguments );
3659 * this.content = new OO.ui.PanelLayout( { $: this.$, padded: true } );
3660 * 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>' );
3661 * this.$body.append( this.content.$element );
3662 * };
3663 * MessageDialog.prototype.getBodyHeight = function () {
3664 * return 100;
3665 * }
3666 * MessageDialog.prototype.getActionProcess = function ( action ) {
3667 * var dialog = this;
3668 * if ( action === 'save' ) {
3669 * dialog.getActions().get({actions: 'save'})[0].pushPending();
3670 * return new OO.ui.Process()
3671 * .next( 1000 )
3672 * .next( function () {
3673 * dialog.getActions().get({actions: 'save'})[0].popPending();
3674 * } );
3675 * }
3676 * return MessageDialog.parent.prototype.getActionProcess.call( this, action );
3677 * };
3678 *
3679 * var windowManager = new OO.ui.WindowManager();
3680 * $( 'body' ).append( windowManager.$element );
3681 *
3682 * var dialog = new MessageDialog();
3683 * windowManager.addWindows( [ dialog ] );
3684 * windowManager.openWindow( dialog );
3685 *
3686 * @abstract
3687 * @class
3688 *
3689 * @constructor
3690 * @param {Object} [config] Configuration options
3691 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
3692 */
3693 OO.ui.mixin.PendingElement = function OoUiMixinPendingElement( config ) {
3694 // Configuration initialization
3695 config = config || {};
3696
3697 // Properties
3698 this.pending = 0;
3699 this.$pending = null;
3700
3701 // Initialisation
3702 this.setPendingElement( config.$pending || this.$element );
3703 };
3704
3705 /* Setup */
3706
3707 OO.initClass( OO.ui.mixin.PendingElement );
3708
3709 /* Methods */
3710
3711 /**
3712 * Set the pending element (and clean up any existing one).
3713 *
3714 * @param {jQuery} $pending The element to set to pending.
3715 */
3716 OO.ui.mixin.PendingElement.prototype.setPendingElement = function ( $pending ) {
3717 if ( this.$pending ) {
3718 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
3719 }
3720
3721 this.$pending = $pending;
3722 if ( this.pending > 0 ) {
3723 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
3724 }
3725 };
3726
3727 /**
3728 * Check if an element is pending.
3729 *
3730 * @return {boolean} Element is pending
3731 */
3732 OO.ui.mixin.PendingElement.prototype.isPending = function () {
3733 return !!this.pending;
3734 };
3735
3736 /**
3737 * Increase the pending counter. The pending state will remain active until the counter is zero
3738 * (i.e., the number of calls to #pushPending and #popPending is the same).
3739 *
3740 * @chainable
3741 */
3742 OO.ui.mixin.PendingElement.prototype.pushPending = function () {
3743 if ( this.pending === 0 ) {
3744 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
3745 this.updateThemeClasses();
3746 }
3747 this.pending++;
3748
3749 return this;
3750 };
3751
3752 /**
3753 * Decrease the pending counter. The pending state will remain active until the counter is zero
3754 * (i.e., the number of calls to #pushPending and #popPending is the same).
3755 *
3756 * @chainable
3757 */
3758 OO.ui.mixin.PendingElement.prototype.popPending = function () {
3759 if ( this.pending === 1 ) {
3760 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
3761 this.updateThemeClasses();
3762 }
3763 this.pending = Math.max( 0, this.pending - 1 );
3764
3765 return this;
3766 };
3767
3768 /**
3769 * Element that can be automatically clipped to visible boundaries.
3770 *
3771 * Whenever the element's natural height changes, you have to call
3772 * {@link OO.ui.mixin.ClippableElement#clip} to make sure it's still
3773 * clipping correctly.
3774 *
3775 * The dimensions of #$clippableContainer will be compared to the boundaries of the
3776 * nearest scrollable container. If #$clippableContainer is too tall and/or too wide,
3777 * then #$clippable will be given a fixed reduced height and/or width and will be made
3778 * scrollable. By default, #$clippable and #$clippableContainer are the same element,
3779 * but you can build a static footer by setting #$clippableContainer to an element that contains
3780 * #$clippable and the footer.
3781 *
3782 * @abstract
3783 * @class
3784 *
3785 * @constructor
3786 * @param {Object} [config] Configuration options
3787 * @cfg {jQuery} [$clippable] Node to clip, assigned to #$clippable, omit to use #$element
3788 * @cfg {jQuery} [$clippableContainer] Node to keep visible, assigned to #$clippableContainer,
3789 * omit to use #$clippable
3790 */
3791 OO.ui.mixin.ClippableElement = function OoUiMixinClippableElement( config ) {
3792 // Configuration initialization
3793 config = config || {};
3794
3795 // Properties
3796 this.$clippable = null;
3797 this.$clippableContainer = null;
3798 this.clipping = false;
3799 this.clippedHorizontally = false;
3800 this.clippedVertically = false;
3801 this.$clippableScrollableContainer = null;
3802 this.$clippableScroller = null;
3803 this.$clippableWindow = null;
3804 this.idealWidth = null;
3805 this.idealHeight = null;
3806 this.onClippableScrollHandler = this.clip.bind( this );
3807 this.onClippableWindowResizeHandler = this.clip.bind( this );
3808
3809 // Initialization
3810 if ( config.$clippableContainer ) {
3811 this.setClippableContainer( config.$clippableContainer );
3812 }
3813 this.setClippableElement( config.$clippable || this.$element );
3814 };
3815
3816 /* Methods */
3817
3818 /**
3819 * Set clippable element.
3820 *
3821 * If an element is already set, it will be cleaned up before setting up the new element.
3822 *
3823 * @param {jQuery} $clippable Element to make clippable
3824 */
3825 OO.ui.mixin.ClippableElement.prototype.setClippableElement = function ( $clippable ) {
3826 if ( this.$clippable ) {
3827 this.$clippable.removeClass( 'oo-ui-clippableElement-clippable' );
3828 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
3829 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
3830 }
3831
3832 this.$clippable = $clippable.addClass( 'oo-ui-clippableElement-clippable' );
3833 this.clip();
3834 };
3835
3836 /**
3837 * Set clippable container.
3838 *
3839 * This is the container that will be measured when deciding whether to clip. When clipping,
3840 * #$clippable will be resized in order to keep the clippable container fully visible.
3841 *
3842 * If the clippable container is unset, #$clippable will be used.
3843 *
3844 * @param {jQuery|null} $clippableContainer Container to keep visible, or null to unset
3845 */
3846 OO.ui.mixin.ClippableElement.prototype.setClippableContainer = function ( $clippableContainer ) {
3847 this.$clippableContainer = $clippableContainer;
3848 if ( this.$clippable ) {
3849 this.clip();
3850 }
3851 };
3852
3853 /**
3854 * Toggle clipping.
3855 *
3856 * Do not turn clipping on until after the element is attached to the DOM and visible.
3857 *
3858 * @param {boolean} [clipping] Enable clipping, omit to toggle
3859 * @chainable
3860 */
3861 OO.ui.mixin.ClippableElement.prototype.toggleClipping = function ( clipping ) {
3862 clipping = clipping === undefined ? !this.clipping : !!clipping;
3863
3864 if ( this.clipping !== clipping ) {
3865 this.clipping = clipping;
3866 if ( clipping ) {
3867 this.$clippableScrollableContainer = $( this.getClosestScrollableElementContainer() );
3868 // If the clippable container is the root, we have to listen to scroll events and check
3869 // jQuery.scrollTop on the window because of browser inconsistencies
3870 this.$clippableScroller = this.$clippableScrollableContainer.is( 'html, body' ) ?
3871 $( OO.ui.Element.static.getWindow( this.$clippableScrollableContainer ) ) :
3872 this.$clippableScrollableContainer;
3873 this.$clippableScroller.on( 'scroll', this.onClippableScrollHandler );
3874 this.$clippableWindow = $( this.getElementWindow() )
3875 .on( 'resize', this.onClippableWindowResizeHandler );
3876 // Initial clip after visible
3877 this.clip();
3878 } else {
3879 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
3880 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
3881
3882 this.$clippableScrollableContainer = null;
3883 this.$clippableScroller.off( 'scroll', this.onClippableScrollHandler );
3884 this.$clippableScroller = null;
3885 this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
3886 this.$clippableWindow = null;
3887 }
3888 }
3889
3890 return this;
3891 };
3892
3893 /**
3894 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
3895 *
3896 * @return {boolean} Element will be clipped to the visible area
3897 */
3898 OO.ui.mixin.ClippableElement.prototype.isClipping = function () {
3899 return this.clipping;
3900 };
3901
3902 /**
3903 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
3904 *
3905 * @return {boolean} Part of the element is being clipped
3906 */
3907 OO.ui.mixin.ClippableElement.prototype.isClipped = function () {
3908 return this.clippedHorizontally || this.clippedVertically;
3909 };
3910
3911 /**
3912 * Check if the right of the element is being clipped by the nearest scrollable container.
3913 *
3914 * @return {boolean} Part of the element is being clipped
3915 */
3916 OO.ui.mixin.ClippableElement.prototype.isClippedHorizontally = function () {
3917 return this.clippedHorizontally;
3918 };
3919
3920 /**
3921 * Check if the bottom of the element is being clipped by the nearest scrollable container.
3922 *
3923 * @return {boolean} Part of the element is being clipped
3924 */
3925 OO.ui.mixin.ClippableElement.prototype.isClippedVertically = function () {
3926 return this.clippedVertically;
3927 };
3928
3929 /**
3930 * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
3931 *
3932 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
3933 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
3934 */
3935 OO.ui.mixin.ClippableElement.prototype.setIdealSize = function ( width, height ) {
3936 this.idealWidth = width;
3937 this.idealHeight = height;
3938
3939 if ( !this.clipping ) {
3940 // Update dimensions
3941 this.$clippable.css( { width: width, height: height } );
3942 }
3943 // While clipping, idealWidth and idealHeight are not considered
3944 };
3945
3946 /**
3947 * Clip element to visible boundaries and allow scrolling when needed. Call this method when
3948 * the element's natural height changes.
3949 *
3950 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
3951 * overlapped by, the visible area of the nearest scrollable container.
3952 *
3953 * @chainable
3954 */
3955 OO.ui.mixin.ClippableElement.prototype.clip = function () {
3956 var $container, extraHeight, extraWidth, ccOffset,
3957 $scrollableContainer, scOffset, scHeight, scWidth,
3958 ccWidth, scrollerIsWindow, scrollTop, scrollLeft,
3959 desiredWidth, desiredHeight, allotedWidth, allotedHeight,
3960 naturalWidth, naturalHeight, clipWidth, clipHeight,
3961 buffer = 7; // Chosen by fair dice roll
3962
3963 if ( !this.clipping ) {
3964 // this.$clippableScrollableContainer and this.$clippableWindow are null, so the below will fail
3965 return this;
3966 }
3967
3968 $container = this.$clippableContainer || this.$clippable;
3969 extraHeight = $container.outerHeight() - this.$clippable.outerHeight();
3970 extraWidth = $container.outerWidth() - this.$clippable.outerWidth();
3971 ccOffset = $container.offset();
3972 $scrollableContainer = this.$clippableScrollableContainer.is( 'html, body' ) ?
3973 this.$clippableWindow : this.$clippableScrollableContainer;
3974 scOffset = $scrollableContainer.offset() || { top: 0, left: 0 };
3975 scHeight = $scrollableContainer.innerHeight() - buffer;
3976 scWidth = $scrollableContainer.innerWidth() - buffer;
3977 ccWidth = $container.outerWidth() + buffer;
3978 scrollerIsWindow = this.$clippableScroller[ 0 ] === this.$clippableWindow[ 0 ];
3979 scrollTop = scrollerIsWindow ? this.$clippableScroller.scrollTop() : 0;
3980 scrollLeft = scrollerIsWindow ? this.$clippableScroller.scrollLeft() : 0;
3981 desiredWidth = ccOffset.left < 0 ?
3982 ccWidth + ccOffset.left :
3983 ( scOffset.left + scrollLeft + scWidth ) - ccOffset.left;
3984 desiredHeight = ( scOffset.top + scrollTop + scHeight ) - ccOffset.top;
3985 allotedWidth = Math.ceil( desiredWidth - extraWidth );
3986 allotedHeight = Math.ceil( desiredHeight - extraHeight );
3987 naturalWidth = this.$clippable.prop( 'scrollWidth' );
3988 naturalHeight = this.$clippable.prop( 'scrollHeight' );
3989 clipWidth = allotedWidth < naturalWidth;
3990 clipHeight = allotedHeight < naturalHeight;
3991
3992 if ( clipWidth ) {
3993 this.$clippable.css( { overflowX: 'scroll', width: Math.max( 0, allotedWidth ) } );
3994 } else {
3995 this.$clippable.css( { width: this.idealWidth ? this.idealWidth - extraWidth : '', overflowX: '' } );
3996 }
3997 if ( clipHeight ) {
3998 this.$clippable.css( { overflowY: 'scroll', height: Math.max( 0, allotedHeight ) } );
3999 } else {
4000 this.$clippable.css( { height: this.idealHeight ? this.idealHeight - extraHeight : '', overflowY: '' } );
4001 }
4002
4003 // If we stopped clipping in at least one of the dimensions
4004 if ( ( this.clippedHorizontally && !clipWidth ) || ( this.clippedVertically && !clipHeight ) ) {
4005 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
4006 }
4007
4008 this.clippedHorizontally = clipWidth;
4009 this.clippedVertically = clipHeight;
4010
4011 return this;
4012 };
4013
4014 /**
4015 * PopupWidget is a container for content. The popup is overlaid and positioned absolutely.
4016 * By default, each popup has an anchor that points toward its origin.
4017 * Please see the [OOjs UI documentation on Mediawiki] [1] for more information and examples.
4018 *
4019 * @example
4020 * // A popup widget.
4021 * var popup = new OO.ui.PopupWidget( {
4022 * $content: $( '<p>Hi there!</p>' ),
4023 * padded: true,
4024 * width: 300
4025 * } );
4026 *
4027 * $( 'body' ).append( popup.$element );
4028 * // To display the popup, toggle the visibility to 'true'.
4029 * popup.toggle( true );
4030 *
4031 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups
4032 *
4033 * @class
4034 * @extends OO.ui.Widget
4035 * @mixins OO.ui.mixin.LabelElement
4036 * @mixins OO.ui.mixin.ClippableElement
4037 *
4038 * @constructor
4039 * @param {Object} [config] Configuration options
4040 * @cfg {number} [width=320] Width of popup in pixels
4041 * @cfg {number} [height] Height of popup in pixels. Omit to use the automatic height.
4042 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
4043 * @cfg {string} [align='center'] Alignment of the popup: `center`, `force-left`, `force-right`, `backwards` or `forwards`.
4044 * If the popup is forced-left the popup body is leaning towards the left. For force-right alignment, the body of the
4045 * popup is leaning towards the right of the screen.
4046 * Using 'backwards' is a logical direction which will result in the popup leaning towards the beginning of the sentence
4047 * in the given language, which means it will flip to the correct positioning in right-to-left languages.
4048 * Using 'forward' will also result in a logical alignment where the body of the popup leans towards the end of the
4049 * sentence in the given language.
4050 * @cfg {jQuery} [$container] Constrain the popup to the boundaries of the specified container.
4051 * See the [OOjs UI docs on MediaWiki][3] for an example.
4052 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#containerExample
4053 * @cfg {number} [containerPadding=10] Padding between the popup and its container, specified as a number of pixels.
4054 * @cfg {jQuery} [$content] Content to append to the popup's body
4055 * @cfg {jQuery} [$footer] Content to append to the popup's footer
4056 * @cfg {boolean} [autoClose=false] Automatically close the popup when it loses focus.
4057 * @cfg {jQuery} [$autoCloseIgnore] Elements that will not close the popup when clicked.
4058 * This config option is only relevant if #autoClose is set to `true`. See the [OOjs UI docs on MediaWiki][2]
4059 * for an example.
4060 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Popups#autocloseExample
4061 * @cfg {boolean} [head] Show a popup header that contains a #label (if specified) and close
4062 * button.
4063 * @cfg {boolean} [padded] Add padding to the popup's body
4064 */
4065 OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
4066 // Configuration initialization
4067 config = config || {};
4068
4069 // Parent constructor
4070 OO.ui.PopupWidget.parent.call( this, config );
4071
4072 // Properties (must be set before ClippableElement constructor call)
4073 this.$body = $( '<div>' );
4074 this.$popup = $( '<div>' );
4075
4076 // Mixin constructors
4077 OO.ui.mixin.LabelElement.call( this, config );
4078 OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, {
4079 $clippable: this.$body,
4080 $clippableContainer: this.$popup
4081 } ) );
4082
4083 // Properties
4084 this.$head = $( '<div>' );
4085 this.$footer = $( '<div>' );
4086 this.$anchor = $( '<div>' );
4087 // If undefined, will be computed lazily in updateDimensions()
4088 this.$container = config.$container;
4089 this.containerPadding = config.containerPadding !== undefined ? config.containerPadding : 10;
4090 this.autoClose = !!config.autoClose;
4091 this.$autoCloseIgnore = config.$autoCloseIgnore;
4092 this.transitionTimeout = null;
4093 this.anchor = null;
4094 this.width = config.width !== undefined ? config.width : 320;
4095 this.height = config.height !== undefined ? config.height : null;
4096 this.setAlignment( config.align );
4097 this.closeButton = new OO.ui.ButtonWidget( { framed: false, icon: 'close' } );
4098 this.onMouseDownHandler = this.onMouseDown.bind( this );
4099 this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
4100
4101 // Events
4102 this.closeButton.connect( this, { click: 'onCloseButtonClick' } );
4103
4104 // Initialization
4105 this.toggleAnchor( config.anchor === undefined || config.anchor );
4106 this.$body.addClass( 'oo-ui-popupWidget-body' );
4107 this.$anchor.addClass( 'oo-ui-popupWidget-anchor' );
4108 this.$head
4109 .addClass( 'oo-ui-popupWidget-head' )
4110 .append( this.$label, this.closeButton.$element );
4111 this.$footer.addClass( 'oo-ui-popupWidget-footer' );
4112 if ( !config.head ) {
4113 this.$head.addClass( 'oo-ui-element-hidden' );
4114 }
4115 if ( !config.$footer ) {
4116 this.$footer.addClass( 'oo-ui-element-hidden' );
4117 }
4118 this.$popup
4119 .addClass( 'oo-ui-popupWidget-popup' )
4120 .append( this.$head, this.$body, this.$footer );
4121 this.$element
4122 .addClass( 'oo-ui-popupWidget' )
4123 .append( this.$popup, this.$anchor );
4124 // Move content, which was added to #$element by OO.ui.Widget, to the body
4125 if ( config.$content instanceof jQuery ) {
4126 this.$body.append( config.$content );
4127 }
4128 if ( config.$footer instanceof jQuery ) {
4129 this.$footer.append( config.$footer );
4130 }
4131 if ( config.padded ) {
4132 this.$body.addClass( 'oo-ui-popupWidget-body-padded' );
4133 }
4134
4135 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
4136 // that reference properties not initialized at that time of parent class construction
4137 // TODO: Find a better way to handle post-constructor setup
4138 this.visible = false;
4139 this.$element.addClass( 'oo-ui-element-hidden' );
4140 };
4141
4142 /* Setup */
4143
4144 OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
4145 OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.LabelElement );
4146 OO.mixinClass( OO.ui.PopupWidget, OO.ui.mixin.ClippableElement );
4147
4148 /* Methods */
4149
4150 /**
4151 * Handles mouse down events.
4152 *
4153 * @private
4154 * @param {MouseEvent} e Mouse down event
4155 */
4156 OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
4157 if (
4158 this.isVisible() &&
4159 !$.contains( this.$element[ 0 ], e.target ) &&
4160 ( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length )
4161 ) {
4162 this.toggle( false );
4163 }
4164 };
4165
4166 /**
4167 * Bind mouse down listener.
4168 *
4169 * @private
4170 */
4171 OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
4172 // Capture clicks outside popup
4173 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
4174 };
4175
4176 /**
4177 * Handles close button click events.
4178 *
4179 * @private
4180 */
4181 OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
4182 if ( this.isVisible() ) {
4183 this.toggle( false );
4184 }
4185 };
4186
4187 /**
4188 * Unbind mouse down listener.
4189 *
4190 * @private
4191 */
4192 OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
4193 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
4194 };
4195
4196 /**
4197 * Handles key down events.
4198 *
4199 * @private
4200 * @param {KeyboardEvent} e Key down event
4201 */
4202 OO.ui.PopupWidget.prototype.onDocumentKeyDown = function ( e ) {
4203 if (
4204 e.which === OO.ui.Keys.ESCAPE &&
4205 this.isVisible()
4206 ) {
4207 this.toggle( false );
4208 e.preventDefault();
4209 e.stopPropagation();
4210 }
4211 };
4212
4213 /**
4214 * Bind key down listener.
4215 *
4216 * @private
4217 */
4218 OO.ui.PopupWidget.prototype.bindKeyDownListener = function () {
4219 this.getElementWindow().addEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
4220 };
4221
4222 /**
4223 * Unbind key down listener.
4224 *
4225 * @private
4226 */
4227 OO.ui.PopupWidget.prototype.unbindKeyDownListener = function () {
4228 this.getElementWindow().removeEventListener( 'keydown', this.onDocumentKeyDownHandler, true );
4229 };
4230
4231 /**
4232 * Show, hide, or toggle the visibility of the anchor.
4233 *
4234 * @param {boolean} [show] Show anchor, omit to toggle
4235 */
4236 OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) {
4237 show = show === undefined ? !this.anchored : !!show;
4238
4239 if ( this.anchored !== show ) {
4240 if ( show ) {
4241 this.$element.addClass( 'oo-ui-popupWidget-anchored' );
4242 } else {
4243 this.$element.removeClass( 'oo-ui-popupWidget-anchored' );
4244 }
4245 this.anchored = show;
4246 }
4247 };
4248
4249 /**
4250 * Check if the anchor is visible.
4251 *
4252 * @return {boolean} Anchor is visible
4253 */
4254 OO.ui.PopupWidget.prototype.hasAnchor = function () {
4255 return this.anchor;
4256 };
4257
4258 /**
4259 * @inheritdoc
4260 */
4261 OO.ui.PopupWidget.prototype.toggle = function ( show ) {
4262 var change;
4263 show = show === undefined ? !this.isVisible() : !!show;
4264
4265 change = show !== this.isVisible();
4266
4267 // Parent method
4268 OO.ui.PopupWidget.parent.prototype.toggle.call( this, show );
4269
4270 if ( change ) {
4271 if ( show ) {
4272 if ( this.autoClose ) {
4273 this.bindMouseDownListener();
4274 this.bindKeyDownListener();
4275 }
4276 this.updateDimensions();
4277 this.toggleClipping( true );
4278 } else {
4279 this.toggleClipping( false );
4280 if ( this.autoClose ) {
4281 this.unbindMouseDownListener();
4282 this.unbindKeyDownListener();
4283 }
4284 }
4285 }
4286
4287 return this;
4288 };
4289
4290 /**
4291 * Set the size of the popup.
4292 *
4293 * Changing the size may also change the popup's position depending on the alignment.
4294 *
4295 * @param {number} width Width in pixels
4296 * @param {number} height Height in pixels
4297 * @param {boolean} [transition=false] Use a smooth transition
4298 * @chainable
4299 */
4300 OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) {
4301 this.width = width;
4302 this.height = height !== undefined ? height : null;
4303 if ( this.isVisible() ) {
4304 this.updateDimensions( transition );
4305 }
4306 };
4307
4308 /**
4309 * Update the size and position.
4310 *
4311 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
4312 * be called automatically.
4313 *
4314 * @param {boolean} [transition=false] Use a smooth transition
4315 * @chainable
4316 */
4317 OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
4318 var popupOffset, originOffset, containerLeft, containerWidth, containerRight,
4319 popupLeft, popupRight, overlapLeft, overlapRight, anchorWidth,
4320 align = this.align,
4321 widget = this;
4322
4323 if ( !this.$container ) {
4324 // Lazy-initialize $container if not specified in constructor
4325 this.$container = $( this.getClosestScrollableElementContainer() );
4326 }
4327
4328 // Set height and width before measuring things, since it might cause our measurements
4329 // to change (e.g. due to scrollbars appearing or disappearing)
4330 this.$popup.css( {
4331 width: this.width,
4332 height: this.height !== null ? this.height : 'auto'
4333 } );
4334
4335 // If we are in RTL, we need to flip the alignment, unless it is center
4336 if ( align === 'forwards' || align === 'backwards' ) {
4337 if ( this.$container.css( 'direction' ) === 'rtl' ) {
4338 align = ( { forwards: 'force-left', backwards: 'force-right' } )[ this.align ];
4339 } else {
4340 align = ( { forwards: 'force-right', backwards: 'force-left' } )[ this.align ];
4341 }
4342
4343 }
4344
4345 // Compute initial popupOffset based on alignment
4346 popupOffset = this.width * ( { 'force-left': -1, center: -0.5, 'force-right': 0 } )[ align ];
4347
4348 // Figure out if this will cause the popup to go beyond the edge of the container
4349 originOffset = this.$element.offset().left;
4350 containerLeft = this.$container.offset().left;
4351 containerWidth = this.$container.innerWidth();
4352 containerRight = containerLeft + containerWidth;
4353 popupLeft = popupOffset - this.containerPadding;
4354 popupRight = popupOffset + this.containerPadding + this.width + this.containerPadding;
4355 overlapLeft = ( originOffset + popupLeft ) - containerLeft;
4356 overlapRight = containerRight - ( originOffset + popupRight );
4357
4358 // Adjust offset to make the popup not go beyond the edge, if needed
4359 if ( overlapRight < 0 ) {
4360 popupOffset += overlapRight;
4361 } else if ( overlapLeft < 0 ) {
4362 popupOffset -= overlapLeft;
4363 }
4364
4365 // Adjust offset to avoid anchor being rendered too close to the edge
4366 // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
4367 // TODO: Find a measurement that works for CSS anchors and image anchors
4368 anchorWidth = this.$anchor[ 0 ].scrollWidth * 2;
4369 if ( popupOffset + this.width < anchorWidth ) {
4370 popupOffset = anchorWidth - this.width;
4371 } else if ( -popupOffset < anchorWidth ) {
4372 popupOffset = -anchorWidth;
4373 }
4374
4375 // Prevent transition from being interrupted
4376 clearTimeout( this.transitionTimeout );
4377 if ( transition ) {
4378 // Enable transition
4379 this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
4380 }
4381
4382 // Position body relative to anchor
4383 this.$popup.css( 'margin-left', popupOffset );
4384
4385 if ( transition ) {
4386 // Prevent transitioning after transition is complete
4387 this.transitionTimeout = setTimeout( function () {
4388 widget.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
4389 }, 200 );
4390 } else {
4391 // Prevent transitioning immediately
4392 this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
4393 }
4394
4395 // Reevaluate clipping state since we've relocated and resized the popup
4396 this.clip();
4397
4398 return this;
4399 };
4400
4401 /**
4402 * Set popup alignment
4403 * @param {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
4404 * `backwards` or `forwards`.
4405 */
4406 OO.ui.PopupWidget.prototype.setAlignment = function ( align ) {
4407 // Validate alignment and transform deprecated values
4408 if ( [ 'left', 'right', 'force-left', 'force-right', 'backwards', 'forwards', 'center' ].indexOf( align ) > -1 ) {
4409 this.align = { left: 'force-right', right: 'force-left' }[ align ] || align;
4410 } else {
4411 this.align = 'center';
4412 }
4413 };
4414
4415 /**
4416 * Get popup alignment
4417 * @return {string} align Alignment of the popup, `center`, `force-left`, `force-right`,
4418 * `backwards` or `forwards`.
4419 */
4420 OO.ui.PopupWidget.prototype.getAlignment = function () {
4421 return this.align;
4422 };
4423
4424 /**
4425 * PopupElement is mixed into other classes to generate a {@link OO.ui.PopupWidget popup widget}.
4426 * A popup is a container for content. It is overlaid and positioned absolutely. By default, each
4427 * popup has an anchor, which is an arrow-like protrusion that points toward the popup’s origin.
4428 * See {@link OO.ui.PopupWidget PopupWidget} for an example.
4429 *
4430 * @abstract
4431 * @class
4432 *
4433 * @constructor
4434 * @param {Object} [config] Configuration options
4435 * @cfg {Object} [popup] Configuration to pass to popup
4436 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
4437 */
4438 OO.ui.mixin.PopupElement = function OoUiMixinPopupElement( config ) {
4439 // Configuration initialization
4440 config = config || {};
4441
4442 // Properties
4443 this.popup = new OO.ui.PopupWidget( $.extend(
4444 { autoClose: true },
4445 config.popup,
4446 { $autoCloseIgnore: this.$element }
4447 ) );
4448 };
4449
4450 /* Methods */
4451
4452 /**
4453 * Get popup.
4454 *
4455 * @return {OO.ui.PopupWidget} Popup widget
4456 */
4457 OO.ui.mixin.PopupElement.prototype.getPopup = function () {
4458 return this.popup;
4459 };
4460
4461 /**
4462 * PopupButtonWidgets toggle the visibility of a contained {@link OO.ui.PopupWidget PopupWidget},
4463 * which is used to display additional information or options.
4464 *
4465 * @example
4466 * // Example of a popup button.
4467 * var popupButton = new OO.ui.PopupButtonWidget( {
4468 * label: 'Popup button with options',
4469 * icon: 'menu',
4470 * popup: {
4471 * $content: $( '<p>Additional options here.</p>' ),
4472 * padded: true,
4473 * align: 'force-left'
4474 * }
4475 * } );
4476 * // Append the button to the DOM.
4477 * $( 'body' ).append( popupButton.$element );
4478 *
4479 * @class
4480 * @extends OO.ui.ButtonWidget
4481 * @mixins OO.ui.mixin.PopupElement
4482 *
4483 * @constructor
4484 * @param {Object} [config] Configuration options
4485 */
4486 OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
4487 // Parent constructor
4488 OO.ui.PopupButtonWidget.parent.call( this, config );
4489
4490 // Mixin constructors
4491 OO.ui.mixin.PopupElement.call( this, config );
4492
4493 // Events
4494 this.connect( this, { click: 'onAction' } );
4495
4496 // Initialization
4497 this.$element
4498 .addClass( 'oo-ui-popupButtonWidget' )
4499 .attr( 'aria-haspopup', 'true' )
4500 .append( this.popup.$element );
4501 };
4502
4503 /* Setup */
4504
4505 OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
4506 OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.mixin.PopupElement );
4507
4508 /* Methods */
4509
4510 /**
4511 * Handle the button action being triggered.
4512 *
4513 * @private
4514 */
4515 OO.ui.PopupButtonWidget.prototype.onAction = function () {
4516 this.popup.toggle();
4517 };
4518
4519 /**
4520 * Mixin for OO.ui.Widget subclasses to provide OO.ui.mixin.GroupElement.
4521 *
4522 * Use together with OO.ui.mixin.ItemWidget to make disabled state inheritable.
4523 *
4524 * @private
4525 * @abstract
4526 * @class
4527 * @extends OO.ui.mixin.GroupElement
4528 *
4529 * @constructor
4530 * @param {Object} [config] Configuration options
4531 */
4532 OO.ui.mixin.GroupWidget = function OoUiMixinGroupWidget( config ) {
4533 // Parent constructor
4534 OO.ui.mixin.GroupWidget.parent.call( this, config );
4535 };
4536
4537 /* Setup */
4538
4539 OO.inheritClass( OO.ui.mixin.GroupWidget, OO.ui.mixin.GroupElement );
4540
4541 /* Methods */
4542
4543 /**
4544 * Set the disabled state of the widget.
4545 *
4546 * This will also update the disabled state of child widgets.
4547 *
4548 * @param {boolean} disabled Disable widget
4549 * @chainable
4550 */
4551 OO.ui.mixin.GroupWidget.prototype.setDisabled = function ( disabled ) {
4552 var i, len;
4553
4554 // Parent method
4555 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
4556 OO.ui.Widget.prototype.setDisabled.call( this, disabled );
4557
4558 // During construction, #setDisabled is called before the OO.ui.mixin.GroupElement constructor
4559 if ( this.items ) {
4560 for ( i = 0, len = this.items.length; i < len; i++ ) {
4561 this.items[ i ].updateDisabled();
4562 }
4563 }
4564
4565 return this;
4566 };
4567
4568 /**
4569 * Mixin for widgets used as items in widgets that mix in OO.ui.mixin.GroupWidget.
4570 *
4571 * Item widgets have a reference to a OO.ui.mixin.GroupWidget while they are attached to the group. This
4572 * allows bidirectional communication.
4573 *
4574 * Use together with OO.ui.mixin.GroupWidget to make disabled state inheritable.
4575 *
4576 * @private
4577 * @abstract
4578 * @class
4579 *
4580 * @constructor
4581 */
4582 OO.ui.mixin.ItemWidget = function OoUiMixinItemWidget() {
4583 //
4584 };
4585
4586 /* Methods */
4587
4588 /**
4589 * Check if widget is disabled.
4590 *
4591 * Checks parent if present, making disabled state inheritable.
4592 *
4593 * @return {boolean} Widget is disabled
4594 */
4595 OO.ui.mixin.ItemWidget.prototype.isDisabled = function () {
4596 return this.disabled ||
4597 ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() );
4598 };
4599
4600 /**
4601 * Set group element is in.
4602 *
4603 * @param {OO.ui.mixin.GroupElement|null} group Group element, null if none
4604 * @chainable
4605 */
4606 OO.ui.mixin.ItemWidget.prototype.setElementGroup = function ( group ) {
4607 // Parent method
4608 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
4609 OO.ui.Element.prototype.setElementGroup.call( this, group );
4610
4611 // Initialize item disabled states
4612 this.updateDisabled();
4613
4614 return this;
4615 };
4616
4617 /**
4618 * OptionWidgets are special elements that can be selected and configured with data. The
4619 * data is often unique for each option, but it does not have to be. OptionWidgets are used
4620 * with OO.ui.SelectWidget to create a selection of mutually exclusive options. For more information
4621 * and examples, please see the [OOjs UI documentation on MediaWiki][1].
4622 *
4623 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
4624 *
4625 * @class
4626 * @extends OO.ui.Widget
4627 * @mixins OO.ui.mixin.LabelElement
4628 * @mixins OO.ui.mixin.FlaggedElement
4629 *
4630 * @constructor
4631 * @param {Object} [config] Configuration options
4632 */
4633 OO.ui.OptionWidget = function OoUiOptionWidget( config ) {
4634 // Configuration initialization
4635 config = config || {};
4636
4637 // Parent constructor
4638 OO.ui.OptionWidget.parent.call( this, config );
4639
4640 // Mixin constructors
4641 OO.ui.mixin.ItemWidget.call( this );
4642 OO.ui.mixin.LabelElement.call( this, config );
4643 OO.ui.mixin.FlaggedElement.call( this, config );
4644
4645 // Properties
4646 this.selected = false;
4647 this.highlighted = false;
4648 this.pressed = false;
4649
4650 // Initialization
4651 this.$element
4652 .data( 'oo-ui-optionWidget', this )
4653 .attr( 'role', 'option' )
4654 .attr( 'aria-selected', 'false' )
4655 .addClass( 'oo-ui-optionWidget' )
4656 .append( this.$label );
4657 };
4658
4659 /* Setup */
4660
4661 OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
4662 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.ItemWidget );
4663 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.LabelElement );
4664 OO.mixinClass( OO.ui.OptionWidget, OO.ui.mixin.FlaggedElement );
4665
4666 /* Static Properties */
4667
4668 OO.ui.OptionWidget.static.selectable = true;
4669
4670 OO.ui.OptionWidget.static.highlightable = true;
4671
4672 OO.ui.OptionWidget.static.pressable = true;
4673
4674 OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
4675
4676 /* Methods */
4677
4678 /**
4679 * Check if the option can be selected.
4680 *
4681 * @return {boolean} Item is selectable
4682 */
4683 OO.ui.OptionWidget.prototype.isSelectable = function () {
4684 return this.constructor.static.selectable && !this.isDisabled() && this.isVisible();
4685 };
4686
4687 /**
4688 * Check if the option can be highlighted. A highlight indicates that the option
4689 * may be selected when a user presses enter or clicks. Disabled items cannot
4690 * be highlighted.
4691 *
4692 * @return {boolean} Item is highlightable
4693 */
4694 OO.ui.OptionWidget.prototype.isHighlightable = function () {
4695 return this.constructor.static.highlightable && !this.isDisabled() && this.isVisible();
4696 };
4697
4698 /**
4699 * Check if the option can be pressed. The pressed state occurs when a user mouses
4700 * down on an item, but has not yet let go of the mouse.
4701 *
4702 * @return {boolean} Item is pressable
4703 */
4704 OO.ui.OptionWidget.prototype.isPressable = function () {
4705 return this.constructor.static.pressable && !this.isDisabled() && this.isVisible();
4706 };
4707
4708 /**
4709 * Check if the option is selected.
4710 *
4711 * @return {boolean} Item is selected
4712 */
4713 OO.ui.OptionWidget.prototype.isSelected = function () {
4714 return this.selected;
4715 };
4716
4717 /**
4718 * Check if the option is highlighted. A highlight indicates that the
4719 * item may be selected when a user presses enter or clicks.
4720 *
4721 * @return {boolean} Item is highlighted
4722 */
4723 OO.ui.OptionWidget.prototype.isHighlighted = function () {
4724 return this.highlighted;
4725 };
4726
4727 /**
4728 * Check if the option is pressed. The pressed state occurs when a user mouses
4729 * down on an item, but has not yet let go of the mouse. The item may appear
4730 * selected, but it will not be selected until the user releases the mouse.
4731 *
4732 * @return {boolean} Item is pressed
4733 */
4734 OO.ui.OptionWidget.prototype.isPressed = function () {
4735 return this.pressed;
4736 };
4737
4738 /**
4739 * Set the option’s selected state. In general, all modifications to the selection
4740 * should be handled by the SelectWidget’s {@link OO.ui.SelectWidget#selectItem selectItem( [item] )}
4741 * method instead of this method.
4742 *
4743 * @param {boolean} [state=false] Select option
4744 * @chainable
4745 */
4746 OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
4747 if ( this.constructor.static.selectable ) {
4748 this.selected = !!state;
4749 this.$element
4750 .toggleClass( 'oo-ui-optionWidget-selected', state )
4751 .attr( 'aria-selected', state.toString() );
4752 if ( state && this.constructor.static.scrollIntoViewOnSelect ) {
4753 this.scrollElementIntoView();
4754 }
4755 this.updateThemeClasses();
4756 }
4757 return this;
4758 };
4759
4760 /**
4761 * Set the option’s highlighted state. In general, all programmatic
4762 * modifications to the highlight should be handled by the
4763 * SelectWidget’s {@link OO.ui.SelectWidget#highlightItem highlightItem( [item] )}
4764 * method instead of this method.
4765 *
4766 * @param {boolean} [state=false] Highlight option
4767 * @chainable
4768 */
4769 OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
4770 if ( this.constructor.static.highlightable ) {
4771 this.highlighted = !!state;
4772 this.$element.toggleClass( 'oo-ui-optionWidget-highlighted', state );
4773 this.updateThemeClasses();
4774 }
4775 return this;
4776 };
4777
4778 /**
4779 * Set the option’s pressed state. In general, all
4780 * programmatic modifications to the pressed state should be handled by the
4781 * SelectWidget’s {@link OO.ui.SelectWidget#pressItem pressItem( [item] )}
4782 * method instead of this method.
4783 *
4784 * @param {boolean} [state=false] Press option
4785 * @chainable
4786 */
4787 OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
4788 if ( this.constructor.static.pressable ) {
4789 this.pressed = !!state;
4790 this.$element.toggleClass( 'oo-ui-optionWidget-pressed', state );
4791 this.updateThemeClasses();
4792 }
4793 return this;
4794 };
4795
4796 /**
4797 * A SelectWidget is of a generic selection of options. The OOjs UI library contains several types of
4798 * select widgets, including {@link OO.ui.ButtonSelectWidget button selects},
4799 * {@link OO.ui.RadioSelectWidget radio selects}, and {@link OO.ui.MenuSelectWidget
4800 * menu selects}.
4801 *
4802 * This class should be used together with OO.ui.OptionWidget or OO.ui.DecoratedOptionWidget. For more
4803 * information, please see the [OOjs UI documentation on MediaWiki][1].
4804 *
4805 * @example
4806 * // Example of a select widget with three options
4807 * var select = new OO.ui.SelectWidget( {
4808 * items: [
4809 * new OO.ui.OptionWidget( {
4810 * data: 'a',
4811 * label: 'Option One',
4812 * } ),
4813 * new OO.ui.OptionWidget( {
4814 * data: 'b',
4815 * label: 'Option Two',
4816 * } ),
4817 * new OO.ui.OptionWidget( {
4818 * data: 'c',
4819 * label: 'Option Three',
4820 * } )
4821 * ]
4822 * } );
4823 * $( 'body' ).append( select.$element );
4824 *
4825 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
4826 *
4827 * @abstract
4828 * @class
4829 * @extends OO.ui.Widget
4830 * @mixins OO.ui.mixin.GroupWidget
4831 *
4832 * @constructor
4833 * @param {Object} [config] Configuration options
4834 * @cfg {OO.ui.OptionWidget[]} [items] An array of options to add to the select.
4835 * Options are created with {@link OO.ui.OptionWidget OptionWidget} classes. See
4836 * the [OOjs UI documentation on MediaWiki] [2] for examples.
4837 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
4838 */
4839 OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
4840 // Configuration initialization
4841 config = config || {};
4842
4843 // Parent constructor
4844 OO.ui.SelectWidget.parent.call( this, config );
4845
4846 // Mixin constructors
4847 OO.ui.mixin.GroupWidget.call( this, $.extend( {}, config, { $group: this.$element } ) );
4848
4849 // Properties
4850 this.pressed = false;
4851 this.selecting = null;
4852 this.onMouseUpHandler = this.onMouseUp.bind( this );
4853 this.onMouseMoveHandler = this.onMouseMove.bind( this );
4854 this.onKeyDownHandler = this.onKeyDown.bind( this );
4855 this.onKeyPressHandler = this.onKeyPress.bind( this );
4856 this.keyPressBuffer = '';
4857 this.keyPressBufferTimer = null;
4858
4859 // Events
4860 this.connect( this, {
4861 toggle: 'onToggle'
4862 } );
4863 this.$element.on( {
4864 mousedown: this.onMouseDown.bind( this ),
4865 mouseover: this.onMouseOver.bind( this ),
4866 mouseleave: this.onMouseLeave.bind( this )
4867 } );
4868
4869 // Initialization
4870 this.$element
4871 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
4872 .attr( 'role', 'listbox' );
4873 if ( Array.isArray( config.items ) ) {
4874 this.addItems( config.items );
4875 }
4876 };
4877
4878 /* Setup */
4879
4880 OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
4881
4882 // Need to mixin base class as well
4883 OO.mixinClass( OO.ui.SelectWidget, OO.ui.mixin.GroupElement );
4884 OO.mixinClass( OO.ui.SelectWidget, OO.ui.mixin.GroupWidget );
4885
4886 /* Static */
4887 OO.ui.SelectWidget.static.passAllFilter = function () {
4888 return true;
4889 };
4890
4891 /* Events */
4892
4893 /**
4894 * @event highlight
4895 *
4896 * A `highlight` event is emitted when the highlight is changed with the #highlightItem method.
4897 *
4898 * @param {OO.ui.OptionWidget|null} item Highlighted item
4899 */
4900
4901 /**
4902 * @event press
4903 *
4904 * A `press` event is emitted when the #pressItem method is used to programmatically modify the
4905 * pressed state of an option.
4906 *
4907 * @param {OO.ui.OptionWidget|null} item Pressed item
4908 */
4909
4910 /**
4911 * @event select
4912 *
4913 * A `select` event is emitted when the selection is modified programmatically with the #selectItem method.
4914 *
4915 * @param {OO.ui.OptionWidget|null} item Selected item
4916 */
4917
4918 /**
4919 * @event choose
4920 * A `choose` event is emitted when an item is chosen with the #chooseItem method.
4921 * @param {OO.ui.OptionWidget} item Chosen item
4922 */
4923
4924 /**
4925 * @event add
4926 *
4927 * An `add` event is emitted when options are added to the select with the #addItems method.
4928 *
4929 * @param {OO.ui.OptionWidget[]} items Added items
4930 * @param {number} index Index of insertion point
4931 */
4932
4933 /**
4934 * @event remove
4935 *
4936 * A `remove` event is emitted when options are removed from the select with the #clearItems
4937 * or #removeItems methods.
4938 *
4939 * @param {OO.ui.OptionWidget[]} items Removed items
4940 */
4941
4942 /* Methods */
4943
4944 /**
4945 * Handle mouse down events.
4946 *
4947 * @private
4948 * @param {jQuery.Event} e Mouse down event
4949 */
4950 OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
4951 var item;
4952
4953 if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
4954 this.togglePressed( true );
4955 item = this.getTargetItem( e );
4956 if ( item && item.isSelectable() ) {
4957 this.pressItem( item );
4958 this.selecting = item;
4959 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
4960 this.getElementDocument().addEventListener( 'mousemove', this.onMouseMoveHandler, true );
4961 }
4962 }
4963 return false;
4964 };
4965
4966 /**
4967 * Handle mouse up events.
4968 *
4969 * @private
4970 * @param {jQuery.Event} e Mouse up event
4971 */
4972 OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
4973 var item;
4974
4975 this.togglePressed( false );
4976 if ( !this.selecting ) {
4977 item = this.getTargetItem( e );
4978 if ( item && item.isSelectable() ) {
4979 this.selecting = item;
4980 }
4981 }
4982 if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT && this.selecting ) {
4983 this.pressItem( null );
4984 this.chooseItem( this.selecting );
4985 this.selecting = null;
4986 }
4987
4988 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
4989 this.getElementDocument().removeEventListener( 'mousemove', this.onMouseMoveHandler, true );
4990
4991 return false;
4992 };
4993
4994 /**
4995 * Handle mouse move events.
4996 *
4997 * @private
4998 * @param {jQuery.Event} e Mouse move event
4999 */
5000 OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
5001 var item;
5002
5003 if ( !this.isDisabled() && this.pressed ) {
5004 item = this.getTargetItem( e );
5005 if ( item && item !== this.selecting && item.isSelectable() ) {
5006 this.pressItem( item );
5007 this.selecting = item;
5008 }
5009 }
5010 return false;
5011 };
5012
5013 /**
5014 * Handle mouse over events.
5015 *
5016 * @private
5017 * @param {jQuery.Event} e Mouse over event
5018 */
5019 OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
5020 var item;
5021
5022 if ( !this.isDisabled() ) {
5023 item = this.getTargetItem( e );
5024 this.highlightItem( item && item.isHighlightable() ? item : null );
5025 }
5026 return false;
5027 };
5028
5029 /**
5030 * Handle mouse leave events.
5031 *
5032 * @private
5033 * @param {jQuery.Event} e Mouse over event
5034 */
5035 OO.ui.SelectWidget.prototype.onMouseLeave = function () {
5036 if ( !this.isDisabled() ) {
5037 this.highlightItem( null );
5038 }
5039 return false;
5040 };
5041
5042 /**
5043 * Handle key down events.
5044 *
5045 * @protected
5046 * @param {jQuery.Event} e Key down event
5047 */
5048 OO.ui.SelectWidget.prototype.onKeyDown = function ( e ) {
5049 var nextItem,
5050 handled = false,
5051 currentItem = this.getHighlightedItem() || this.getSelectedItem();
5052
5053 if ( !this.isDisabled() && this.isVisible() ) {
5054 switch ( e.keyCode ) {
5055 case OO.ui.Keys.ENTER:
5056 if ( currentItem && currentItem.constructor.static.highlightable ) {
5057 // Was only highlighted, now let's select it. No-op if already selected.
5058 this.chooseItem( currentItem );
5059 handled = true;
5060 }
5061 break;
5062 case OO.ui.Keys.UP:
5063 case OO.ui.Keys.LEFT:
5064 this.clearKeyPressBuffer();
5065 nextItem = this.getRelativeSelectableItem( currentItem, -1 );
5066 handled = true;
5067 break;
5068 case OO.ui.Keys.DOWN:
5069 case OO.ui.Keys.RIGHT:
5070 this.clearKeyPressBuffer();
5071 nextItem = this.getRelativeSelectableItem( currentItem, 1 );
5072 handled = true;
5073 break;
5074 case OO.ui.Keys.ESCAPE:
5075 case OO.ui.Keys.TAB:
5076 if ( currentItem && currentItem.constructor.static.highlightable ) {
5077 currentItem.setHighlighted( false );
5078 }
5079 this.unbindKeyDownListener();
5080 this.unbindKeyPressListener();
5081 // Don't prevent tabbing away / defocusing
5082 handled = false;
5083 break;
5084 }
5085
5086 if ( nextItem ) {
5087 if ( nextItem.constructor.static.highlightable ) {
5088 this.highlightItem( nextItem );
5089 } else {
5090 this.chooseItem( nextItem );
5091 }
5092 nextItem.scrollElementIntoView();
5093 }
5094
5095 if ( handled ) {
5096 // Can't just return false, because e is not always a jQuery event
5097 e.preventDefault();
5098 e.stopPropagation();
5099 }
5100 }
5101 };
5102
5103 /**
5104 * Bind key down listener.
5105 *
5106 * @protected
5107 */
5108 OO.ui.SelectWidget.prototype.bindKeyDownListener = function () {
5109 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
5110 };
5111
5112 /**
5113 * Unbind key down listener.
5114 *
5115 * @protected
5116 */
5117 OO.ui.SelectWidget.prototype.unbindKeyDownListener = function () {
5118 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
5119 };
5120
5121 /**
5122 * Clear the key-press buffer
5123 *
5124 * @protected
5125 */
5126 OO.ui.SelectWidget.prototype.clearKeyPressBuffer = function () {
5127 if ( this.keyPressBufferTimer ) {
5128 clearTimeout( this.keyPressBufferTimer );
5129 this.keyPressBufferTimer = null;
5130 }
5131 this.keyPressBuffer = '';
5132 };
5133
5134 /**
5135 * Handle key press events.
5136 *
5137 * @protected
5138 * @param {jQuery.Event} e Key press event
5139 */
5140 OO.ui.SelectWidget.prototype.onKeyPress = function ( e ) {
5141 var c, filter, item;
5142
5143 if ( !e.charCode ) {
5144 if ( e.keyCode === OO.ui.Keys.BACKSPACE && this.keyPressBuffer !== '' ) {
5145 this.keyPressBuffer = this.keyPressBuffer.substr( 0, this.keyPressBuffer.length - 1 );
5146 return false;
5147 }
5148 return;
5149 }
5150 if ( String.fromCodePoint ) {
5151 c = String.fromCodePoint( e.charCode );
5152 } else {
5153 c = String.fromCharCode( e.charCode );
5154 }
5155
5156 if ( this.keyPressBufferTimer ) {
5157 clearTimeout( this.keyPressBufferTimer );
5158 }
5159 this.keyPressBufferTimer = setTimeout( this.clearKeyPressBuffer.bind( this ), 1500 );
5160
5161 item = this.getHighlightedItem() || this.getSelectedItem();
5162
5163 if ( this.keyPressBuffer === c ) {
5164 // Common (if weird) special case: typing "xxxx" will cycle through all
5165 // the items beginning with "x".
5166 if ( item ) {
5167 item = this.getRelativeSelectableItem( item, 1 );
5168 }
5169 } else {
5170 this.keyPressBuffer += c;
5171 }
5172
5173 filter = this.getItemMatcher( this.keyPressBuffer, false );
5174 if ( !item || !filter( item ) ) {
5175 item = this.getRelativeSelectableItem( item, 1, filter );
5176 }
5177 if ( item ) {
5178 if ( item.constructor.static.highlightable ) {
5179 this.highlightItem( item );
5180 } else {
5181 this.chooseItem( item );
5182 }
5183 item.scrollElementIntoView();
5184 }
5185
5186 return false;
5187 };
5188
5189 /**
5190 * Get a matcher for the specific string
5191 *
5192 * @protected
5193 * @param {string} s String to match against items
5194 * @param {boolean} [exact=false] Only accept exact matches
5195 * @return {Function} function ( OO.ui.OptionItem ) => boolean
5196 */
5197 OO.ui.SelectWidget.prototype.getItemMatcher = function ( s, exact ) {
5198 var re;
5199
5200 if ( s.normalize ) {
5201 s = s.normalize();
5202 }
5203 s = exact ? s.trim() : s.replace( /^\s+/, '' );
5204 re = '^\\s*' + s.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' ).replace( /\s+/g, '\\s+' );
5205 if ( exact ) {
5206 re += '\\s*$';
5207 }
5208 re = new RegExp( re, 'i' );
5209 return function ( item ) {
5210 var l = item.getLabel();
5211 if ( typeof l !== 'string' ) {
5212 l = item.$label.text();
5213 }
5214 if ( l.normalize ) {
5215 l = l.normalize();
5216 }
5217 return re.test( l );
5218 };
5219 };
5220
5221 /**
5222 * Bind key press listener.
5223 *
5224 * @protected
5225 */
5226 OO.ui.SelectWidget.prototype.bindKeyPressListener = function () {
5227 this.getElementWindow().addEventListener( 'keypress', this.onKeyPressHandler, true );
5228 };
5229
5230 /**
5231 * Unbind key down listener.
5232 *
5233 * If you override this, be sure to call this.clearKeyPressBuffer() from your
5234 * implementation.
5235 *
5236 * @protected
5237 */
5238 OO.ui.SelectWidget.prototype.unbindKeyPressListener = function () {
5239 this.getElementWindow().removeEventListener( 'keypress', this.onKeyPressHandler, true );
5240 this.clearKeyPressBuffer();
5241 };
5242
5243 /**
5244 * Visibility change handler
5245 *
5246 * @protected
5247 * @param {boolean} visible
5248 */
5249 OO.ui.SelectWidget.prototype.onToggle = function ( visible ) {
5250 if ( !visible ) {
5251 this.clearKeyPressBuffer();
5252 }
5253 };
5254
5255 /**
5256 * Get the closest item to a jQuery.Event.
5257 *
5258 * @private
5259 * @param {jQuery.Event} e
5260 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
5261 */
5262 OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) {
5263 return $( e.target ).closest( '.oo-ui-optionWidget' ).data( 'oo-ui-optionWidget' ) || null;
5264 };
5265
5266 /**
5267 * Get selected item.
5268 *
5269 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
5270 */
5271 OO.ui.SelectWidget.prototype.getSelectedItem = function () {
5272 var i, len;
5273
5274 for ( i = 0, len = this.items.length; i < len; i++ ) {
5275 if ( this.items[ i ].isSelected() ) {
5276 return this.items[ i ];
5277 }
5278 }
5279 return null;
5280 };
5281
5282 /**
5283 * Get highlighted item.
5284 *
5285 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
5286 */
5287 OO.ui.SelectWidget.prototype.getHighlightedItem = function () {
5288 var i, len;
5289
5290 for ( i = 0, len = this.items.length; i < len; i++ ) {
5291 if ( this.items[ i ].isHighlighted() ) {
5292 return this.items[ i ];
5293 }
5294 }
5295 return null;
5296 };
5297
5298 /**
5299 * Toggle pressed state.
5300 *
5301 * Press is a state that occurs when a user mouses down on an item, but
5302 * has not yet let go of the mouse. The item may appear selected, but it will not be selected
5303 * until the user releases the mouse.
5304 *
5305 * @param {boolean} pressed An option is being pressed
5306 */
5307 OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) {
5308 if ( pressed === undefined ) {
5309 pressed = !this.pressed;
5310 }
5311 if ( pressed !== this.pressed ) {
5312 this.$element
5313 .toggleClass( 'oo-ui-selectWidget-pressed', pressed )
5314 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed );
5315 this.pressed = pressed;
5316 }
5317 };
5318
5319 /**
5320 * Highlight an option. If the `item` param is omitted, no options will be highlighted
5321 * and any existing highlight will be removed. The highlight is mutually exclusive.
5322 *
5323 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit for no highlight
5324 * @fires highlight
5325 * @chainable
5326 */
5327 OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
5328 var i, len, highlighted,
5329 changed = false;
5330
5331 for ( i = 0, len = this.items.length; i < len; i++ ) {
5332 highlighted = this.items[ i ] === item;
5333 if ( this.items[ i ].isHighlighted() !== highlighted ) {
5334 this.items[ i ].setHighlighted( highlighted );
5335 changed = true;
5336 }
5337 }
5338 if ( changed ) {
5339 this.emit( 'highlight', item );
5340 }
5341
5342 return this;
5343 };
5344
5345 /**
5346 * Fetch an item by its label.
5347 *
5348 * @param {string} label Label of the item to select.
5349 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
5350 * @return {OO.ui.Element|null} Item with equivalent label, `null` if none exists
5351 */
5352 OO.ui.SelectWidget.prototype.getItemFromLabel = function ( label, prefix ) {
5353 var i, item, found,
5354 len = this.items.length,
5355 filter = this.getItemMatcher( label, true );
5356
5357 for ( i = 0; i < len; i++ ) {
5358 item = this.items[ i ];
5359 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() && filter( item ) ) {
5360 return item;
5361 }
5362 }
5363
5364 if ( prefix ) {
5365 found = null;
5366 filter = this.getItemMatcher( label, false );
5367 for ( i = 0; i < len; i++ ) {
5368 item = this.items[ i ];
5369 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() && filter( item ) ) {
5370 if ( found ) {
5371 return null;
5372 }
5373 found = item;
5374 }
5375 }
5376 if ( found ) {
5377 return found;
5378 }
5379 }
5380
5381 return null;
5382 };
5383
5384 /**
5385 * Programmatically select an option by its label. If the item does not exist,
5386 * all options will be deselected.
5387 *
5388 * @param {string} [label] Label of the item to select.
5389 * @param {boolean} [prefix=false] Allow a prefix match, if only a single item matches
5390 * @fires select
5391 * @chainable
5392 */
5393 OO.ui.SelectWidget.prototype.selectItemByLabel = function ( label, prefix ) {
5394 var itemFromLabel = this.getItemFromLabel( label, !!prefix );
5395 if ( label === undefined || !itemFromLabel ) {
5396 return this.selectItem();
5397 }
5398 return this.selectItem( itemFromLabel );
5399 };
5400
5401 /**
5402 * Programmatically select an option by its data. If the `data` parameter is omitted,
5403 * or if the item does not exist, all options will be deselected.
5404 *
5405 * @param {Object|string} [data] Value of the item to select, omit to deselect all
5406 * @fires select
5407 * @chainable
5408 */
5409 OO.ui.SelectWidget.prototype.selectItemByData = function ( data ) {
5410 var itemFromData = this.getItemFromData( data );
5411 if ( data === undefined || !itemFromData ) {
5412 return this.selectItem();
5413 }
5414 return this.selectItem( itemFromData );
5415 };
5416
5417 /**
5418 * Programmatically select an option by its reference. If the `item` parameter is omitted,
5419 * all options will be deselected.
5420 *
5421 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
5422 * @fires select
5423 * @chainable
5424 */
5425 OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
5426 var i, len, selected,
5427 changed = false;
5428
5429 for ( i = 0, len = this.items.length; i < len; i++ ) {
5430 selected = this.items[ i ] === item;
5431 if ( this.items[ i ].isSelected() !== selected ) {
5432 this.items[ i ].setSelected( selected );
5433 changed = true;
5434 }
5435 }
5436 if ( changed ) {
5437 this.emit( 'select', item );
5438 }
5439
5440 return this;
5441 };
5442
5443 /**
5444 * Press an item.
5445 *
5446 * Press is a state that occurs when a user mouses down on an item, but has not
5447 * yet let go of the mouse. The item may appear selected, but it will not be selected until the user
5448 * releases the mouse.
5449 *
5450 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
5451 * @fires press
5452 * @chainable
5453 */
5454 OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
5455 var i, len, pressed,
5456 changed = false;
5457
5458 for ( i = 0, len = this.items.length; i < len; i++ ) {
5459 pressed = this.items[ i ] === item;
5460 if ( this.items[ i ].isPressed() !== pressed ) {
5461 this.items[ i ].setPressed( pressed );
5462 changed = true;
5463 }
5464 }
5465 if ( changed ) {
5466 this.emit( 'press', item );
5467 }
5468
5469 return this;
5470 };
5471
5472 /**
5473 * Choose an item.
5474 *
5475 * Note that ‘choose’ should never be modified programmatically. A user can choose
5476 * an option with the keyboard or mouse and it becomes selected. To select an item programmatically,
5477 * use the #selectItem method.
5478 *
5479 * This method is identical to #selectItem, but may vary in subclasses that take additional action
5480 * when users choose an item with the keyboard or mouse.
5481 *
5482 * @param {OO.ui.OptionWidget} item Item to choose
5483 * @fires choose
5484 * @chainable
5485 */
5486 OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
5487 if ( item ) {
5488 this.selectItem( item );
5489 this.emit( 'choose', item );
5490 }
5491
5492 return this;
5493 };
5494
5495 /**
5496 * Get an option by its position relative to the specified item (or to the start of the option array,
5497 * if item is `null`). The direction in which to search through the option array is specified with a
5498 * number: -1 for reverse (the default) or 1 for forward. The method will return an option, or
5499 * `null` if there are no options in the array.
5500 *
5501 * @param {OO.ui.OptionWidget|null} item Item to describe the start position, or `null` to start at the beginning of the array.
5502 * @param {number} direction Direction to move in: -1 to move backward, 1 to move forward
5503 * @param {Function} filter Only consider items for which this function returns
5504 * true. Function takes an OO.ui.OptionWidget and returns a boolean.
5505 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the select
5506 */
5507 OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction, filter ) {
5508 var currentIndex, nextIndex, i,
5509 increase = direction > 0 ? 1 : -1,
5510 len = this.items.length;
5511
5512 if ( !$.isFunction( filter ) ) {
5513 filter = OO.ui.SelectWidget.static.passAllFilter;
5514 }
5515
5516 if ( item instanceof OO.ui.OptionWidget ) {
5517 currentIndex = this.items.indexOf( item );
5518 nextIndex = ( currentIndex + increase + len ) % len;
5519 } else {
5520 // If no item is selected and moving forward, start at the beginning.
5521 // If moving backward, start at the end.
5522 nextIndex = direction > 0 ? 0 : len - 1;
5523 }
5524
5525 for ( i = 0; i < len; i++ ) {
5526 item = this.items[ nextIndex ];
5527 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() && filter( item ) ) {
5528 return item;
5529 }
5530 nextIndex = ( nextIndex + increase + len ) % len;
5531 }
5532 return null;
5533 };
5534
5535 /**
5536 * Get the next selectable item or `null` if there are no selectable items.
5537 * Disabled options and menu-section markers and breaks are not selectable.
5538 *
5539 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
5540 */
5541 OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
5542 var i, len, item;
5543
5544 for ( i = 0, len = this.items.length; i < len; i++ ) {
5545 item = this.items[ i ];
5546 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
5547 return item;
5548 }
5549 }
5550
5551 return null;
5552 };
5553
5554 /**
5555 * Add an array of options to the select. Optionally, an index number can be used to
5556 * specify an insertion point.
5557 *
5558 * @param {OO.ui.OptionWidget[]} items Items to add
5559 * @param {number} [index] Index to insert items after
5560 * @fires add
5561 * @chainable
5562 */
5563 OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
5564 // Mixin method
5565 OO.ui.mixin.GroupWidget.prototype.addItems.call( this, items, index );
5566
5567 // Always provide an index, even if it was omitted
5568 this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index );
5569
5570 return this;
5571 };
5572
5573 /**
5574 * Remove the specified array of options from the select. Options will be detached
5575 * from the DOM, not removed, so they can be reused later. To remove all options from
5576 * the select, you may wish to use the #clearItems method instead.
5577 *
5578 * @param {OO.ui.OptionWidget[]} items Items to remove
5579 * @fires remove
5580 * @chainable
5581 */
5582 OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
5583 var i, len, item;
5584
5585 // Deselect items being removed
5586 for ( i = 0, len = items.length; i < len; i++ ) {
5587 item = items[ i ];
5588 if ( item.isSelected() ) {
5589 this.selectItem( null );
5590 }
5591 }
5592
5593 // Mixin method
5594 OO.ui.mixin.GroupWidget.prototype.removeItems.call( this, items );
5595
5596 this.emit( 'remove', items );
5597
5598 return this;
5599 };
5600
5601 /**
5602 * Clear all options from the select. Options will be detached from the DOM, not removed,
5603 * so that they can be reused later. To remove a subset of options from the select, use
5604 * the #removeItems method.
5605 *
5606 * @fires remove
5607 * @chainable
5608 */
5609 OO.ui.SelectWidget.prototype.clearItems = function () {
5610 var items = this.items.slice();
5611
5612 // Mixin method
5613 OO.ui.mixin.GroupWidget.prototype.clearItems.call( this );
5614
5615 // Clear selection
5616 this.selectItem( null );
5617
5618 this.emit( 'remove', items );
5619
5620 return this;
5621 };
5622
5623 /**
5624 * DecoratedOptionWidgets are {@link OO.ui.OptionWidget options} that can be configured
5625 * with an {@link OO.ui.mixin.IconElement icon} and/or {@link OO.ui.mixin.IndicatorElement indicator}.
5626 * This class is used with OO.ui.SelectWidget to create a selection of mutually exclusive
5627 * options. For more information about options and selects, please see the
5628 * [OOjs UI documentation on MediaWiki][1].
5629 *
5630 * @example
5631 * // Decorated options in a select widget
5632 * var select = new OO.ui.SelectWidget( {
5633 * items: [
5634 * new OO.ui.DecoratedOptionWidget( {
5635 * data: 'a',
5636 * label: 'Option with icon',
5637 * icon: 'help'
5638 * } ),
5639 * new OO.ui.DecoratedOptionWidget( {
5640 * data: 'b',
5641 * label: 'Option with indicator',
5642 * indicator: 'next'
5643 * } )
5644 * ]
5645 * } );
5646 * $( 'body' ).append( select.$element );
5647 *
5648 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5649 *
5650 * @class
5651 * @extends OO.ui.OptionWidget
5652 * @mixins OO.ui.mixin.IconElement
5653 * @mixins OO.ui.mixin.IndicatorElement
5654 *
5655 * @constructor
5656 * @param {Object} [config] Configuration options
5657 */
5658 OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( config ) {
5659 // Parent constructor
5660 OO.ui.DecoratedOptionWidget.parent.call( this, config );
5661
5662 // Mixin constructors
5663 OO.ui.mixin.IconElement.call( this, config );
5664 OO.ui.mixin.IndicatorElement.call( this, config );
5665
5666 // Initialization
5667 this.$element
5668 .addClass( 'oo-ui-decoratedOptionWidget' )
5669 .prepend( this.$icon )
5670 .append( this.$indicator );
5671 };
5672
5673 /* Setup */
5674
5675 OO.inheritClass( OO.ui.DecoratedOptionWidget, OO.ui.OptionWidget );
5676 OO.mixinClass( OO.ui.DecoratedOptionWidget, OO.ui.mixin.IconElement );
5677 OO.mixinClass( OO.ui.DecoratedOptionWidget, OO.ui.mixin.IndicatorElement );
5678
5679 /**
5680 * MenuOptionWidget is an option widget that looks like a menu item. The class is used with
5681 * OO.ui.MenuSelectWidget to create a menu of mutually exclusive options. Please see
5682 * the [OOjs UI documentation on MediaWiki] [1] for more information.
5683 *
5684 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
5685 *
5686 * @class
5687 * @extends OO.ui.DecoratedOptionWidget
5688 *
5689 * @constructor
5690 * @param {Object} [config] Configuration options
5691 */
5692 OO.ui.MenuOptionWidget = function OoUiMenuOptionWidget( config ) {
5693 // Configuration initialization
5694 config = $.extend( { icon: 'check' }, config );
5695
5696 // Parent constructor
5697 OO.ui.MenuOptionWidget.parent.call( this, config );
5698
5699 // Initialization
5700 this.$element
5701 .attr( 'role', 'menuitem' )
5702 .addClass( 'oo-ui-menuOptionWidget' );
5703 };
5704
5705 /* Setup */
5706
5707 OO.inheritClass( OO.ui.MenuOptionWidget, OO.ui.DecoratedOptionWidget );
5708
5709 /* Static Properties */
5710
5711 OO.ui.MenuOptionWidget.static.scrollIntoViewOnSelect = true;
5712
5713 /**
5714 * MenuSectionOptionWidgets are used inside {@link OO.ui.MenuSelectWidget menu select widgets} to group one or more related
5715 * {@link OO.ui.MenuOptionWidget menu options}. MenuSectionOptionWidgets cannot be highlighted or selected.
5716 *
5717 * @example
5718 * var myDropdown = new OO.ui.DropdownWidget( {
5719 * menu: {
5720 * items: [
5721 * new OO.ui.MenuSectionOptionWidget( {
5722 * label: 'Dogs'
5723 * } ),
5724 * new OO.ui.MenuOptionWidget( {
5725 * data: 'corgi',
5726 * label: 'Welsh Corgi'
5727 * } ),
5728 * new OO.ui.MenuOptionWidget( {
5729 * data: 'poodle',
5730 * label: 'Standard Poodle'
5731 * } ),
5732 * new OO.ui.MenuSectionOptionWidget( {
5733 * label: 'Cats'
5734 * } ),
5735 * new OO.ui.MenuOptionWidget( {
5736 * data: 'lion',
5737 * label: 'Lion'
5738 * } )
5739 * ]
5740 * }
5741 * } );
5742 * $( 'body' ).append( myDropdown.$element );
5743 *
5744 * @class
5745 * @extends OO.ui.DecoratedOptionWidget
5746 *
5747 * @constructor
5748 * @param {Object} [config] Configuration options
5749 */
5750 OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( config ) {
5751 // Parent constructor
5752 OO.ui.MenuSectionOptionWidget.parent.call( this, config );
5753
5754 // Initialization
5755 this.$element.addClass( 'oo-ui-menuSectionOptionWidget' );
5756 };
5757
5758 /* Setup */
5759
5760 OO.inheritClass( OO.ui.MenuSectionOptionWidget, OO.ui.DecoratedOptionWidget );
5761
5762 /* Static Properties */
5763
5764 OO.ui.MenuSectionOptionWidget.static.selectable = false;
5765
5766 OO.ui.MenuSectionOptionWidget.static.highlightable = false;
5767
5768 /**
5769 * MenuSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains options and
5770 * is used together with OO.ui.MenuOptionWidget. It is designed be used as part of another widget.
5771 * See {@link OO.ui.DropdownWidget DropdownWidget}, {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget},
5772 * and {@link OO.ui.mixin.LookupElement LookupElement} for examples of widgets that contain menus.
5773 * MenuSelectWidgets themselves are not instantiated directly, rather subclassed
5774 * and customized to be opened, closed, and displayed as needed.
5775 *
5776 * By default, menus are clipped to the visible viewport and are not visible when a user presses the
5777 * mouse outside the menu.
5778 *
5779 * Menus also have support for keyboard interaction:
5780 *
5781 * - Enter/Return key: choose and select a menu option
5782 * - Up-arrow key: highlight the previous menu option
5783 * - Down-arrow key: highlight the next menu option
5784 * - Esc key: hide the menu
5785 *
5786 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
5787 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
5788 *
5789 * @class
5790 * @extends OO.ui.SelectWidget
5791 * @mixins OO.ui.mixin.ClippableElement
5792 *
5793 * @constructor
5794 * @param {Object} [config] Configuration options
5795 * @cfg {OO.ui.TextInputWidget} [input] Text input used to implement option highlighting for menu items that match
5796 * the text the user types. This config is used by {@link OO.ui.ComboBoxInputWidget ComboBoxInputWidget}
5797 * and {@link OO.ui.mixin.LookupElement LookupElement}
5798 * @cfg {jQuery} [$input] Text input used to implement option highlighting for menu items that match
5799 * the text the user types. This config is used by {@link OO.ui.CapsuleMultiSelectWidget CapsuleMultiSelectWidget}
5800 * @cfg {OO.ui.Widget} [widget] Widget associated with the menu's active state. If the user clicks the mouse
5801 * anywhere on the page outside of this widget, the menu is hidden. For example, if there is a button
5802 * that toggles the menu's visibility on click, the menu will be hidden then re-shown when the user clicks
5803 * that button, unless the button (or its parent widget) is passed in here.
5804 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu.
5805 * @cfg {boolean} [filterFromInput=false] Filter the displayed options from the input
5806 */
5807 OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) {
5808 // Configuration initialization
5809 config = config || {};
5810
5811 // Parent constructor
5812 OO.ui.MenuSelectWidget.parent.call( this, config );
5813
5814 // Mixin constructors
5815 OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
5816
5817 // Properties
5818 this.newItems = null;
5819 this.autoHide = config.autoHide === undefined || !!config.autoHide;
5820 this.filterFromInput = !!config.filterFromInput;
5821 this.$input = config.$input ? config.$input : config.input ? config.input.$input : null;
5822 this.$widget = config.widget ? config.widget.$element : null;
5823 this.onDocumentMouseDownHandler = this.onDocumentMouseDown.bind( this );
5824 this.onInputEditHandler = OO.ui.debounce( this.updateItemVisibility.bind( this ), 100 );
5825
5826 // Initialization
5827 this.$element
5828 .addClass( 'oo-ui-menuSelectWidget' )
5829 .attr( 'role', 'menu' );
5830
5831 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
5832 // that reference properties not initialized at that time of parent class construction
5833 // TODO: Find a better way to handle post-constructor setup
5834 this.visible = false;
5835 this.$element.addClass( 'oo-ui-element-hidden' );
5836 };
5837
5838 /* Setup */
5839
5840 OO.inheritClass( OO.ui.MenuSelectWidget, OO.ui.SelectWidget );
5841 OO.mixinClass( OO.ui.MenuSelectWidget, OO.ui.mixin.ClippableElement );
5842
5843 /* Methods */
5844
5845 /**
5846 * Handles document mouse down events.
5847 *
5848 * @protected
5849 * @param {jQuery.Event} e Key down event
5850 */
5851 OO.ui.MenuSelectWidget.prototype.onDocumentMouseDown = function ( e ) {
5852 if (
5853 !OO.ui.contains( this.$element[ 0 ], e.target, true ) &&
5854 ( !this.$widget || !OO.ui.contains( this.$widget[ 0 ], e.target, true ) )
5855 ) {
5856 this.toggle( false );
5857 }
5858 };
5859
5860 /**
5861 * @inheritdoc
5862 */
5863 OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) {
5864 var currentItem = this.getHighlightedItem() || this.getSelectedItem();
5865
5866 if ( !this.isDisabled() && this.isVisible() ) {
5867 switch ( e.keyCode ) {
5868 case OO.ui.Keys.LEFT:
5869 case OO.ui.Keys.RIGHT:
5870 // Do nothing if a text field is associated, arrow keys will be handled natively
5871 if ( !this.$input ) {
5872 OO.ui.MenuSelectWidget.parent.prototype.onKeyDown.call( this, e );
5873 }
5874 break;
5875 case OO.ui.Keys.ESCAPE:
5876 case OO.ui.Keys.TAB:
5877 if ( currentItem ) {
5878 currentItem.setHighlighted( false );
5879 }
5880 this.toggle( false );
5881 // Don't prevent tabbing away, prevent defocusing
5882 if ( e.keyCode === OO.ui.Keys.ESCAPE ) {
5883 e.preventDefault();
5884 e.stopPropagation();
5885 }
5886 break;
5887 default:
5888 OO.ui.MenuSelectWidget.parent.prototype.onKeyDown.call( this, e );
5889 return;
5890 }
5891 }
5892 };
5893
5894 /**
5895 * Update menu item visibility after input changes.
5896 * @protected
5897 */
5898 OO.ui.MenuSelectWidget.prototype.updateItemVisibility = function () {
5899 var i, item,
5900 len = this.items.length,
5901 showAll = !this.isVisible(),
5902 filter = showAll ? null : this.getItemMatcher( this.$input.val() );
5903
5904 for ( i = 0; i < len; i++ ) {
5905 item = this.items[ i ];
5906 if ( item instanceof OO.ui.OptionWidget ) {
5907 item.toggle( showAll || filter( item ) );
5908 }
5909 }
5910
5911 // Reevaluate clipping
5912 this.clip();
5913 };
5914
5915 /**
5916 * @inheritdoc
5917 */
5918 OO.ui.MenuSelectWidget.prototype.bindKeyDownListener = function () {
5919 if ( this.$input ) {
5920 this.$input.on( 'keydown', this.onKeyDownHandler );
5921 } else {
5922 OO.ui.MenuSelectWidget.parent.prototype.bindKeyDownListener.call( this );
5923 }
5924 };
5925
5926 /**
5927 * @inheritdoc
5928 */
5929 OO.ui.MenuSelectWidget.prototype.unbindKeyDownListener = function () {
5930 if ( this.$input ) {
5931 this.$input.off( 'keydown', this.onKeyDownHandler );
5932 } else {
5933 OO.ui.MenuSelectWidget.parent.prototype.unbindKeyDownListener.call( this );
5934 }
5935 };
5936
5937 /**
5938 * @inheritdoc
5939 */
5940 OO.ui.MenuSelectWidget.prototype.bindKeyPressListener = function () {
5941 if ( this.$input ) {
5942 if ( this.filterFromInput ) {
5943 this.$input.on( 'keydown mouseup cut paste change input select', this.onInputEditHandler );
5944 }
5945 } else {
5946 OO.ui.MenuSelectWidget.parent.prototype.bindKeyPressListener.call( this );
5947 }
5948 };
5949
5950 /**
5951 * @inheritdoc
5952 */
5953 OO.ui.MenuSelectWidget.prototype.unbindKeyPressListener = function () {
5954 if ( this.$input ) {
5955 if ( this.filterFromInput ) {
5956 this.$input.off( 'keydown mouseup cut paste change input select', this.onInputEditHandler );
5957 this.updateItemVisibility();
5958 }
5959 } else {
5960 OO.ui.MenuSelectWidget.parent.prototype.unbindKeyPressListener.call( this );
5961 }
5962 };
5963
5964 /**
5965 * Choose an item.
5966 *
5967 * When a user chooses an item, the menu is closed.
5968 *
5969 * Note that ‘choose’ should never be modified programmatically. A user can choose an option with the keyboard
5970 * or mouse and it becomes selected. To select an item programmatically, use the #selectItem method.
5971 * @param {OO.ui.OptionWidget} item Item to choose
5972 * @chainable
5973 */
5974 OO.ui.MenuSelectWidget.prototype.chooseItem = function ( item ) {
5975 OO.ui.MenuSelectWidget.parent.prototype.chooseItem.call( this, item );
5976 this.toggle( false );
5977 return this;
5978 };
5979
5980 /**
5981 * @inheritdoc
5982 */
5983 OO.ui.MenuSelectWidget.prototype.addItems = function ( items, index ) {
5984 var i, len, item;
5985
5986 // Parent method
5987 OO.ui.MenuSelectWidget.parent.prototype.addItems.call( this, items, index );
5988
5989 // Auto-initialize
5990 if ( !this.newItems ) {
5991 this.newItems = [];
5992 }
5993
5994 for ( i = 0, len = items.length; i < len; i++ ) {
5995 item = items[ i ];
5996 if ( this.isVisible() ) {
5997 // Defer fitting label until item has been attached
5998 item.fitLabel();
5999 } else {
6000 this.newItems.push( item );
6001 }
6002 }
6003
6004 // Reevaluate clipping
6005 this.clip();
6006
6007 return this;
6008 };
6009
6010 /**
6011 * @inheritdoc
6012 */
6013 OO.ui.MenuSelectWidget.prototype.removeItems = function ( items ) {
6014 // Parent method
6015 OO.ui.MenuSelectWidget.parent.prototype.removeItems.call( this, items );
6016
6017 // Reevaluate clipping
6018 this.clip();
6019
6020 return this;
6021 };
6022
6023 /**
6024 * @inheritdoc
6025 */
6026 OO.ui.MenuSelectWidget.prototype.clearItems = function () {
6027 // Parent method
6028 OO.ui.MenuSelectWidget.parent.prototype.clearItems.call( this );
6029
6030 // Reevaluate clipping
6031 this.clip();
6032
6033 return this;
6034 };
6035
6036 /**
6037 * @inheritdoc
6038 */
6039 OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
6040 var i, len, change;
6041
6042 visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
6043 change = visible !== this.isVisible();
6044
6045 // Parent method
6046 OO.ui.MenuSelectWidget.parent.prototype.toggle.call( this, visible );
6047
6048 if ( change ) {
6049 if ( visible ) {
6050 this.bindKeyDownListener();
6051 this.bindKeyPressListener();
6052
6053 if ( this.newItems && this.newItems.length ) {
6054 for ( i = 0, len = this.newItems.length; i < len; i++ ) {
6055 this.newItems[ i ].fitLabel();
6056 }
6057 this.newItems = null;
6058 }
6059 this.toggleClipping( true );
6060
6061 // Auto-hide
6062 if ( this.autoHide ) {
6063 this.getElementDocument().addEventListener( 'mousedown', this.onDocumentMouseDownHandler, true );
6064 }
6065 } else {
6066 this.unbindKeyDownListener();
6067 this.unbindKeyPressListener();
6068 this.getElementDocument().removeEventListener( 'mousedown', this.onDocumentMouseDownHandler, true );
6069 this.toggleClipping( false );
6070 }
6071 }
6072
6073 return this;
6074 };
6075
6076 /**
6077 * DropdownWidgets are not menus themselves, rather they contain a menu of options created with
6078 * OO.ui.MenuOptionWidget. The DropdownWidget takes care of opening and displaying the menu so that
6079 * users can interact with it.
6080 *
6081 * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
6082 * OO.ui.DropdownInputWidget instead.
6083 *
6084 * @example
6085 * // Example: A DropdownWidget with a menu that contains three options
6086 * var dropDown = new OO.ui.DropdownWidget( {
6087 * label: 'Dropdown menu: Select a menu option',
6088 * menu: {
6089 * items: [
6090 * new OO.ui.MenuOptionWidget( {
6091 * data: 'a',
6092 * label: 'First'
6093 * } ),
6094 * new OO.ui.MenuOptionWidget( {
6095 * data: 'b',
6096 * label: 'Second'
6097 * } ),
6098 * new OO.ui.MenuOptionWidget( {
6099 * data: 'c',
6100 * label: 'Third'
6101 * } )
6102 * ]
6103 * }
6104 * } );
6105 *
6106 * $( 'body' ).append( dropDown.$element );
6107 *
6108 * dropDown.getMenu().selectItemByData( 'b' );
6109 *
6110 * dropDown.getMenu().getSelectedItem().getData(); // returns 'b'
6111 *
6112 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
6113 *
6114 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
6115 *
6116 * @class
6117 * @extends OO.ui.Widget
6118 * @mixins OO.ui.mixin.IconElement
6119 * @mixins OO.ui.mixin.IndicatorElement
6120 * @mixins OO.ui.mixin.LabelElement
6121 * @mixins OO.ui.mixin.TitledElement
6122 * @mixins OO.ui.mixin.TabIndexedElement
6123 *
6124 * @constructor
6125 * @param {Object} [config] Configuration options
6126 * @cfg {Object} [menu] Configuration options to pass to {@link OO.ui.FloatingMenuSelectWidget menu select widget}
6127 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
6128 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
6129 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
6130 */
6131 OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
6132 // Configuration initialization
6133 config = $.extend( { indicator: 'down' }, config );
6134
6135 // Parent constructor
6136 OO.ui.DropdownWidget.parent.call( this, config );
6137
6138 // Properties (must be set before TabIndexedElement constructor call)
6139 this.$handle = this.$( '<span>' );
6140 this.$overlay = config.$overlay || this.$element;
6141
6142 // Mixin constructors
6143 OO.ui.mixin.IconElement.call( this, config );
6144 OO.ui.mixin.IndicatorElement.call( this, config );
6145 OO.ui.mixin.LabelElement.call( this, config );
6146 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
6147 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
6148
6149 // Properties
6150 this.menu = new OO.ui.FloatingMenuSelectWidget( $.extend( {
6151 widget: this,
6152 $container: this.$element
6153 }, config.menu ) );
6154
6155 // Events
6156 this.$handle.on( {
6157 click: this.onClick.bind( this ),
6158 keypress: this.onKeyPress.bind( this )
6159 } );
6160 this.menu.connect( this, { select: 'onMenuSelect' } );
6161
6162 // Initialization
6163 this.$handle
6164 .addClass( 'oo-ui-dropdownWidget-handle' )
6165 .append( this.$icon, this.$label, this.$indicator );
6166 this.$element
6167 .addClass( 'oo-ui-dropdownWidget' )
6168 .append( this.$handle );
6169 this.$overlay.append( this.menu.$element );
6170 };
6171
6172 /* Setup */
6173
6174 OO.inheritClass( OO.ui.DropdownWidget, OO.ui.Widget );
6175 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.IconElement );
6176 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.IndicatorElement );
6177 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.LabelElement );
6178 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.TitledElement );
6179 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.mixin.TabIndexedElement );
6180
6181 /* Methods */
6182
6183 /**
6184 * Get the menu.
6185 *
6186 * @return {OO.ui.MenuSelectWidget} Menu of widget
6187 */
6188 OO.ui.DropdownWidget.prototype.getMenu = function () {
6189 return this.menu;
6190 };
6191
6192 /**
6193 * Handles menu select events.
6194 *
6195 * @private
6196 * @param {OO.ui.MenuOptionWidget} item Selected menu item
6197 */
6198 OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
6199 var selectedLabel;
6200
6201 if ( !item ) {
6202 this.setLabel( null );
6203 return;
6204 }
6205
6206 selectedLabel = item.getLabel();
6207
6208 // If the label is a DOM element, clone it, because setLabel will append() it
6209 if ( selectedLabel instanceof jQuery ) {
6210 selectedLabel = selectedLabel.clone();
6211 }
6212
6213 this.setLabel( selectedLabel );
6214 };
6215
6216 /**
6217 * Handle mouse click events.
6218 *
6219 * @private
6220 * @param {jQuery.Event} e Mouse click event
6221 */
6222 OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
6223 if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
6224 this.menu.toggle();
6225 }
6226 return false;
6227 };
6228
6229 /**
6230 * Handle key press events.
6231 *
6232 * @private
6233 * @param {jQuery.Event} e Key press event
6234 */
6235 OO.ui.DropdownWidget.prototype.onKeyPress = function ( e ) {
6236 if ( !this.isDisabled() &&
6237 ( ( e.which === OO.ui.Keys.SPACE && !this.menu.isVisible() ) || e.which === OO.ui.Keys.ENTER )
6238 ) {
6239 this.menu.toggle();
6240 return false;
6241 }
6242 };
6243
6244 /**
6245 * RadioOptionWidget is an option widget that looks like a radio button.
6246 * The class is used with OO.ui.RadioSelectWidget to create a selection of radio options.
6247 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information.
6248 *
6249 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_option
6250 *
6251 * @class
6252 * @extends OO.ui.OptionWidget
6253 *
6254 * @constructor
6255 * @param {Object} [config] Configuration options
6256 */
6257 OO.ui.RadioOptionWidget = function OoUiRadioOptionWidget( config ) {
6258 // Configuration initialization
6259 config = config || {};
6260
6261 // Properties (must be done before parent constructor which calls #setDisabled)
6262 this.radio = new OO.ui.RadioInputWidget( { value: config.data, tabIndex: -1 } );
6263
6264 // Parent constructor
6265 OO.ui.RadioOptionWidget.parent.call( this, config );
6266
6267 // Events
6268 this.radio.$input.on( 'focus', this.onInputFocus.bind( this ) );
6269
6270 // Initialization
6271 // Remove implicit role, we're handling it ourselves
6272 this.radio.$input.attr( 'role', 'presentation' );
6273 this.$element
6274 .addClass( 'oo-ui-radioOptionWidget' )
6275 .attr( 'role', 'radio' )
6276 .attr( 'aria-checked', 'false' )
6277 .removeAttr( 'aria-selected' )
6278 .prepend( this.radio.$element );
6279 };
6280
6281 /* Setup */
6282
6283 OO.inheritClass( OO.ui.RadioOptionWidget, OO.ui.OptionWidget );
6284
6285 /* Static Properties */
6286
6287 OO.ui.RadioOptionWidget.static.highlightable = false;
6288
6289 OO.ui.RadioOptionWidget.static.scrollIntoViewOnSelect = true;
6290
6291 OO.ui.RadioOptionWidget.static.pressable = false;
6292
6293 OO.ui.RadioOptionWidget.static.tagName = 'label';
6294
6295 /* Methods */
6296
6297 /**
6298 * @param {jQuery.Event} e Focus event
6299 * @private
6300 */
6301 OO.ui.RadioOptionWidget.prototype.onInputFocus = function () {
6302 this.radio.$input.blur();
6303 this.$element.parent().focus();
6304 };
6305
6306 /**
6307 * @inheritdoc
6308 */
6309 OO.ui.RadioOptionWidget.prototype.setSelected = function ( state ) {
6310 OO.ui.RadioOptionWidget.parent.prototype.setSelected.call( this, state );
6311
6312 this.radio.setSelected( state );
6313 this.$element
6314 .attr( 'aria-checked', state.toString() )
6315 .removeAttr( 'aria-selected' );
6316
6317 return this;
6318 };
6319
6320 /**
6321 * @inheritdoc
6322 */
6323 OO.ui.RadioOptionWidget.prototype.setDisabled = function ( disabled ) {
6324 OO.ui.RadioOptionWidget.parent.prototype.setDisabled.call( this, disabled );
6325
6326 this.radio.setDisabled( this.isDisabled() );
6327
6328 return this;
6329 };
6330
6331 /**
6332 * RadioSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains radio
6333 * options and is used together with OO.ui.RadioOptionWidget. The RadioSelectWidget provides
6334 * an interface for adding, removing and selecting options.
6335 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
6336 *
6337 * If you want to use this within a HTML form, such as a OO.ui.FormLayout, use
6338 * OO.ui.RadioSelectInputWidget instead.
6339 *
6340 * @example
6341 * // A RadioSelectWidget with RadioOptions.
6342 * var option1 = new OO.ui.RadioOptionWidget( {
6343 * data: 'a',
6344 * label: 'Selected radio option'
6345 * } );
6346 *
6347 * var option2 = new OO.ui.RadioOptionWidget( {
6348 * data: 'b',
6349 * label: 'Unselected radio option'
6350 * } );
6351 *
6352 * var radioSelect=new OO.ui.RadioSelectWidget( {
6353 * items: [ option1, option2 ]
6354 * } );
6355 *
6356 * // Select 'option 1' using the RadioSelectWidget's selectItem() method.
6357 * radioSelect.selectItem( option1 );
6358 *
6359 * $( 'body' ).append( radioSelect.$element );
6360 *
6361 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
6362
6363 *
6364 * @class
6365 * @extends OO.ui.SelectWidget
6366 * @mixins OO.ui.mixin.TabIndexedElement
6367 *
6368 * @constructor
6369 * @param {Object} [config] Configuration options
6370 */
6371 OO.ui.RadioSelectWidget = function OoUiRadioSelectWidget( config ) {
6372 // Parent constructor
6373 OO.ui.RadioSelectWidget.parent.call( this, config );
6374
6375 // Mixin constructors
6376 OO.ui.mixin.TabIndexedElement.call( this, config );
6377
6378 // Events
6379 this.$element.on( {
6380 focus: this.bindKeyDownListener.bind( this ),
6381 blur: this.unbindKeyDownListener.bind( this )
6382 } );
6383
6384 // Initialization
6385 this.$element
6386 .addClass( 'oo-ui-radioSelectWidget' )
6387 .attr( 'role', 'radiogroup' );
6388 };
6389
6390 /* Setup */
6391
6392 OO.inheritClass( OO.ui.RadioSelectWidget, OO.ui.SelectWidget );
6393 OO.mixinClass( OO.ui.RadioSelectWidget, OO.ui.mixin.TabIndexedElement );
6394
6395 /**
6396 * Element that will stick under a specified container, even when it is inserted elsewhere in the
6397 * document (for example, in a OO.ui.Window's $overlay).
6398 *
6399 * The elements's position is automatically calculated and maintained when window is resized or the
6400 * page is scrolled. If you reposition the container manually, you have to call #position to make
6401 * sure the element is still placed correctly.
6402 *
6403 * As positioning is only possible when both the element and the container are attached to the DOM
6404 * and visible, it's only done after you call #togglePositioning. You might want to do this inside
6405 * the #toggle method to display a floating popup, for example.
6406 *
6407 * @abstract
6408 * @class
6409 *
6410 * @constructor
6411 * @param {Object} [config] Configuration options
6412 * @cfg {jQuery} [$floatable] Node to position, assigned to #$floatable, omit to use #$element
6413 * @cfg {jQuery} [$floatableContainer] Node to position below
6414 */
6415 OO.ui.mixin.FloatableElement = function OoUiMixinFloatableElement( config ) {
6416 // Configuration initialization
6417 config = config || {};
6418
6419 // Properties
6420 this.$floatable = null;
6421 this.$floatableContainer = null;
6422 this.$floatableWindow = null;
6423 this.$floatableClosestScrollable = null;
6424 this.onFloatableScrollHandler = this.position.bind( this );
6425 this.onFloatableWindowResizeHandler = this.position.bind( this );
6426
6427 // Initialization
6428 this.setFloatableContainer( config.$floatableContainer );
6429 this.setFloatableElement( config.$floatable || this.$element );
6430 };
6431
6432 /* Methods */
6433
6434 /**
6435 * Set floatable element.
6436 *
6437 * If an element is already set, it will be cleaned up before setting up the new element.
6438 *
6439 * @param {jQuery} $floatable Element to make floatable
6440 */
6441 OO.ui.mixin.FloatableElement.prototype.setFloatableElement = function ( $floatable ) {
6442 if ( this.$floatable ) {
6443 this.$floatable.removeClass( 'oo-ui-floatableElement-floatable' );
6444 this.$floatable.css( { left: '', top: '' } );
6445 }
6446
6447 this.$floatable = $floatable.addClass( 'oo-ui-floatableElement-floatable' );
6448 this.position();
6449 };
6450
6451 /**
6452 * Set floatable container.
6453 *
6454 * The element will be always positioned under the specified container.
6455 *
6456 * @param {jQuery|null} $floatableContainer Container to keep visible, or null to unset
6457 */
6458 OO.ui.mixin.FloatableElement.prototype.setFloatableContainer = function ( $floatableContainer ) {
6459 this.$floatableContainer = $floatableContainer;
6460 if ( this.$floatable ) {
6461 this.position();
6462 }
6463 };
6464
6465 /**
6466 * Toggle positioning.
6467 *
6468 * Do not turn positioning on until after the element is attached to the DOM and visible.
6469 *
6470 * @param {boolean} [positioning] Enable positioning, omit to toggle
6471 * @chainable
6472 */
6473 OO.ui.mixin.FloatableElement.prototype.togglePositioning = function ( positioning ) {
6474 var closestScrollableOfContainer, closestScrollableOfFloatable;
6475
6476 positioning = positioning === undefined ? !this.positioning : !!positioning;
6477
6478 if ( this.positioning !== positioning ) {
6479 this.positioning = positioning;
6480
6481 closestScrollableOfContainer = OO.ui.Element.static.getClosestScrollableContainer( this.$floatableContainer[ 0 ] );
6482 closestScrollableOfFloatable = OO.ui.Element.static.getClosestScrollableContainer( this.$floatable[ 0 ] );
6483 if ( closestScrollableOfContainer !== closestScrollableOfFloatable ) {
6484 // If the scrollable is the root, we have to listen to scroll events
6485 // on the window because of browser inconsistencies (or do we? someone should verify this)
6486 if ( $( closestScrollableOfContainer ).is( 'html, body' ) ) {
6487 closestScrollableOfContainer = OO.ui.Element.static.getWindow( closestScrollableOfContainer );
6488 }
6489 }
6490
6491 if ( positioning ) {
6492 this.$floatableWindow = $( this.getElementWindow() );
6493 this.$floatableWindow.on( 'resize', this.onFloatableWindowResizeHandler );
6494
6495 if ( closestScrollableOfContainer !== closestScrollableOfFloatable ) {
6496 this.$floatableClosestScrollable = $( closestScrollableOfContainer );
6497 this.$floatableClosestScrollable.on( 'scroll', this.onFloatableScrollHandler );
6498 }
6499
6500 // Initial position after visible
6501 this.position();
6502 } else {
6503 if ( this.$floatableWindow ) {
6504 this.$floatableWindow.off( 'resize', this.onFloatableWindowResizeHandler );
6505 this.$floatableWindow = null;
6506 }
6507
6508 if ( this.$floatableClosestScrollable ) {
6509 this.$floatableClosestScrollable.off( 'scroll', this.onFloatableScrollHandler );
6510 this.$floatableClosestScrollable = null;
6511 }
6512
6513 this.$floatable.css( { left: '', top: '' } );
6514 }
6515 }
6516
6517 return this;
6518 };
6519
6520 /**
6521 * Position the floatable below its container.
6522 *
6523 * This should only be done when both of them are attached to the DOM and visible.
6524 *
6525 * @chainable
6526 */
6527 OO.ui.mixin.FloatableElement.prototype.position = function () {
6528 var pos;
6529
6530 if ( !this.positioning ) {
6531 return this;
6532 }
6533
6534 pos = OO.ui.Element.static.getRelativePosition( this.$floatableContainer, this.$floatable.offsetParent() );
6535
6536 // Position under container
6537 pos.top += this.$floatableContainer.height();
6538 this.$floatable.css( pos );
6539
6540 // We updated the position, so re-evaluate the clipping state.
6541 // (ClippableElement does not listen to 'scroll' events on $floatableContainer's parent, and so
6542 // will not notice the need to update itself.)
6543 // TODO: This is terrible, we shouldn't need to know about ClippableElement at all here. Why does
6544 // it not listen to the right events in the right places?
6545 if ( this.clip ) {
6546 this.clip();
6547 }
6548
6549 return this;
6550 };
6551
6552 /**
6553 * FloatingMenuSelectWidget is a menu that will stick under a specified
6554 * container, even when it is inserted elsewhere in the document (for example,
6555 * in a OO.ui.Window's $overlay). This is sometimes necessary to prevent the
6556 * menu from being clipped too aggresively.
6557 *
6558 * The menu's position is automatically calculated and maintained when the menu
6559 * is toggled or the window is resized.
6560 *
6561 * See OO.ui.ComboBoxInputWidget for an example of a widget that uses this class.
6562 *
6563 * @class
6564 * @extends OO.ui.MenuSelectWidget
6565 * @mixins OO.ui.mixin.FloatableElement
6566 *
6567 * @constructor
6568 * @param {OO.ui.Widget} [inputWidget] Widget to provide the menu for.
6569 * Deprecated, omit this parameter and specify `$container` instead.
6570 * @param {Object} [config] Configuration options
6571 * @cfg {jQuery} [$container=inputWidget.$element] Element to render menu under
6572 */
6573 OO.ui.FloatingMenuSelectWidget = function OoUiFloatingMenuSelectWidget( inputWidget, config ) {
6574 // Allow 'inputWidget' parameter and config for backwards compatibility
6575 if ( OO.isPlainObject( inputWidget ) && config === undefined ) {
6576 config = inputWidget;
6577 inputWidget = config.inputWidget;
6578 }
6579
6580 // Configuration initialization
6581 config = config || {};
6582
6583 // Parent constructor
6584 OO.ui.FloatingMenuSelectWidget.parent.call( this, config );
6585
6586 // Properties (must be set before mixin constructors)
6587 this.inputWidget = inputWidget; // For backwards compatibility
6588 this.$container = config.$container || this.inputWidget.$element;
6589
6590 // Mixins constructors
6591 OO.ui.mixin.FloatableElement.call( this, $.extend( {}, config, { $floatableContainer: this.$container } ) );
6592
6593 // Initialization
6594 this.$element.addClass( 'oo-ui-floatingMenuSelectWidget' );
6595 // For backwards compatibility
6596 this.$element.addClass( 'oo-ui-textInputMenuSelectWidget' );
6597 };
6598
6599 /* Setup */
6600
6601 OO.inheritClass( OO.ui.FloatingMenuSelectWidget, OO.ui.MenuSelectWidget );
6602 OO.mixinClass( OO.ui.FloatingMenuSelectWidget, OO.ui.mixin.FloatableElement );
6603
6604 // For backwards compatibility
6605 OO.ui.TextInputMenuSelectWidget = OO.ui.FloatingMenuSelectWidget;
6606
6607 /* Methods */
6608
6609 /**
6610 * @inheritdoc
6611 */
6612 OO.ui.FloatingMenuSelectWidget.prototype.toggle = function ( visible ) {
6613 var change;
6614 visible = visible === undefined ? !this.isVisible() : !!visible;
6615 change = visible !== this.isVisible();
6616
6617 if ( change && visible ) {
6618 // Make sure the width is set before the parent method runs.
6619 this.setIdealSize( this.$container.width() );
6620 }
6621
6622 // Parent method
6623 // This will call this.clip(), which is nonsensical since we're not positioned yet...
6624 OO.ui.FloatingMenuSelectWidget.parent.prototype.toggle.call( this, visible );
6625
6626 if ( change ) {
6627 this.togglePositioning( this.isVisible() );
6628 }
6629
6630 return this;
6631 };
6632
6633 /**
6634 * InputWidget is the base class for all input widgets, which
6635 * include {@link OO.ui.TextInputWidget text inputs}, {@link OO.ui.CheckboxInputWidget checkbox inputs},
6636 * {@link OO.ui.RadioInputWidget radio inputs}, and {@link OO.ui.ButtonInputWidget button inputs}.
6637 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
6638 *
6639 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
6640 *
6641 * @abstract
6642 * @class
6643 * @extends OO.ui.Widget
6644 * @mixins OO.ui.mixin.FlaggedElement
6645 * @mixins OO.ui.mixin.TabIndexedElement
6646 * @mixins OO.ui.mixin.TitledElement
6647 * @mixins OO.ui.mixin.AccessKeyedElement
6648 *
6649 * @constructor
6650 * @param {Object} [config] Configuration options
6651 * @cfg {string} [name=''] The value of the input’s HTML `name` attribute.
6652 * @cfg {string} [value=''] The value of the input.
6653 * @cfg {string} [dir] The directionality of the input (ltr/rtl).
6654 * @cfg {Function} [inputFilter] The name of an input filter function. Input filters modify the value of an input
6655 * before it is accepted.
6656 */
6657 OO.ui.InputWidget = function OoUiInputWidget( config ) {
6658 // Configuration initialization
6659 config = config || {};
6660
6661 // Parent constructor
6662 OO.ui.InputWidget.parent.call( this, config );
6663
6664 // Properties
6665 this.$input = this.getInputElement( config );
6666 this.value = '';
6667 this.inputFilter = config.inputFilter;
6668
6669 // Mixin constructors
6670 OO.ui.mixin.FlaggedElement.call( this, config );
6671 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$input } ) );
6672 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
6673 OO.ui.mixin.AccessKeyedElement.call( this, $.extend( {}, config, { $accessKeyed: this.$input } ) );
6674
6675 // Events
6676 this.$input.on( 'keydown mouseup cut paste change input select', this.onEdit.bind( this ) );
6677
6678 // Initialization
6679 this.$input
6680 .addClass( 'oo-ui-inputWidget-input' )
6681 .attr( 'name', config.name )
6682 .prop( 'disabled', this.isDisabled() );
6683 this.$element
6684 .addClass( 'oo-ui-inputWidget' )
6685 .append( this.$input );
6686 this.setValue( config.value );
6687 this.setAccessKey( config.accessKey );
6688 if ( config.dir ) {
6689 this.setDir( config.dir );
6690 }
6691 };
6692
6693 /* Setup */
6694
6695 OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
6696 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.FlaggedElement );
6697 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.TabIndexedElement );
6698 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.TitledElement );
6699 OO.mixinClass( OO.ui.InputWidget, OO.ui.mixin.AccessKeyedElement );
6700
6701 /* Static Properties */
6702
6703 OO.ui.InputWidget.static.supportsSimpleLabel = true;
6704
6705 /* Static Methods */
6706
6707 /**
6708 * @inheritdoc
6709 */
6710 OO.ui.InputWidget.static.reusePreInfuseDOM = function ( node, config ) {
6711 config = OO.ui.InputWidget.parent.static.reusePreInfuseDOM( node, config );
6712 // Reusing $input lets browsers preserve inputted values across page reloads (T114134)
6713 config.$input = $( node ).find( '.oo-ui-inputWidget-input' );
6714 return config;
6715 };
6716
6717 /**
6718 * @inheritdoc
6719 */
6720 OO.ui.InputWidget.static.gatherPreInfuseState = function ( node, config ) {
6721 var state = OO.ui.InputWidget.parent.static.gatherPreInfuseState( node, config );
6722 state.value = config.$input.val();
6723 // Might be better in TabIndexedElement, but it's awkward to do there because mixins are awkward
6724 state.focus = config.$input.is( ':focus' );
6725 return state;
6726 };
6727
6728 /* Events */
6729
6730 /**
6731 * @event change
6732 *
6733 * A change event is emitted when the value of the input changes.
6734 *
6735 * @param {string} value
6736 */
6737
6738 /* Methods */
6739
6740 /**
6741 * Get input element.
6742 *
6743 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
6744 * different circumstances. The element must have a `value` property (like form elements).
6745 *
6746 * @protected
6747 * @param {Object} config Configuration options
6748 * @return {jQuery} Input element
6749 */
6750 OO.ui.InputWidget.prototype.getInputElement = function ( config ) {
6751 // See #reusePreInfuseDOM about config.$input
6752 return config.$input || $( '<input>' );
6753 };
6754
6755 /**
6756 * Handle potentially value-changing events.
6757 *
6758 * @private
6759 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
6760 */
6761 OO.ui.InputWidget.prototype.onEdit = function () {
6762 var widget = this;
6763 if ( !this.isDisabled() ) {
6764 // Allow the stack to clear so the value will be updated
6765 setTimeout( function () {
6766 widget.setValue( widget.$input.val() );
6767 } );
6768 }
6769 };
6770
6771 /**
6772 * Get the value of the input.
6773 *
6774 * @return {string} Input value
6775 */
6776 OO.ui.InputWidget.prototype.getValue = function () {
6777 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
6778 // it, and we won't know unless they're kind enough to trigger a 'change' event.
6779 var value = this.$input.val();
6780 if ( this.value !== value ) {
6781 this.setValue( value );
6782 }
6783 return this.value;
6784 };
6785
6786 /**
6787 * Set the directionality of the input, either RTL (right-to-left) or LTR (left-to-right).
6788 *
6789 * @deprecated since v0.13.1, use #setDir directly
6790 * @param {boolean} isRTL Directionality is right-to-left
6791 * @chainable
6792 */
6793 OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
6794 this.setDir( isRTL ? 'rtl' : 'ltr' );
6795 return this;
6796 };
6797
6798 /**
6799 * Set the directionality of the input.
6800 *
6801 * @param {string} dir Text directionality: 'ltr', 'rtl' or 'auto'
6802 * @chainable
6803 */
6804 OO.ui.InputWidget.prototype.setDir = function ( dir ) {
6805 this.$input.prop( 'dir', dir );
6806 return this;
6807 };
6808
6809 /**
6810 * Set the value of the input.
6811 *
6812 * @param {string} value New value
6813 * @fires change
6814 * @chainable
6815 */
6816 OO.ui.InputWidget.prototype.setValue = function ( value ) {
6817 value = this.cleanUpValue( value );
6818 // Update the DOM if it has changed. Note that with cleanUpValue, it
6819 // is possible for the DOM value to change without this.value changing.
6820 if ( this.$input.val() !== value ) {
6821 this.$input.val( value );
6822 }
6823 if ( this.value !== value ) {
6824 this.value = value;
6825 this.emit( 'change', this.value );
6826 }
6827 return this;
6828 };
6829
6830 /**
6831 * Set the input's access key.
6832 * FIXME: This is the same code as in OO.ui.mixin.ButtonElement, maybe find a better place for it?
6833 *
6834 * @param {string} accessKey Input's access key, use empty string to remove
6835 * @chainable
6836 */
6837 OO.ui.InputWidget.prototype.setAccessKey = function ( accessKey ) {
6838 accessKey = typeof accessKey === 'string' && accessKey.length ? accessKey : null;
6839
6840 if ( this.accessKey !== accessKey ) {
6841 if ( this.$input ) {
6842 if ( accessKey !== null ) {
6843 this.$input.attr( 'accesskey', accessKey );
6844 } else {
6845 this.$input.removeAttr( 'accesskey' );
6846 }
6847 }
6848 this.accessKey = accessKey;
6849 }
6850
6851 return this;
6852 };
6853
6854 /**
6855 * Clean up incoming value.
6856 *
6857 * Ensures value is a string, and converts undefined and null to empty string.
6858 *
6859 * @private
6860 * @param {string} value Original value
6861 * @return {string} Cleaned up value
6862 */
6863 OO.ui.InputWidget.prototype.cleanUpValue = function ( value ) {
6864 if ( value === undefined || value === null ) {
6865 return '';
6866 } else if ( this.inputFilter ) {
6867 return this.inputFilter( String( value ) );
6868 } else {
6869 return String( value );
6870 }
6871 };
6872
6873 /**
6874 * Simulate the behavior of clicking on a label bound to this input. This method is only called by
6875 * {@link OO.ui.LabelWidget LabelWidget} and {@link OO.ui.FieldLayout FieldLayout}. It should not be
6876 * called directly.
6877 */
6878 OO.ui.InputWidget.prototype.simulateLabelClick = function () {
6879 if ( !this.isDisabled() ) {
6880 if ( this.$input.is( ':checkbox, :radio' ) ) {
6881 this.$input.click();
6882 }
6883 if ( this.$input.is( ':input' ) ) {
6884 this.$input[ 0 ].focus();
6885 }
6886 }
6887 };
6888
6889 /**
6890 * @inheritdoc
6891 */
6892 OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
6893 OO.ui.InputWidget.parent.prototype.setDisabled.call( this, state );
6894 if ( this.$input ) {
6895 this.$input.prop( 'disabled', this.isDisabled() );
6896 }
6897 return this;
6898 };
6899
6900 /**
6901 * Focus the input.
6902 *
6903 * @chainable
6904 */
6905 OO.ui.InputWidget.prototype.focus = function () {
6906 this.$input[ 0 ].focus();
6907 return this;
6908 };
6909
6910 /**
6911 * Blur the input.
6912 *
6913 * @chainable
6914 */
6915 OO.ui.InputWidget.prototype.blur = function () {
6916 this.$input[ 0 ].blur();
6917 return this;
6918 };
6919
6920 /**
6921 * @inheritdoc
6922 */
6923 OO.ui.InputWidget.prototype.restorePreInfuseState = function ( state ) {
6924 OO.ui.InputWidget.parent.prototype.restorePreInfuseState.call( this, state );
6925 if ( state.value !== undefined && state.value !== this.getValue() ) {
6926 this.setValue( state.value );
6927 }
6928 if ( state.focus ) {
6929 this.focus();
6930 }
6931 };
6932
6933 /**
6934 * ButtonInputWidget is used to submit HTML forms and is intended to be used within
6935 * a OO.ui.FormLayout. If you do not need the button to work with HTML forms, you probably
6936 * want to use OO.ui.ButtonWidget instead. Button input widgets can be rendered as either an
6937 * HTML `<button/>` (the default) or an HTML `<input/>` tags. See the
6938 * [OOjs UI documentation on MediaWiki] [1] for more information.
6939 *
6940 * @example
6941 * // A ButtonInputWidget rendered as an HTML button, the default.
6942 * var button = new OO.ui.ButtonInputWidget( {
6943 * label: 'Input button',
6944 * icon: 'check',
6945 * value: 'check'
6946 * } );
6947 * $( 'body' ).append( button.$element );
6948 *
6949 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs#Button_inputs
6950 *
6951 * @class
6952 * @extends OO.ui.InputWidget
6953 * @mixins OO.ui.mixin.ButtonElement
6954 * @mixins OO.ui.mixin.IconElement
6955 * @mixins OO.ui.mixin.IndicatorElement
6956 * @mixins OO.ui.mixin.LabelElement
6957 * @mixins OO.ui.mixin.TitledElement
6958 *
6959 * @constructor
6960 * @param {Object} [config] Configuration options
6961 * @cfg {string} [type='button'] The value of the HTML `'type'` attribute: 'button', 'submit' or 'reset'.
6962 * @cfg {boolean} [useInputTag=false] Use an `<input/>` tag instead of a `<button/>` tag, the default.
6963 * Widgets configured to be an `<input/>` do not support {@link #icon icons} and {@link #indicator indicators},
6964 * non-plaintext {@link #label labels}, or {@link #value values}. In general, useInputTag should only
6965 * be set to `true` when there’s need to support IE6 in a form with multiple buttons.
6966 */
6967 OO.ui.ButtonInputWidget = function OoUiButtonInputWidget( config ) {
6968 // Configuration initialization
6969 config = $.extend( { type: 'button', useInputTag: false }, config );
6970
6971 // Properties (must be set before parent constructor, which calls #setValue)
6972 this.useInputTag = config.useInputTag;
6973
6974 // Parent constructor
6975 OO.ui.ButtonInputWidget.parent.call( this, config );
6976
6977 // Mixin constructors
6978 OO.ui.mixin.ButtonElement.call( this, $.extend( {}, config, { $button: this.$input } ) );
6979 OO.ui.mixin.IconElement.call( this, config );
6980 OO.ui.mixin.IndicatorElement.call( this, config );
6981 OO.ui.mixin.LabelElement.call( this, config );
6982 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
6983
6984 // Initialization
6985 if ( !config.useInputTag ) {
6986 this.$input.append( this.$icon, this.$label, this.$indicator );
6987 }
6988 this.$element.addClass( 'oo-ui-buttonInputWidget' );
6989 };
6990
6991 /* Setup */
6992
6993 OO.inheritClass( OO.ui.ButtonInputWidget, OO.ui.InputWidget );
6994 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.ButtonElement );
6995 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IconElement );
6996 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.IndicatorElement );
6997 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.LabelElement );
6998 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.mixin.TitledElement );
6999
7000 /* Static Properties */
7001
7002 /**
7003 * Disable generating `<label>` elements for buttons. One would very rarely need additional label
7004 * for a button, and it's already a big clickable target, and it causes unexpected rendering.
7005 */
7006 OO.ui.ButtonInputWidget.static.supportsSimpleLabel = false;
7007
7008 /* Methods */
7009
7010 /**
7011 * @inheritdoc
7012 * @protected
7013 */
7014 OO.ui.ButtonInputWidget.prototype.getInputElement = function ( config ) {
7015 var type;
7016 // See InputWidget#reusePreInfuseDOM about config.$input
7017 if ( config.$input ) {
7018 return config.$input.empty();
7019 }
7020 type = [ 'button', 'submit', 'reset' ].indexOf( config.type ) !== -1 ? config.type : 'button';
7021 return $( '<' + ( config.useInputTag ? 'input' : 'button' ) + ' type="' + type + '">' );
7022 };
7023
7024 /**
7025 * Set label value.
7026 *
7027 * If #useInputTag is `true`, the label is set as the `value` of the `<input/>` tag.
7028 *
7029 * @param {jQuery|string|Function|null} label Label nodes, text, a function that returns nodes or
7030 * text, or `null` for no label
7031 * @chainable
7032 */
7033 OO.ui.ButtonInputWidget.prototype.setLabel = function ( label ) {
7034 OO.ui.mixin.LabelElement.prototype.setLabel.call( this, label );
7035
7036 if ( this.useInputTag ) {
7037 if ( typeof label === 'function' ) {
7038 label = OO.ui.resolveMsg( label );
7039 }
7040 if ( label instanceof jQuery ) {
7041 label = label.text();
7042 }
7043 if ( !label ) {
7044 label = '';
7045 }
7046 this.$input.val( label );
7047 }
7048
7049 return this;
7050 };
7051
7052 /**
7053 * Set the value of the input.
7054 *
7055 * This method is disabled for button inputs configured as {@link #useInputTag <input/> tags}, as
7056 * they do not support {@link #value values}.
7057 *
7058 * @param {string} value New value
7059 * @chainable
7060 */
7061 OO.ui.ButtonInputWidget.prototype.setValue = function ( value ) {
7062 if ( !this.useInputTag ) {
7063 OO.ui.ButtonInputWidget.parent.prototype.setValue.call( this, value );
7064 }
7065 return this;
7066 };
7067
7068 /**
7069 * CheckboxInputWidgets, like HTML checkboxes, can be selected and/or configured with a value.
7070 * Note that these {@link OO.ui.InputWidget input widgets} are best laid out
7071 * in {@link OO.ui.FieldLayout field layouts} that use the {@link OO.ui.FieldLayout#align inline}
7072 * alignment. For more information, please see the [OOjs UI documentation on MediaWiki][1].
7073 *
7074 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
7075 *
7076 * @example
7077 * // An example of selected, unselected, and disabled checkbox inputs
7078 * var checkbox1=new OO.ui.CheckboxInputWidget( {
7079 * value: 'a',
7080 * selected: true
7081 * } );
7082 * var checkbox2=new OO.ui.CheckboxInputWidget( {
7083 * value: 'b'
7084 * } );
7085 * var checkbox3=new OO.ui.CheckboxInputWidget( {
7086 * value:'c',
7087 * disabled: true
7088 * } );
7089 * // Create a fieldset layout with fields for each checkbox.
7090 * var fieldset = new OO.ui.FieldsetLayout( {
7091 * label: 'Checkboxes'
7092 * } );
7093 * fieldset.addItems( [
7094 * new OO.ui.FieldLayout( checkbox1, { label: 'Selected checkbox', align: 'inline' } ),
7095 * new OO.ui.FieldLayout( checkbox2, { label: 'Unselected checkbox', align: 'inline' } ),
7096 * new OO.ui.FieldLayout( checkbox3, { label: 'Disabled checkbox', align: 'inline' } ),
7097 * ] );
7098 * $( 'body' ).append( fieldset.$element );
7099 *
7100 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7101 *
7102 * @class
7103 * @extends OO.ui.InputWidget
7104 *
7105 * @constructor
7106 * @param {Object} [config] Configuration options
7107 * @cfg {boolean} [selected=false] Select the checkbox initially. By default, the checkbox is not selected.
7108 */
7109 OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
7110 // Configuration initialization
7111 config = config || {};
7112
7113 // Parent constructor
7114 OO.ui.CheckboxInputWidget.parent.call( this, config );
7115
7116 // Initialization
7117 this.$element
7118 .addClass( 'oo-ui-checkboxInputWidget' )
7119 // Required for pretty styling in MediaWiki theme
7120 .append( $( '<span>' ) );
7121 this.setSelected( config.selected !== undefined ? config.selected : false );
7122 };
7123
7124 /* Setup */
7125
7126 OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
7127
7128 /* Static Methods */
7129
7130 /**
7131 * @inheritdoc
7132 */
7133 OO.ui.CheckboxInputWidget.static.gatherPreInfuseState = function ( node, config ) {
7134 var state = OO.ui.CheckboxInputWidget.parent.static.gatherPreInfuseState( node, config );
7135 state.checked = config.$input.prop( 'checked' );
7136 return state;
7137 };
7138
7139 /* Methods */
7140
7141 /**
7142 * @inheritdoc
7143 * @protected
7144 */
7145 OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
7146 return $( '<input type="checkbox" />' );
7147 };
7148
7149 /**
7150 * @inheritdoc
7151 */
7152 OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
7153 var widget = this;
7154 if ( !this.isDisabled() ) {
7155 // Allow the stack to clear so the value will be updated
7156 setTimeout( function () {
7157 widget.setSelected( widget.$input.prop( 'checked' ) );
7158 } );
7159 }
7160 };
7161
7162 /**
7163 * Set selection state of this checkbox.
7164 *
7165 * @param {boolean} state `true` for selected
7166 * @chainable
7167 */
7168 OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state ) {
7169 state = !!state;
7170 if ( this.selected !== state ) {
7171 this.selected = state;
7172 this.$input.prop( 'checked', this.selected );
7173 this.emit( 'change', this.selected );
7174 }
7175 return this;
7176 };
7177
7178 /**
7179 * Check if this checkbox is selected.
7180 *
7181 * @return {boolean} Checkbox is selected
7182 */
7183 OO.ui.CheckboxInputWidget.prototype.isSelected = function () {
7184 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
7185 // it, and we won't know unless they're kind enough to trigger a 'change' event.
7186 var selected = this.$input.prop( 'checked' );
7187 if ( this.selected !== selected ) {
7188 this.setSelected( selected );
7189 }
7190 return this.selected;
7191 };
7192
7193 /**
7194 * @inheritdoc
7195 */
7196 OO.ui.CheckboxInputWidget.prototype.restorePreInfuseState = function ( state ) {
7197 OO.ui.CheckboxInputWidget.parent.prototype.restorePreInfuseState.call( this, state );
7198 if ( state.checked !== undefined && state.checked !== this.isSelected() ) {
7199 this.setSelected( state.checked );
7200 }
7201 };
7202
7203 /**
7204 * DropdownInputWidget is a {@link OO.ui.DropdownWidget DropdownWidget} intended to be used
7205 * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
7206 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
7207 * more information about input widgets.
7208 *
7209 * A DropdownInputWidget always has a value (one of the options is always selected), unless there
7210 * are no options. If no `value` configuration option is provided, the first option is selected.
7211 * If you need a state representing no value (no option being selected), use a DropdownWidget.
7212 *
7213 * This and OO.ui.RadioSelectInputWidget support the same configuration options.
7214 *
7215 * @example
7216 * // Example: A DropdownInputWidget with three options
7217 * var dropdownInput = new OO.ui.DropdownInputWidget( {
7218 * options: [
7219 * { data: 'a', label: 'First' },
7220 * { data: 'b', label: 'Second'},
7221 * { data: 'c', label: 'Third' }
7222 * ]
7223 * } );
7224 * $( 'body' ).append( dropdownInput.$element );
7225 *
7226 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7227 *
7228 * @class
7229 * @extends OO.ui.InputWidget
7230 * @mixins OO.ui.mixin.TitledElement
7231 *
7232 * @constructor
7233 * @param {Object} [config] Configuration options
7234 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
7235 * @cfg {Object} [dropdown] Configuration options for {@link OO.ui.DropdownWidget DropdownWidget}
7236 */
7237 OO.ui.DropdownInputWidget = function OoUiDropdownInputWidget( config ) {
7238 // Configuration initialization
7239 config = config || {};
7240
7241 // Properties (must be done before parent constructor which calls #setDisabled)
7242 this.dropdownWidget = new OO.ui.DropdownWidget( config.dropdown );
7243
7244 // Parent constructor
7245 OO.ui.DropdownInputWidget.parent.call( this, config );
7246
7247 // Mixin constructors
7248 OO.ui.mixin.TitledElement.call( this, config );
7249
7250 // Events
7251 this.dropdownWidget.getMenu().connect( this, { select: 'onMenuSelect' } );
7252
7253 // Initialization
7254 this.setOptions( config.options || [] );
7255 this.$element
7256 .addClass( 'oo-ui-dropdownInputWidget' )
7257 .append( this.dropdownWidget.$element );
7258 };
7259
7260 /* Setup */
7261
7262 OO.inheritClass( OO.ui.DropdownInputWidget, OO.ui.InputWidget );
7263 OO.mixinClass( OO.ui.DropdownInputWidget, OO.ui.mixin.TitledElement );
7264
7265 /* Methods */
7266
7267 /**
7268 * @inheritdoc
7269 * @protected
7270 */
7271 OO.ui.DropdownInputWidget.prototype.getInputElement = function ( config ) {
7272 // See InputWidget#reusePreInfuseDOM about config.$input
7273 if ( config.$input ) {
7274 return config.$input.addClass( 'oo-ui-element-hidden' );
7275 }
7276 return $( '<input type="hidden">' );
7277 };
7278
7279 /**
7280 * Handles menu select events.
7281 *
7282 * @private
7283 * @param {OO.ui.MenuOptionWidget} item Selected menu item
7284 */
7285 OO.ui.DropdownInputWidget.prototype.onMenuSelect = function ( item ) {
7286 this.setValue( item.getData() );
7287 };
7288
7289 /**
7290 * @inheritdoc
7291 */
7292 OO.ui.DropdownInputWidget.prototype.setValue = function ( value ) {
7293 value = this.cleanUpValue( value );
7294 this.dropdownWidget.getMenu().selectItemByData( value );
7295 OO.ui.DropdownInputWidget.parent.prototype.setValue.call( this, value );
7296 return this;
7297 };
7298
7299 /**
7300 * @inheritdoc
7301 */
7302 OO.ui.DropdownInputWidget.prototype.setDisabled = function ( state ) {
7303 this.dropdownWidget.setDisabled( state );
7304 OO.ui.DropdownInputWidget.parent.prototype.setDisabled.call( this, state );
7305 return this;
7306 };
7307
7308 /**
7309 * Set the options available for this input.
7310 *
7311 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
7312 * @chainable
7313 */
7314 OO.ui.DropdownInputWidget.prototype.setOptions = function ( options ) {
7315 var
7316 value = this.getValue(),
7317 widget = this;
7318
7319 // Rebuild the dropdown menu
7320 this.dropdownWidget.getMenu()
7321 .clearItems()
7322 .addItems( options.map( function ( opt ) {
7323 var optValue = widget.cleanUpValue( opt.data );
7324 return new OO.ui.MenuOptionWidget( {
7325 data: optValue,
7326 label: opt.label !== undefined ? opt.label : optValue
7327 } );
7328 } ) );
7329
7330 // Restore the previous value, or reset to something sensible
7331 if ( this.dropdownWidget.getMenu().getItemFromData( value ) ) {
7332 // Previous value is still available, ensure consistency with the dropdown
7333 this.setValue( value );
7334 } else {
7335 // No longer valid, reset
7336 if ( options.length ) {
7337 this.setValue( options[ 0 ].data );
7338 }
7339 }
7340
7341 return this;
7342 };
7343
7344 /**
7345 * @inheritdoc
7346 */
7347 OO.ui.DropdownInputWidget.prototype.focus = function () {
7348 this.dropdownWidget.getMenu().toggle( true );
7349 return this;
7350 };
7351
7352 /**
7353 * @inheritdoc
7354 */
7355 OO.ui.DropdownInputWidget.prototype.blur = function () {
7356 this.dropdownWidget.getMenu().toggle( false );
7357 return this;
7358 };
7359
7360 /**
7361 * RadioInputWidget creates a single radio button. Because radio buttons are usually used as a set,
7362 * in most cases you will want to use a {@link OO.ui.RadioSelectWidget radio select}
7363 * with {@link OO.ui.RadioOptionWidget radio options} instead of this class. For more information,
7364 * please see the [OOjs UI documentation on MediaWiki][1].
7365 *
7366 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
7367 *
7368 * @example
7369 * // An example of selected, unselected, and disabled radio inputs
7370 * var radio1 = new OO.ui.RadioInputWidget( {
7371 * value: 'a',
7372 * selected: true
7373 * } );
7374 * var radio2 = new OO.ui.RadioInputWidget( {
7375 * value: 'b'
7376 * } );
7377 * var radio3 = new OO.ui.RadioInputWidget( {
7378 * value: 'c',
7379 * disabled: true
7380 * } );
7381 * // Create a fieldset layout with fields for each radio button.
7382 * var fieldset = new OO.ui.FieldsetLayout( {
7383 * label: 'Radio inputs'
7384 * } );
7385 * fieldset.addItems( [
7386 * new OO.ui.FieldLayout( radio1, { label: 'Selected', align: 'inline' } ),
7387 * new OO.ui.FieldLayout( radio2, { label: 'Unselected', align: 'inline' } ),
7388 * new OO.ui.FieldLayout( radio3, { label: 'Disabled', align: 'inline' } ),
7389 * ] );
7390 * $( 'body' ).append( fieldset.$element );
7391 *
7392 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7393 *
7394 * @class
7395 * @extends OO.ui.InputWidget
7396 *
7397 * @constructor
7398 * @param {Object} [config] Configuration options
7399 * @cfg {boolean} [selected=false] Select the radio button initially. By default, the radio button is not selected.
7400 */
7401 OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) {
7402 // Configuration initialization
7403 config = config || {};
7404
7405 // Parent constructor
7406 OO.ui.RadioInputWidget.parent.call( this, config );
7407
7408 // Initialization
7409 this.$element
7410 .addClass( 'oo-ui-radioInputWidget' )
7411 // Required for pretty styling in MediaWiki theme
7412 .append( $( '<span>' ) );
7413 this.setSelected( config.selected !== undefined ? config.selected : false );
7414 };
7415
7416 /* Setup */
7417
7418 OO.inheritClass( OO.ui.RadioInputWidget, OO.ui.InputWidget );
7419
7420 /* Static Methods */
7421
7422 /**
7423 * @inheritdoc
7424 */
7425 OO.ui.RadioInputWidget.static.gatherPreInfuseState = function ( node, config ) {
7426 var state = OO.ui.RadioInputWidget.parent.static.gatherPreInfuseState( node, config );
7427 state.checked = config.$input.prop( 'checked' );
7428 return state;
7429 };
7430
7431 /* Methods */
7432
7433 /**
7434 * @inheritdoc
7435 * @protected
7436 */
7437 OO.ui.RadioInputWidget.prototype.getInputElement = function () {
7438 return $( '<input type="radio" />' );
7439 };
7440
7441 /**
7442 * @inheritdoc
7443 */
7444 OO.ui.RadioInputWidget.prototype.onEdit = function () {
7445 // RadioInputWidget doesn't track its state.
7446 };
7447
7448 /**
7449 * Set selection state of this radio button.
7450 *
7451 * @param {boolean} state `true` for selected
7452 * @chainable
7453 */
7454 OO.ui.RadioInputWidget.prototype.setSelected = function ( state ) {
7455 // RadioInputWidget doesn't track its state.
7456 this.$input.prop( 'checked', state );
7457 return this;
7458 };
7459
7460 /**
7461 * Check if this radio button is selected.
7462 *
7463 * @return {boolean} Radio is selected
7464 */
7465 OO.ui.RadioInputWidget.prototype.isSelected = function () {
7466 return this.$input.prop( 'checked' );
7467 };
7468
7469 /**
7470 * @inheritdoc
7471 */
7472 OO.ui.RadioInputWidget.prototype.restorePreInfuseState = function ( state ) {
7473 OO.ui.RadioInputWidget.parent.prototype.restorePreInfuseState.call( this, state );
7474 if ( state.checked !== undefined && state.checked !== this.isSelected() ) {
7475 this.setSelected( state.checked );
7476 }
7477 };
7478
7479 /**
7480 * RadioSelectInputWidget is a {@link OO.ui.RadioSelectWidget RadioSelectWidget} intended to be used
7481 * within a HTML form, such as a OO.ui.FormLayout. The selected value is synchronized with the value
7482 * of a hidden HTML `input` tag. Please see the [OOjs UI documentation on MediaWiki][1] for
7483 * more information about input widgets.
7484 *
7485 * This and OO.ui.DropdownInputWidget support the same configuration options.
7486 *
7487 * @example
7488 * // Example: A RadioSelectInputWidget with three options
7489 * var radioSelectInput = new OO.ui.RadioSelectInputWidget( {
7490 * options: [
7491 * { data: 'a', label: 'First' },
7492 * { data: 'b', label: 'Second'},
7493 * { data: 'c', label: 'Third' }
7494 * ]
7495 * } );
7496 * $( 'body' ).append( radioSelectInput.$element );
7497 *
7498 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7499 *
7500 * @class
7501 * @extends OO.ui.InputWidget
7502 *
7503 * @constructor
7504 * @param {Object} [config] Configuration options
7505 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
7506 */
7507 OO.ui.RadioSelectInputWidget = function OoUiRadioSelectInputWidget( config ) {
7508 // Configuration initialization
7509 config = config || {};
7510
7511 // Properties (must be done before parent constructor which calls #setDisabled)
7512 this.radioSelectWidget = new OO.ui.RadioSelectWidget();
7513
7514 // Parent constructor
7515 OO.ui.RadioSelectInputWidget.parent.call( this, config );
7516
7517 // Events
7518 this.radioSelectWidget.connect( this, { select: 'onMenuSelect' } );
7519
7520 // Initialization
7521 this.setOptions( config.options || [] );
7522 this.$element
7523 .addClass( 'oo-ui-radioSelectInputWidget' )
7524 .append( this.radioSelectWidget.$element );
7525 };
7526
7527 /* Setup */
7528
7529 OO.inheritClass( OO.ui.RadioSelectInputWidget, OO.ui.InputWidget );
7530
7531 /* Static Properties */
7532
7533 OO.ui.RadioSelectInputWidget.static.supportsSimpleLabel = false;
7534
7535 /* Static Methods */
7536
7537 /**
7538 * @inheritdoc
7539 */
7540 OO.ui.RadioSelectInputWidget.static.gatherPreInfuseState = function ( node, config ) {
7541 var state = OO.ui.RadioSelectInputWidget.parent.static.gatherPreInfuseState( node, config );
7542 state.value = $( node ).find( '.oo-ui-radioInputWidget .oo-ui-inputWidget-input:checked' ).val();
7543 return state;
7544 };
7545
7546 /* Methods */
7547
7548 /**
7549 * @inheritdoc
7550 * @protected
7551 */
7552 OO.ui.RadioSelectInputWidget.prototype.getInputElement = function () {
7553 return $( '<input type="hidden">' );
7554 };
7555
7556 /**
7557 * Handles menu select events.
7558 *
7559 * @private
7560 * @param {OO.ui.RadioOptionWidget} item Selected menu item
7561 */
7562 OO.ui.RadioSelectInputWidget.prototype.onMenuSelect = function ( item ) {
7563 this.setValue( item.getData() );
7564 };
7565
7566 /**
7567 * @inheritdoc
7568 */
7569 OO.ui.RadioSelectInputWidget.prototype.setValue = function ( value ) {
7570 value = this.cleanUpValue( value );
7571 this.radioSelectWidget.selectItemByData( value );
7572 OO.ui.RadioSelectInputWidget.parent.prototype.setValue.call( this, value );
7573 return this;
7574 };
7575
7576 /**
7577 * @inheritdoc
7578 */
7579 OO.ui.RadioSelectInputWidget.prototype.setDisabled = function ( state ) {
7580 this.radioSelectWidget.setDisabled( state );
7581 OO.ui.RadioSelectInputWidget.parent.prototype.setDisabled.call( this, state );
7582 return this;
7583 };
7584
7585 /**
7586 * Set the options available for this input.
7587 *
7588 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
7589 * @chainable
7590 */
7591 OO.ui.RadioSelectInputWidget.prototype.setOptions = function ( options ) {
7592 var
7593 value = this.getValue(),
7594 widget = this;
7595
7596 // Rebuild the radioSelect menu
7597 this.radioSelectWidget
7598 .clearItems()
7599 .addItems( options.map( function ( opt ) {
7600 var optValue = widget.cleanUpValue( opt.data );
7601 return new OO.ui.RadioOptionWidget( {
7602 data: optValue,
7603 label: opt.label !== undefined ? opt.label : optValue
7604 } );
7605 } ) );
7606
7607 // Restore the previous value, or reset to something sensible
7608 if ( this.radioSelectWidget.getItemFromData( value ) ) {
7609 // Previous value is still available, ensure consistency with the radioSelect
7610 this.setValue( value );
7611 } else {
7612 // No longer valid, reset
7613 if ( options.length ) {
7614 this.setValue( options[ 0 ].data );
7615 }
7616 }
7617
7618 return this;
7619 };
7620
7621 /**
7622 * TextInputWidgets, like HTML text inputs, can be configured with options that customize the
7623 * size of the field as well as its presentation. In addition, these widgets can be configured
7624 * with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators}, an optional
7625 * validation-pattern (used to determine if an input value is valid or not) and an input filter,
7626 * which modifies incoming values rather than validating them.
7627 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
7628 *
7629 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
7630 *
7631 * @example
7632 * // Example of a text input widget
7633 * var textInput = new OO.ui.TextInputWidget( {
7634 * value: 'Text input'
7635 * } )
7636 * $( 'body' ).append( textInput.$element );
7637 *
7638 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
7639 *
7640 * @class
7641 * @extends OO.ui.InputWidget
7642 * @mixins OO.ui.mixin.IconElement
7643 * @mixins OO.ui.mixin.IndicatorElement
7644 * @mixins OO.ui.mixin.PendingElement
7645 * @mixins OO.ui.mixin.LabelElement
7646 *
7647 * @constructor
7648 * @param {Object} [config] Configuration options
7649 * @cfg {string} [type='text'] The value of the HTML `type` attribute: 'text', 'password', 'search',
7650 * 'email' or 'url'. Ignored if `multiline` is true.
7651 *
7652 * Some values of `type` result in additional behaviors:
7653 *
7654 * - `search`: implies `icon: 'search'` and `indicator: 'clear'`; when clicked, the indicator
7655 * empties the text field
7656 * @cfg {string} [placeholder] Placeholder text
7657 * @cfg {boolean} [autofocus=false] Use an HTML `autofocus` attribute to
7658 * instruct the browser to focus this widget.
7659 * @cfg {boolean} [readOnly=false] Prevent changes to the value of the text input.
7660 * @cfg {number} [maxLength] Maximum number of characters allowed in the input.
7661 * @cfg {boolean} [multiline=false] Allow multiple lines of text
7662 * @cfg {number} [rows] If multiline, number of visible lines in textarea. If used with `autosize`,
7663 * specifies minimum number of rows to display.
7664 * @cfg {boolean} [autosize=false] Automatically resize the text input to fit its content.
7665 * Use the #maxRows config to specify a maximum number of displayed rows.
7666 * @cfg {boolean} [maxRows] Maximum number of rows to display when #autosize is set to true.
7667 * Defaults to the maximum of `10` and `2 * rows`, or `10` if `rows` isn't provided.
7668 * @cfg {string} [labelPosition='after'] The position of the inline label relative to that of
7669 * the value or placeholder text: `'before'` or `'after'`
7670 * @cfg {boolean} [required=false] Mark the field as required. Implies `indicator: 'required'`.
7671 * @cfg {boolean} [autocomplete=true] Should the browser support autocomplete for this field
7672 * @cfg {RegExp|Function|string} [validate] Validation pattern: when string, a symbolic name of a
7673 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer'
7674 * (the value must contain only numbers); when RegExp, a regular expression that must match the
7675 * value for it to be considered valid; when Function, a function receiving the value as parameter
7676 * that must return true, or promise resolving to true, for it to be considered valid.
7677 */
7678 OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
7679 // Configuration initialization
7680 config = $.extend( {
7681 type: 'text',
7682 labelPosition: 'after'
7683 }, config );
7684 if ( config.type === 'search' ) {
7685 if ( config.icon === undefined ) {
7686 config.icon = 'search';
7687 }
7688 // indicator: 'clear' is set dynamically later, depending on value
7689 }
7690 if ( config.required ) {
7691 if ( config.indicator === undefined ) {
7692 config.indicator = 'required';
7693 }
7694 }
7695
7696 // Parent constructor
7697 OO.ui.TextInputWidget.parent.call( this, config );
7698
7699 // Mixin constructors
7700 OO.ui.mixin.IconElement.call( this, config );
7701 OO.ui.mixin.IndicatorElement.call( this, config );
7702 OO.ui.mixin.PendingElement.call( this, $.extend( {}, config, { $pending: this.$input } ) );
7703 OO.ui.mixin.LabelElement.call( this, config );
7704
7705 // Properties
7706 this.type = this.getSaneType( config );
7707 this.readOnly = false;
7708 this.multiline = !!config.multiline;
7709 this.autosize = !!config.autosize;
7710 this.minRows = config.rows !== undefined ? config.rows : '';
7711 this.maxRows = config.maxRows || Math.max( 2 * ( this.minRows || 0 ), 10 );
7712 this.validate = null;
7713 this.styleHeight = null;
7714 this.scrollWidth = null;
7715
7716 // Clone for resizing
7717 if ( this.autosize ) {
7718 this.$clone = this.$input
7719 .clone()
7720 .insertAfter( this.$input )
7721 .attr( 'aria-hidden', 'true' )
7722 .addClass( 'oo-ui-element-hidden' );
7723 }
7724
7725 this.setValidation( config.validate );
7726 this.setLabelPosition( config.labelPosition );
7727
7728 // Events
7729 this.$input.on( {
7730 keypress: this.onKeyPress.bind( this ),
7731 blur: this.onBlur.bind( this )
7732 } );
7733 this.$input.one( {
7734 focus: this.onElementAttach.bind( this )
7735 } );
7736 this.$icon.on( 'mousedown', this.onIconMouseDown.bind( this ) );
7737 this.$indicator.on( 'mousedown', this.onIndicatorMouseDown.bind( this ) );
7738 this.on( 'labelChange', this.updatePosition.bind( this ) );
7739 this.connect( this, {
7740 change: 'onChange',
7741 disable: 'onDisable'
7742 } );
7743
7744 // Initialization
7745 this.$element
7746 .addClass( 'oo-ui-textInputWidget oo-ui-textInputWidget-type-' + this.type )
7747 .append( this.$icon, this.$indicator );
7748 this.setReadOnly( !!config.readOnly );
7749 this.updateSearchIndicator();
7750 if ( config.placeholder ) {
7751 this.$input.attr( 'placeholder', config.placeholder );
7752 }
7753 if ( config.maxLength !== undefined ) {
7754 this.$input.attr( 'maxlength', config.maxLength );
7755 }
7756 if ( config.autofocus ) {
7757 this.$input.attr( 'autofocus', 'autofocus' );
7758 }
7759 if ( config.required ) {
7760 this.$input.attr( 'required', 'required' );
7761 this.$input.attr( 'aria-required', 'true' );
7762 }
7763 if ( config.autocomplete === false ) {
7764 this.$input.attr( 'autocomplete', 'off' );
7765 // Turning off autocompletion also disables "form caching" when the user navigates to a
7766 // different page and then clicks "Back". Re-enable it when leaving. Borrowed from jQuery UI.
7767 $( window ).on( {
7768 beforeunload: function () {
7769 this.$input.removeAttr( 'autocomplete' );
7770 }.bind( this ),
7771 pageshow: function () {
7772 // Browsers don't seem to actually fire this event on "Back", they instead just reload the
7773 // whole page... it shouldn't hurt, though.
7774 this.$input.attr( 'autocomplete', 'off' );
7775 }.bind( this )
7776 } );
7777 }
7778 if ( this.multiline && config.rows ) {
7779 this.$input.attr( 'rows', config.rows );
7780 }
7781 if ( this.label || config.autosize ) {
7782 this.installParentChangeDetector();
7783 }
7784 };
7785
7786 /* Setup */
7787
7788 OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
7789 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IconElement );
7790 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.IndicatorElement );
7791 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.PendingElement );
7792 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.mixin.LabelElement );
7793
7794 /* Static Properties */
7795
7796 OO.ui.TextInputWidget.static.validationPatterns = {
7797 'non-empty': /.+/,
7798 integer: /^\d+$/
7799 };
7800
7801 /* Static Methods */
7802
7803 /**
7804 * @inheritdoc
7805 */
7806 OO.ui.TextInputWidget.static.gatherPreInfuseState = function ( node, config ) {
7807 var state = OO.ui.TextInputWidget.parent.static.gatherPreInfuseState( node, config );
7808 if ( config.multiline ) {
7809 state.scrollTop = config.$input.scrollTop();
7810 }
7811 return state;
7812 };
7813
7814 /* Events */
7815
7816 /**
7817 * An `enter` event is emitted when the user presses 'enter' inside the text box.
7818 *
7819 * Not emitted if the input is multiline.
7820 *
7821 * @event enter
7822 */
7823
7824 /**
7825 * A `resize` event is emitted when autosize is set and the widget resizes
7826 *
7827 * @event resize
7828 */
7829
7830 /* Methods */
7831
7832 /**
7833 * Handle icon mouse down events.
7834 *
7835 * @private
7836 * @param {jQuery.Event} e Mouse down event
7837 * @fires icon
7838 */
7839 OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
7840 if ( e.which === OO.ui.MouseButtons.LEFT ) {
7841 this.$input[ 0 ].focus();
7842 return false;
7843 }
7844 };
7845
7846 /**
7847 * Handle indicator mouse down events.
7848 *
7849 * @private
7850 * @param {jQuery.Event} e Mouse down event
7851 * @fires indicator
7852 */
7853 OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
7854 if ( e.which === OO.ui.MouseButtons.LEFT ) {
7855 if ( this.type === 'search' ) {
7856 // Clear the text field
7857 this.setValue( '' );
7858 }
7859 this.$input[ 0 ].focus();
7860 return false;
7861 }
7862 };
7863
7864 /**
7865 * Handle key press events.
7866 *
7867 * @private
7868 * @param {jQuery.Event} e Key press event
7869 * @fires enter If enter key is pressed and input is not multiline
7870 */
7871 OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
7872 if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) {
7873 this.emit( 'enter', e );
7874 }
7875 };
7876
7877 /**
7878 * Handle blur events.
7879 *
7880 * @private
7881 * @param {jQuery.Event} e Blur event
7882 */
7883 OO.ui.TextInputWidget.prototype.onBlur = function () {
7884 this.setValidityFlag();
7885 };
7886
7887 /**
7888 * Handle element attach events.
7889 *
7890 * @private
7891 * @param {jQuery.Event} e Element attach event
7892 */
7893 OO.ui.TextInputWidget.prototype.onElementAttach = function () {
7894 // Any previously calculated size is now probably invalid if we reattached elsewhere
7895 this.valCache = null;
7896 this.adjustSize();
7897 this.positionLabel();
7898 };
7899
7900 /**
7901 * Handle change events.
7902 *
7903 * @param {string} value
7904 * @private
7905 */
7906 OO.ui.TextInputWidget.prototype.onChange = function () {
7907 this.updateSearchIndicator();
7908 this.setValidityFlag();
7909 this.adjustSize();
7910 };
7911
7912 /**
7913 * Handle disable events.
7914 *
7915 * @param {boolean} disabled Element is disabled
7916 * @private
7917 */
7918 OO.ui.TextInputWidget.prototype.onDisable = function () {
7919 this.updateSearchIndicator();
7920 };
7921
7922 /**
7923 * Check if the input is {@link #readOnly read-only}.
7924 *
7925 * @return {boolean}
7926 */
7927 OO.ui.TextInputWidget.prototype.isReadOnly = function () {
7928 return this.readOnly;
7929 };
7930
7931 /**
7932 * Set the {@link #readOnly read-only} state of the input.
7933 *
7934 * @param {boolean} state Make input read-only
7935 * @chainable
7936 */
7937 OO.ui.TextInputWidget.prototype.setReadOnly = function ( state ) {
7938 this.readOnly = !!state;
7939 this.$input.prop( 'readOnly', this.readOnly );
7940 this.updateSearchIndicator();
7941 return this;
7942 };
7943
7944 /**
7945 * Support function for making #onElementAttach work across browsers.
7946 *
7947 * This whole function could be replaced with one line of code using the DOMNodeInsertedIntoDocument
7948 * event, but it's not supported by Firefox and allegedly deprecated, so we only use it as fallback.
7949 *
7950 * Due to MutationObserver performance woes, #onElementAttach is only somewhat reliably called the
7951 * first time that the element gets attached to the documented.
7952 */
7953 OO.ui.TextInputWidget.prototype.installParentChangeDetector = function () {
7954 var mutationObserver, onRemove, topmostNode, fakeParentNode,
7955 MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver,
7956 widget = this;
7957
7958 if ( MutationObserver ) {
7959 // The new way. If only it wasn't so ugly.
7960
7961 if ( this.$element.closest( 'html' ).length ) {
7962 // Widget is attached already, do nothing. This breaks the functionality of this function when
7963 // the widget is detached and reattached. Alas, doing this correctly with MutationObserver
7964 // would require observation of the whole document, which would hurt performance of other,
7965 // more important code.
7966 return;
7967 }
7968
7969 // Find topmost node in the tree
7970 topmostNode = this.$element[ 0 ];
7971 while ( topmostNode.parentNode ) {
7972 topmostNode = topmostNode.parentNode;
7973 }
7974
7975 // We have no way to detect the $element being attached somewhere without observing the entire
7976 // DOM with subtree modifications, which would hurt performance. So we cheat: we hook to the
7977 // parent node of $element, and instead detect when $element is removed from it (and thus
7978 // probably attached somewhere else). If there is no parent, we create a "fake" one. If it
7979 // doesn't get attached, we end up back here and create the parent.
7980
7981 mutationObserver = new MutationObserver( function ( mutations ) {
7982 var i, j, removedNodes;
7983 for ( i = 0; i < mutations.length; i++ ) {
7984 removedNodes = mutations[ i ].removedNodes;
7985 for ( j = 0; j < removedNodes.length; j++ ) {
7986 if ( removedNodes[ j ] === topmostNode ) {
7987 setTimeout( onRemove, 0 );
7988 return;
7989 }
7990 }
7991 }
7992 } );
7993
7994 onRemove = function () {
7995 // If the node was attached somewhere else, report it
7996 if ( widget.$element.closest( 'html' ).length ) {
7997 widget.onElementAttach();
7998 }
7999 mutationObserver.disconnect();
8000 widget.installParentChangeDetector();
8001 };
8002
8003 // Create a fake parent and observe it
8004 fakeParentNode = $( '<div>' ).append( topmostNode )[ 0 ];
8005 mutationObserver.observe( fakeParentNode, { childList: true } );
8006 } else {
8007 // Using the DOMNodeInsertedIntoDocument event is much nicer and less magical, and works for
8008 // detachment and reattachment, but it's not supported by Firefox and allegedly deprecated.
8009 this.$element.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach.bind( this ) );
8010 }
8011 };
8012
8013 /**
8014 * Automatically adjust the size of the text input.
8015 *
8016 * This only affects #multiline inputs that are {@link #autosize autosized}.
8017 *
8018 * @chainable
8019 * @fires resize
8020 */
8021 OO.ui.TextInputWidget.prototype.adjustSize = function () {
8022 var scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError,
8023 idealHeight, newHeight, scrollWidth, property;
8024
8025 if ( this.multiline && this.$input.val() !== this.valCache ) {
8026 if ( this.autosize ) {
8027 this.$clone
8028 .val( this.$input.val() )
8029 .attr( 'rows', this.minRows )
8030 // Set inline height property to 0 to measure scroll height
8031 .css( 'height', 0 );
8032
8033 this.$clone.removeClass( 'oo-ui-element-hidden' );
8034
8035 this.valCache = this.$input.val();
8036
8037 scrollHeight = this.$clone[ 0 ].scrollHeight;
8038
8039 // Remove inline height property to measure natural heights
8040 this.$clone.css( 'height', '' );
8041 innerHeight = this.$clone.innerHeight();
8042 outerHeight = this.$clone.outerHeight();
8043
8044 // Measure max rows height
8045 this.$clone
8046 .attr( 'rows', this.maxRows )
8047 .css( 'height', 'auto' )
8048 .val( '' );
8049 maxInnerHeight = this.$clone.innerHeight();
8050
8051 // Difference between reported innerHeight and scrollHeight with no scrollbars present
8052 // Equals 1 on Blink-based browsers and 0 everywhere else
8053 measurementError = maxInnerHeight - this.$clone[ 0 ].scrollHeight;
8054 idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
8055
8056 this.$clone.addClass( 'oo-ui-element-hidden' );
8057
8058 // Only apply inline height when expansion beyond natural height is needed
8059 // Use the difference between the inner and outer height as a buffer
8060 newHeight = idealHeight > innerHeight ? idealHeight + ( outerHeight - innerHeight ) : '';
8061 if ( newHeight !== this.styleHeight ) {
8062 this.$input.css( 'height', newHeight );
8063 this.styleHeight = newHeight;
8064 this.emit( 'resize' );
8065 }
8066 }
8067 scrollWidth = this.$input[ 0 ].offsetWidth - this.$input[ 0 ].clientWidth;
8068 if ( scrollWidth !== this.scrollWidth ) {
8069 property = this.$element.css( 'direction' ) === 'rtl' ? 'left' : 'right';
8070 // Reset
8071 this.$label.css( { right: '', left: '' } );
8072 this.$indicator.css( { right: '', left: '' } );
8073
8074 if ( scrollWidth ) {
8075 this.$indicator.css( property, scrollWidth );
8076 if ( this.labelPosition === 'after' ) {
8077 this.$label.css( property, scrollWidth );
8078 }
8079 }
8080
8081 this.scrollWidth = scrollWidth;
8082 this.positionLabel();
8083 }
8084 }
8085 return this;
8086 };
8087
8088 /**
8089 * @inheritdoc
8090 * @protected
8091 */
8092 OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
8093 return config.multiline ?
8094 $( '<textarea>' ) :
8095 $( '<input type="' + this.getSaneType( config ) + '" />' );
8096 };
8097
8098 /**
8099 * Get sanitized value for 'type' for given config.
8100 *
8101 * @param {Object} config Configuration options
8102 * @return {string|null}
8103 * @private
8104 */
8105 OO.ui.TextInputWidget.prototype.getSaneType = function ( config ) {
8106 var type = [ 'text', 'password', 'search', 'email', 'url' ].indexOf( config.type ) !== -1 ?
8107 config.type :
8108 'text';
8109 return config.multiline ? 'multiline' : type;
8110 };
8111
8112 /**
8113 * Check if the input supports multiple lines.
8114 *
8115 * @return {boolean}
8116 */
8117 OO.ui.TextInputWidget.prototype.isMultiline = function () {
8118 return !!this.multiline;
8119 };
8120
8121 /**
8122 * Check if the input automatically adjusts its size.
8123 *
8124 * @return {boolean}
8125 */
8126 OO.ui.TextInputWidget.prototype.isAutosizing = function () {
8127 return !!this.autosize;
8128 };
8129
8130 /**
8131 * Focus the input and select a specified range within the text.
8132 *
8133 * @param {number} from Select from offset
8134 * @param {number} [to] Select to offset, defaults to from
8135 * @chainable
8136 */
8137 OO.ui.TextInputWidget.prototype.selectRange = function ( from, to ) {
8138 var isBackwards, start, end,
8139 input = this.$input[ 0 ];
8140
8141 to = to || from;
8142
8143 isBackwards = to < from;
8144 start = isBackwards ? to : from;
8145 end = isBackwards ? from : to;
8146
8147 this.focus();
8148
8149 input.setSelectionRange( start, end, isBackwards ? 'backward' : 'forward' );
8150 return this;
8151 };
8152
8153 /**
8154 * Get an object describing the current selection range in a directional manner
8155 *
8156 * @return {Object} Object containing 'from' and 'to' offsets
8157 */
8158 OO.ui.TextInputWidget.prototype.getRange = function () {
8159 var input = this.$input[ 0 ],
8160 start = input.selectionStart,
8161 end = input.selectionEnd,
8162 isBackwards = input.selectionDirection === 'backward';
8163
8164 return {
8165 from: isBackwards ? end : start,
8166 to: isBackwards ? start : end
8167 };
8168 };
8169
8170 /**
8171 * Get the length of the text input value.
8172 *
8173 * This could differ from the length of #getValue if the
8174 * value gets filtered
8175 *
8176 * @return {number} Input length
8177 */
8178 OO.ui.TextInputWidget.prototype.getInputLength = function () {
8179 return this.$input[ 0 ].value.length;
8180 };
8181
8182 /**
8183 * Focus the input and select the entire text.
8184 *
8185 * @chainable
8186 */
8187 OO.ui.TextInputWidget.prototype.select = function () {
8188 return this.selectRange( 0, this.getInputLength() );
8189 };
8190
8191 /**
8192 * Focus the input and move the cursor to the start.
8193 *
8194 * @chainable
8195 */
8196 OO.ui.TextInputWidget.prototype.moveCursorToStart = function () {
8197 return this.selectRange( 0 );
8198 };
8199
8200 /**
8201 * Focus the input and move the cursor to the end.
8202 *
8203 * @chainable
8204 */
8205 OO.ui.TextInputWidget.prototype.moveCursorToEnd = function () {
8206 return this.selectRange( this.getInputLength() );
8207 };
8208
8209 /**
8210 * Insert new content into the input.
8211 *
8212 * @param {string} content Content to be inserted
8213 * @chainable
8214 */
8215 OO.ui.TextInputWidget.prototype.insertContent = function ( content ) {
8216 var start, end,
8217 range = this.getRange(),
8218 value = this.getValue();
8219
8220 start = Math.min( range.from, range.to );
8221 end = Math.max( range.from, range.to );
8222
8223 this.setValue( value.slice( 0, start ) + content + value.slice( end ) );
8224 this.selectRange( start + content.length );
8225 return this;
8226 };
8227
8228 /**
8229 * Insert new content either side of a selection.
8230 *
8231 * @param {string} pre Content to be inserted before the selection
8232 * @param {string} post Content to be inserted after the selection
8233 * @chainable
8234 */
8235 OO.ui.TextInputWidget.prototype.encapsulateContent = function ( pre, post ) {
8236 var start, end,
8237 range = this.getRange(),
8238 offset = pre.length;
8239
8240 start = Math.min( range.from, range.to );
8241 end = Math.max( range.from, range.to );
8242
8243 this.selectRange( start ).insertContent( pre );
8244 this.selectRange( offset + end ).insertContent( post );
8245
8246 this.selectRange( offset + start, offset + end );
8247 return this;
8248 };
8249
8250 /**
8251 * Set the validation pattern.
8252 *
8253 * The validation pattern is either a regular expression, a function, or the symbolic name of a
8254 * pattern defined by the class: 'non-empty' (the value cannot be an empty string) or 'integer' (the
8255 * value must contain only numbers).
8256 *
8257 * @param {RegExp|Function|string|null} validate Regular expression, function, or the symbolic name
8258 * of a pattern (either ‘integer’ or ‘non-empty’) defined by the class.
8259 */
8260 OO.ui.TextInputWidget.prototype.setValidation = function ( validate ) {
8261 if ( validate instanceof RegExp || validate instanceof Function ) {
8262 this.validate = validate;
8263 } else {
8264 this.validate = this.constructor.static.validationPatterns[ validate ] || /.*/;
8265 }
8266 };
8267
8268 /**
8269 * Sets the 'invalid' flag appropriately.
8270 *
8271 * @param {boolean} [isValid] Optionally override validation result
8272 */
8273 OO.ui.TextInputWidget.prototype.setValidityFlag = function ( isValid ) {
8274 var widget = this,
8275 setFlag = function ( valid ) {
8276 if ( !valid ) {
8277 widget.$input.attr( 'aria-invalid', 'true' );
8278 } else {
8279 widget.$input.removeAttr( 'aria-invalid' );
8280 }
8281 widget.setFlags( { invalid: !valid } );
8282 };
8283
8284 if ( isValid !== undefined ) {
8285 setFlag( isValid );
8286 } else {
8287 this.getValidity().then( function () {
8288 setFlag( true );
8289 }, function () {
8290 setFlag( false );
8291 } );
8292 }
8293 };
8294
8295 /**
8296 * Check if a value is valid.
8297 *
8298 * This method returns a promise that resolves with a boolean `true` if the current value is
8299 * considered valid according to the supplied {@link #validate validation pattern}.
8300 *
8301 * @deprecated
8302 * @return {jQuery.Promise} A promise that resolves to a boolean `true` if the value is valid.
8303 */
8304 OO.ui.TextInputWidget.prototype.isValid = function () {
8305 var result;
8306
8307 if ( this.validate instanceof Function ) {
8308 result = this.validate( this.getValue() );
8309 if ( result && $.isFunction( result.promise ) ) {
8310 return result.promise();
8311 } else {
8312 return $.Deferred().resolve( !!result ).promise();
8313 }
8314 } else {
8315 return $.Deferred().resolve( !!this.getValue().match( this.validate ) ).promise();
8316 }
8317 };
8318
8319 /**
8320 * Get the validity of current value.
8321 *
8322 * This method returns a promise that resolves if the value is valid and rejects if
8323 * it isn't. Uses the {@link #validate validation pattern} to check for validity.
8324 *
8325 * @return {jQuery.Promise} A promise that resolves if the value is valid, rejects if not.
8326 */
8327 OO.ui.TextInputWidget.prototype.getValidity = function () {
8328 var result;
8329
8330 function rejectOrResolve( valid ) {
8331 if ( valid ) {
8332 return $.Deferred().resolve().promise();
8333 } else {
8334 return $.Deferred().reject().promise();
8335 }
8336 }
8337
8338 if ( this.validate instanceof Function ) {
8339 result = this.validate( this.getValue() );
8340 if ( result && $.isFunction( result.promise ) ) {
8341 return result.promise().then( function ( valid ) {
8342 return rejectOrResolve( valid );
8343 } );
8344 } else {
8345 return rejectOrResolve( result );
8346 }
8347 } else {
8348 return rejectOrResolve( this.getValue().match( this.validate ) );
8349 }
8350 };
8351
8352 /**
8353 * Set the position of the inline label relative to that of the value: `‘before’` or `‘after’`.
8354 *
8355 * @param {string} labelPosition Label position, 'before' or 'after'
8356 * @chainable
8357 */
8358 OO.ui.TextInputWidget.prototype.setLabelPosition = function ( labelPosition ) {
8359 this.labelPosition = labelPosition;
8360 this.updatePosition();
8361 return this;
8362 };
8363
8364 /**
8365 * Update the position of the inline label.
8366 *
8367 * This method is called by #setLabelPosition, and can also be called on its own if
8368 * something causes the label to be mispositioned.
8369 *
8370 * @chainable
8371 */
8372 OO.ui.TextInputWidget.prototype.updatePosition = function () {
8373 var after = this.labelPosition === 'after';
8374
8375 this.$element
8376 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', !!this.label && after )
8377 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', !!this.label && !after );
8378
8379 this.valCache = null;
8380 this.scrollWidth = null;
8381 this.adjustSize();
8382 this.positionLabel();
8383
8384 return this;
8385 };
8386
8387 /**
8388 * Update the 'clear' indicator displayed on type: 'search' text fields, hiding it when the field is
8389 * already empty or when it's not editable.
8390 */
8391 OO.ui.TextInputWidget.prototype.updateSearchIndicator = function () {
8392 if ( this.type === 'search' ) {
8393 if ( this.getValue() === '' || this.isDisabled() || this.isReadOnly() ) {
8394 this.setIndicator( null );
8395 } else {
8396 this.setIndicator( 'clear' );
8397 }
8398 }
8399 };
8400
8401 /**
8402 * Position the label by setting the correct padding on the input.
8403 *
8404 * @private
8405 * @chainable
8406 */
8407 OO.ui.TextInputWidget.prototype.positionLabel = function () {
8408 var after, rtl, property;
8409 // Clear old values
8410 this.$input
8411 // Clear old values if present
8412 .css( {
8413 'padding-right': '',
8414 'padding-left': ''
8415 } );
8416
8417 if ( this.label ) {
8418 this.$element.append( this.$label );
8419 } else {
8420 this.$label.detach();
8421 return;
8422 }
8423
8424 after = this.labelPosition === 'after';
8425 rtl = this.$element.css( 'direction' ) === 'rtl';
8426 property = after === rtl ? 'padding-left' : 'padding-right';
8427
8428 this.$input.css( property, this.$label.outerWidth( true ) + ( after ? this.scrollWidth : 0 ) );
8429
8430 return this;
8431 };
8432
8433 /**
8434 * @inheritdoc
8435 */
8436 OO.ui.TextInputWidget.prototype.restorePreInfuseState = function ( state ) {
8437 OO.ui.TextInputWidget.parent.prototype.restorePreInfuseState.call( this, state );
8438 if ( state.scrollTop !== undefined ) {
8439 this.$input.scrollTop( state.scrollTop );
8440 }
8441 };
8442
8443 /**
8444 * ComboBoxInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
8445 * can be entered manually) and a {@link OO.ui.MenuSelectWidget menu of options} (from which
8446 * a value can be chosen instead). Users can choose options from the combo box in one of two ways:
8447 *
8448 * - by typing a value in the text input field. If the value exactly matches the value of a menu
8449 * option, that option will appear to be selected.
8450 * - by choosing a value from the menu. The value of the chosen option will then appear in the text
8451 * input field.
8452 *
8453 * This widget can be used inside a HTML form, such as a OO.ui.FormLayout.
8454 *
8455 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
8456 *
8457 * @example
8458 * // Example: A ComboBoxInputWidget.
8459 * var comboBox = new OO.ui.ComboBoxInputWidget( {
8460 * label: 'ComboBoxInputWidget',
8461 * value: 'Option 1',
8462 * menu: {
8463 * items: [
8464 * new OO.ui.MenuOptionWidget( {
8465 * data: 'Option 1',
8466 * label: 'Option One'
8467 * } ),
8468 * new OO.ui.MenuOptionWidget( {
8469 * data: 'Option 2',
8470 * label: 'Option Two'
8471 * } ),
8472 * new OO.ui.MenuOptionWidget( {
8473 * data: 'Option 3',
8474 * label: 'Option Three'
8475 * } ),
8476 * new OO.ui.MenuOptionWidget( {
8477 * data: 'Option 4',
8478 * label: 'Option Four'
8479 * } ),
8480 * new OO.ui.MenuOptionWidget( {
8481 * data: 'Option 5',
8482 * label: 'Option Five'
8483 * } )
8484 * ]
8485 * }
8486 * } );
8487 * $( 'body' ).append( comboBox.$element );
8488 *
8489 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
8490 *
8491 * @class
8492 * @extends OO.ui.TextInputWidget
8493 *
8494 * @constructor
8495 * @param {Object} [config] Configuration options
8496 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
8497 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.FloatingMenuSelectWidget menu select widget}.
8498 * @cfg {jQuery} [$overlay] Render the menu into a separate layer. This configuration is useful in cases where
8499 * the expanded menu is larger than its containing `<div>`. The specified overlay layer is usually on top of the
8500 * containing `<div>` and has a larger area. By default, the menu uses relative positioning.
8501 */
8502 OO.ui.ComboBoxInputWidget = function OoUiComboBoxInputWidget( config ) {
8503 // Configuration initialization
8504 config = $.extend( {
8505 indicator: 'down'
8506 }, config );
8507 // For backwards-compatibility with ComboBoxWidget config
8508 $.extend( config, config.input );
8509
8510 // Parent constructor
8511 OO.ui.ComboBoxInputWidget.parent.call( this, config );
8512
8513 // Properties
8514 this.$overlay = config.$overlay || this.$element;
8515 this.menu = new OO.ui.FloatingMenuSelectWidget( $.extend(
8516 {
8517 widget: this,
8518 input: this,
8519 $container: this.$element,
8520 disabled: this.isDisabled()
8521 },
8522 config.menu
8523 ) );
8524 // For backwards-compatibility with ComboBoxWidget
8525 this.input = this;
8526
8527 // Events
8528 this.$indicator.on( {
8529 click: this.onIndicatorClick.bind( this ),
8530 keypress: this.onIndicatorKeyPress.bind( this )
8531 } );
8532 this.connect( this, {
8533 change: 'onInputChange',
8534 enter: 'onInputEnter'
8535 } );
8536 this.menu.connect( this, {
8537 choose: 'onMenuChoose',
8538 add: 'onMenuItemsChange',
8539 remove: 'onMenuItemsChange'
8540 } );
8541
8542 // Initialization
8543 this.$input.attr( {
8544 role: 'combobox',
8545 'aria-autocomplete': 'list'
8546 } );
8547 // Do not override options set via config.menu.items
8548 if ( config.options !== undefined ) {
8549 this.setOptions( config.options );
8550 }
8551 // Extra class for backwards-compatibility with ComboBoxWidget
8552 this.$element.addClass( 'oo-ui-comboBoxInputWidget oo-ui-comboBoxWidget' );
8553 this.$overlay.append( this.menu.$element );
8554 this.onMenuItemsChange();
8555 };
8556
8557 /* Setup */
8558
8559 OO.inheritClass( OO.ui.ComboBoxInputWidget, OO.ui.TextInputWidget );
8560
8561 /* Methods */
8562
8563 /**
8564 * Get the combobox's menu.
8565 * @return {OO.ui.FloatingMenuSelectWidget} Menu widget
8566 */
8567 OO.ui.ComboBoxInputWidget.prototype.getMenu = function () {
8568 return this.menu;
8569 };
8570
8571 /**
8572 * Get the combobox's text input widget.
8573 * @return {OO.ui.TextInputWidget} Text input widget
8574 */
8575 OO.ui.ComboBoxInputWidget.prototype.getInput = function () {
8576 return this;
8577 };
8578
8579 /**
8580 * Handle input change events.
8581 *
8582 * @private
8583 * @param {string} value New value
8584 */
8585 OO.ui.ComboBoxInputWidget.prototype.onInputChange = function ( value ) {
8586 var match = this.menu.getItemFromData( value );
8587
8588 this.menu.selectItem( match );
8589 if ( this.menu.getHighlightedItem() ) {
8590 this.menu.highlightItem( match );
8591 }
8592
8593 if ( !this.isDisabled() ) {
8594 this.menu.toggle( true );
8595 }
8596 };
8597
8598 /**
8599 * Handle mouse click events.
8600 *
8601 * @private
8602 * @param {jQuery.Event} e Mouse click event
8603 */
8604 OO.ui.ComboBoxInputWidget.prototype.onIndicatorClick = function ( e ) {
8605 if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
8606 this.menu.toggle();
8607 this.$input[ 0 ].focus();
8608 }
8609 return false;
8610 };
8611
8612 /**
8613 * Handle key press events.
8614 *
8615 * @private
8616 * @param {jQuery.Event} e Key press event
8617 */
8618 OO.ui.ComboBoxInputWidget.prototype.onIndicatorKeyPress = function ( e ) {
8619 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
8620 this.menu.toggle();
8621 this.$input[ 0 ].focus();
8622 return false;
8623 }
8624 };
8625
8626 /**
8627 * Handle input enter events.
8628 *
8629 * @private
8630 */
8631 OO.ui.ComboBoxInputWidget.prototype.onInputEnter = function () {
8632 if ( !this.isDisabled() ) {
8633 this.menu.toggle( false );
8634 }
8635 };
8636
8637 /**
8638 * Handle menu choose events.
8639 *
8640 * @private
8641 * @param {OO.ui.OptionWidget} item Chosen item
8642 */
8643 OO.ui.ComboBoxInputWidget.prototype.onMenuChoose = function ( item ) {
8644 this.setValue( item.getData() );
8645 };
8646
8647 /**
8648 * Handle menu item change events.
8649 *
8650 * @private
8651 */
8652 OO.ui.ComboBoxInputWidget.prototype.onMenuItemsChange = function () {
8653 var match = this.menu.getItemFromData( this.getValue() );
8654 this.menu.selectItem( match );
8655 if ( this.menu.getHighlightedItem() ) {
8656 this.menu.highlightItem( match );
8657 }
8658 this.$element.toggleClass( 'oo-ui-comboBoxInputWidget-empty', this.menu.isEmpty() );
8659 };
8660
8661 /**
8662 * @inheritdoc
8663 */
8664 OO.ui.ComboBoxInputWidget.prototype.setDisabled = function ( disabled ) {
8665 // Parent method
8666 OO.ui.ComboBoxInputWidget.parent.prototype.setDisabled.call( this, disabled );
8667
8668 if ( this.menu ) {
8669 this.menu.setDisabled( this.isDisabled() );
8670 }
8671
8672 return this;
8673 };
8674
8675 /**
8676 * Set the options available for this input.
8677 *
8678 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
8679 * @chainable
8680 */
8681 OO.ui.ComboBoxInputWidget.prototype.setOptions = function ( options ) {
8682 this.getMenu()
8683 .clearItems()
8684 .addItems( options.map( function ( opt ) {
8685 return new OO.ui.MenuOptionWidget( {
8686 data: opt.data,
8687 label: opt.label !== undefined ? opt.label : opt.data
8688 } );
8689 } ) );
8690
8691 return this;
8692 };
8693
8694 /**
8695 * @class
8696 * @deprecated Use OO.ui.ComboBoxInputWidget instead.
8697 */
8698 OO.ui.ComboBoxWidget = OO.ui.ComboBoxInputWidget;
8699
8700 /**
8701 * FieldLayouts are used with OO.ui.FieldsetLayout. Each FieldLayout requires a field-widget,
8702 * which is a widget that is specified by reference before any optional configuration settings.
8703 *
8704 * Field layouts can be configured with help text and/or labels. Labels are aligned in one of four ways:
8705 *
8706 * - **left**: The label is placed before the field-widget and aligned with the left margin.
8707 * A left-alignment is used for forms with many fields.
8708 * - **right**: The label is placed before the field-widget and aligned to the right margin.
8709 * A right-alignment is used for long but familiar forms which users tab through,
8710 * verifying the current field with a quick glance at the label.
8711 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
8712 * that users fill out from top to bottom.
8713 * - **inline**: The label is placed after the field-widget and aligned to the left.
8714 * An inline-alignment is best used with checkboxes or radio buttons.
8715 *
8716 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout.
8717 * Please see the [OOjs UI documentation on MediaWiki] [1] for examples and more information.
8718 *
8719 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
8720 * @class
8721 * @extends OO.ui.Layout
8722 * @mixins OO.ui.mixin.LabelElement
8723 * @mixins OO.ui.mixin.TitledElement
8724 *
8725 * @constructor
8726 * @param {OO.ui.Widget} fieldWidget Field widget
8727 * @param {Object} [config] Configuration options
8728 * @cfg {string} [align='left'] Alignment of the label: 'left', 'right', 'top' or 'inline'
8729 * @cfg {Array} [errors] Error messages about the widget, which will be displayed below the widget.
8730 * The array may contain strings or OO.ui.HtmlSnippet instances.
8731 * @cfg {Array} [notices] Notices about the widget, which will be displayed below the widget.
8732 * The array may contain strings or OO.ui.HtmlSnippet instances.
8733 * @cfg {string|OO.ui.HtmlSnippet} [help] Help text. When help text is specified, a "help" icon will appear
8734 * in the upper-right corner of the rendered field; clicking it will display the text in a popup.
8735 * For important messages, you are advised to use `notices`, as they are always shown.
8736 *
8737 * @throws {Error} An error is thrown if no widget is specified
8738 */
8739 OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
8740 var hasInputWidget, div;
8741
8742 // Allow passing positional parameters inside the config object
8743 if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
8744 config = fieldWidget;
8745 fieldWidget = config.fieldWidget;
8746 }
8747
8748 // Make sure we have required constructor arguments
8749 if ( fieldWidget === undefined ) {
8750 throw new Error( 'Widget not found' );
8751 }
8752
8753 hasInputWidget = fieldWidget.constructor.static.supportsSimpleLabel;
8754
8755 // Configuration initialization
8756 config = $.extend( { align: 'left' }, config );
8757
8758 // Parent constructor
8759 OO.ui.FieldLayout.parent.call( this, config );
8760
8761 // Mixin constructors
8762 OO.ui.mixin.LabelElement.call( this, config );
8763 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
8764
8765 // Properties
8766 this.fieldWidget = fieldWidget;
8767 this.errors = [];
8768 this.notices = [];
8769 this.$field = $( '<div>' );
8770 this.$messages = $( '<ul>' );
8771 this.$body = $( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
8772 this.align = null;
8773 if ( config.help ) {
8774 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
8775 classes: [ 'oo-ui-fieldLayout-help' ],
8776 framed: false,
8777 icon: 'info'
8778 } );
8779
8780 div = $( '<div>' );
8781 if ( config.help instanceof OO.ui.HtmlSnippet ) {
8782 div.html( config.help.toString() );
8783 } else {
8784 div.text( config.help );
8785 }
8786 this.popupButtonWidget.getPopup().$body.append(
8787 div.addClass( 'oo-ui-fieldLayout-help-content' )
8788 );
8789 this.$help = this.popupButtonWidget.$element;
8790 } else {
8791 this.$help = $( [] );
8792 }
8793
8794 // Events
8795 if ( hasInputWidget ) {
8796 this.$label.on( 'click', this.onLabelClick.bind( this ) );
8797 }
8798 this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
8799
8800 // Initialization
8801 this.$element
8802 .addClass( 'oo-ui-fieldLayout' )
8803 .append( this.$help, this.$body );
8804 this.$body.addClass( 'oo-ui-fieldLayout-body' );
8805 this.$messages.addClass( 'oo-ui-fieldLayout-messages' );
8806 this.$field
8807 .addClass( 'oo-ui-fieldLayout-field' )
8808 .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget.isDisabled() )
8809 .append( this.fieldWidget.$element );
8810
8811 this.setErrors( config.errors || [] );
8812 this.setNotices( config.notices || [] );
8813 this.setAlignment( config.align );
8814 };
8815
8816 /* Setup */
8817
8818 OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
8819 OO.mixinClass( OO.ui.FieldLayout, OO.ui.mixin.LabelElement );
8820 OO.mixinClass( OO.ui.FieldLayout, OO.ui.mixin.TitledElement );
8821
8822 /* Methods */
8823
8824 /**
8825 * Handle field disable events.
8826 *
8827 * @private
8828 * @param {boolean} value Field is disabled
8829 */
8830 OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
8831 this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
8832 };
8833
8834 /**
8835 * Handle label mouse click events.
8836 *
8837 * @private
8838 * @param {jQuery.Event} e Mouse click event
8839 */
8840 OO.ui.FieldLayout.prototype.onLabelClick = function () {
8841 this.fieldWidget.simulateLabelClick();
8842 return false;
8843 };
8844
8845 /**
8846 * Get the widget contained by the field.
8847 *
8848 * @return {OO.ui.Widget} Field widget
8849 */
8850 OO.ui.FieldLayout.prototype.getField = function () {
8851 return this.fieldWidget;
8852 };
8853
8854 /**
8855 * @protected
8856 * @param {string} kind 'error' or 'notice'
8857 * @param {string|OO.ui.HtmlSnippet} text
8858 * @return {jQuery}
8859 */
8860 OO.ui.FieldLayout.prototype.makeMessage = function ( kind, text ) {
8861 var $listItem, $icon, message;
8862 $listItem = $( '<li>' );
8863 if ( kind === 'error' ) {
8864 $icon = new OO.ui.IconWidget( { icon: 'alert', flags: [ 'warning' ] } ).$element;
8865 } else if ( kind === 'notice' ) {
8866 $icon = new OO.ui.IconWidget( { icon: 'info' } ).$element;
8867 } else {
8868 $icon = '';
8869 }
8870 message = new OO.ui.LabelWidget( { label: text } );
8871 $listItem
8872 .append( $icon, message.$element )
8873 .addClass( 'oo-ui-fieldLayout-messages-' + kind );
8874 return $listItem;
8875 };
8876
8877 /**
8878 * Set the field alignment mode.
8879 *
8880 * @private
8881 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
8882 * @chainable
8883 */
8884 OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
8885 if ( value !== this.align ) {
8886 // Default to 'left'
8887 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) {
8888 value = 'left';
8889 }
8890 // Reorder elements
8891 if ( value === 'inline' ) {
8892 this.$body.append( this.$field, this.$label );
8893 } else {
8894 this.$body.append( this.$label, this.$field );
8895 }
8896 // Set classes. The following classes can be used here:
8897 // * oo-ui-fieldLayout-align-left
8898 // * oo-ui-fieldLayout-align-right
8899 // * oo-ui-fieldLayout-align-top
8900 // * oo-ui-fieldLayout-align-inline
8901 if ( this.align ) {
8902 this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
8903 }
8904 this.$element.addClass( 'oo-ui-fieldLayout-align-' + value );
8905 this.align = value;
8906 }
8907
8908 return this;
8909 };
8910
8911 /**
8912 * Set the list of error messages.
8913 *
8914 * @param {Array} errors Error messages about the widget, which will be displayed below the widget.
8915 * The array may contain strings or OO.ui.HtmlSnippet instances.
8916 * @chainable
8917 */
8918 OO.ui.FieldLayout.prototype.setErrors = function ( errors ) {
8919 this.errors = errors.slice();
8920 this.updateMessages();
8921 return this;
8922 };
8923
8924 /**
8925 * Set the list of notice messages.
8926 *
8927 * @param {Array} notices Notices about the widget, which will be displayed below the widget.
8928 * The array may contain strings or OO.ui.HtmlSnippet instances.
8929 * @chainable
8930 */
8931 OO.ui.FieldLayout.prototype.setNotices = function ( notices ) {
8932 this.notices = notices.slice();
8933 this.updateMessages();
8934 return this;
8935 };
8936
8937 /**
8938 * Update the rendering of error and notice messages.
8939 *
8940 * @private
8941 */
8942 OO.ui.FieldLayout.prototype.updateMessages = function () {
8943 var i;
8944 this.$messages.empty();
8945
8946 if ( this.errors.length || this.notices.length ) {
8947 this.$body.after( this.$messages );
8948 } else {
8949 this.$messages.remove();
8950 return;
8951 }
8952
8953 for ( i = 0; i < this.notices.length; i++ ) {
8954 this.$messages.append( this.makeMessage( 'notice', this.notices[ i ] ) );
8955 }
8956 for ( i = 0; i < this.errors.length; i++ ) {
8957 this.$messages.append( this.makeMessage( 'error', this.errors[ i ] ) );
8958 }
8959 };
8960
8961 /**
8962 * ActionFieldLayouts are used with OO.ui.FieldsetLayout. The layout consists of a field-widget, a button,
8963 * and an optional label and/or help text. The field-widget (e.g., a {@link OO.ui.TextInputWidget TextInputWidget}),
8964 * is required and is specified before any optional configuration settings.
8965 *
8966 * Labels can be aligned in one of four ways:
8967 *
8968 * - **left**: The label is placed before the field-widget and aligned with the left margin.
8969 * A left-alignment is used for forms with many fields.
8970 * - **right**: The label is placed before the field-widget and aligned to the right margin.
8971 * A right-alignment is used for long but familiar forms which users tab through,
8972 * verifying the current field with a quick glance at the label.
8973 * - **top**: The label is placed above the field-widget. A top-alignment is used for brief forms
8974 * that users fill out from top to bottom.
8975 * - **inline**: The label is placed after the field-widget and aligned to the left.
8976 * An inline-alignment is best used with checkboxes or radio buttons.
8977 *
8978 * Help text is accessed via a help icon that appears in the upper right corner of the rendered field layout when help
8979 * text is specified.
8980 *
8981 * @example
8982 * // Example of an ActionFieldLayout
8983 * var actionFieldLayout = new OO.ui.ActionFieldLayout(
8984 * new OO.ui.TextInputWidget( {
8985 * placeholder: 'Field widget'
8986 * } ),
8987 * new OO.ui.ButtonWidget( {
8988 * label: 'Button'
8989 * } ),
8990 * {
8991 * label: 'An ActionFieldLayout. This label is aligned top',
8992 * align: 'top',
8993 * help: 'This is help text'
8994 * }
8995 * );
8996 *
8997 * $( 'body' ).append( actionFieldLayout.$element );
8998 *
8999 * @class
9000 * @extends OO.ui.FieldLayout
9001 *
9002 * @constructor
9003 * @param {OO.ui.Widget} fieldWidget Field widget
9004 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
9005 */
9006 OO.ui.ActionFieldLayout = function OoUiActionFieldLayout( fieldWidget, buttonWidget, config ) {
9007 // Allow passing positional parameters inside the config object
9008 if ( OO.isPlainObject( fieldWidget ) && config === undefined ) {
9009 config = fieldWidget;
9010 fieldWidget = config.fieldWidget;
9011 buttonWidget = config.buttonWidget;
9012 }
9013
9014 // Parent constructor
9015 OO.ui.ActionFieldLayout.parent.call( this, fieldWidget, config );
9016
9017 // Properties
9018 this.buttonWidget = buttonWidget;
9019 this.$button = $( '<div>' );
9020 this.$input = $( '<div>' );
9021
9022 // Initialization
9023 this.$element
9024 .addClass( 'oo-ui-actionFieldLayout' );
9025 this.$button
9026 .addClass( 'oo-ui-actionFieldLayout-button' )
9027 .append( this.buttonWidget.$element );
9028 this.$input
9029 .addClass( 'oo-ui-actionFieldLayout-input' )
9030 .append( this.fieldWidget.$element );
9031 this.$field
9032 .append( this.$input, this.$button );
9033 };
9034
9035 /* Setup */
9036
9037 OO.inheritClass( OO.ui.ActionFieldLayout, OO.ui.FieldLayout );
9038
9039 /**
9040 * FieldsetLayouts are composed of one or more {@link OO.ui.FieldLayout FieldLayouts},
9041 * which each contain an individual widget and, optionally, a label. Each Fieldset can be
9042 * configured with a label as well. For more information and examples,
9043 * please see the [OOjs UI documentation on MediaWiki][1].
9044 *
9045 * @example
9046 * // Example of a fieldset layout
9047 * var input1 = new OO.ui.TextInputWidget( {
9048 * placeholder: 'A text input field'
9049 * } );
9050 *
9051 * var input2 = new OO.ui.TextInputWidget( {
9052 * placeholder: 'A text input field'
9053 * } );
9054 *
9055 * var fieldset = new OO.ui.FieldsetLayout( {
9056 * label: 'Example of a fieldset layout'
9057 * } );
9058 *
9059 * fieldset.addItems( [
9060 * new OO.ui.FieldLayout( input1, {
9061 * label: 'Field One'
9062 * } ),
9063 * new OO.ui.FieldLayout( input2, {
9064 * label: 'Field Two'
9065 * } )
9066 * ] );
9067 * $( 'body' ).append( fieldset.$element );
9068 *
9069 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Fields_and_Fieldsets
9070 *
9071 * @class
9072 * @extends OO.ui.Layout
9073 * @mixins OO.ui.mixin.IconElement
9074 * @mixins OO.ui.mixin.LabelElement
9075 * @mixins OO.ui.mixin.GroupElement
9076 *
9077 * @constructor
9078 * @param {Object} [config] Configuration options
9079 * @cfg {OO.ui.FieldLayout[]} [items] An array of fields to add to the fieldset. See OO.ui.FieldLayout for more information about fields.
9080 */
9081 OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
9082 // Configuration initialization
9083 config = config || {};
9084
9085 // Parent constructor
9086 OO.ui.FieldsetLayout.parent.call( this, config );
9087
9088 // Mixin constructors
9089 OO.ui.mixin.IconElement.call( this, config );
9090 OO.ui.mixin.LabelElement.call( this, config );
9091 OO.ui.mixin.GroupElement.call( this, config );
9092
9093 if ( config.help ) {
9094 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
9095 classes: [ 'oo-ui-fieldsetLayout-help' ],
9096 framed: false,
9097 icon: 'info'
9098 } );
9099
9100 this.popupButtonWidget.getPopup().$body.append(
9101 $( '<div>' )
9102 .text( config.help )
9103 .addClass( 'oo-ui-fieldsetLayout-help-content' )
9104 );
9105 this.$help = this.popupButtonWidget.$element;
9106 } else {
9107 this.$help = $( [] );
9108 }
9109
9110 // Initialization
9111 this.$element
9112 .addClass( 'oo-ui-fieldsetLayout' )
9113 .prepend( this.$help, this.$icon, this.$label, this.$group );
9114 if ( Array.isArray( config.items ) ) {
9115 this.addItems( config.items );
9116 }
9117 };
9118
9119 /* Setup */
9120
9121 OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
9122 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.IconElement );
9123 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.LabelElement );
9124 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.mixin.GroupElement );
9125
9126 /**
9127 * FormLayouts are used to wrap {@link OO.ui.FieldsetLayout FieldsetLayouts} when you intend to use browser-based
9128 * form submission for the fields instead of handling them in JavaScript. Form layouts can be configured with an
9129 * HTML form action, an encoding type, and a method using the #action, #enctype, and #method configs, respectively.
9130 * See the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
9131 *
9132 * Only widgets from the {@link OO.ui.InputWidget InputWidget} family support form submission. It
9133 * includes standard form elements like {@link OO.ui.CheckboxInputWidget checkboxes}, {@link
9134 * OO.ui.RadioInputWidget radio buttons} and {@link OO.ui.TextInputWidget text fields}, as well as
9135 * some fancier controls. Some controls have both regular and InputWidget variants, for example
9136 * OO.ui.DropdownWidget and OO.ui.DropdownInputWidget – only the latter support form submission and
9137 * often have simplified APIs to match the capabilities of HTML forms.
9138 * See the [OOjs UI Inputs documentation on MediaWiki] [2] for more information about InputWidgets.
9139 *
9140 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Layouts/Forms
9141 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Inputs
9142 *
9143 * @example
9144 * // Example of a form layout that wraps a fieldset layout
9145 * var input1 = new OO.ui.TextInputWidget( {
9146 * placeholder: 'Username'
9147 * } );
9148 * var input2 = new OO.ui.TextInputWidget( {
9149 * placeholder: 'Password',
9150 * type: 'password'
9151 * } );
9152 * var submit = new OO.ui.ButtonInputWidget( {
9153 * label: 'Submit'
9154 * } );
9155 *
9156 * var fieldset = new OO.ui.FieldsetLayout( {
9157 * label: 'A form layout'
9158 * } );
9159 * fieldset.addItems( [
9160 * new OO.ui.FieldLayout( input1, {
9161 * label: 'Username',
9162 * align: 'top'
9163 * } ),
9164 * new OO.ui.FieldLayout( input2, {
9165 * label: 'Password',
9166 * align: 'top'
9167 * } ),
9168 * new OO.ui.FieldLayout( submit )
9169 * ] );
9170 * var form = new OO.ui.FormLayout( {
9171 * items: [ fieldset ],
9172 * action: '/api/formhandler',
9173 * method: 'get'
9174 * } )
9175 * $( 'body' ).append( form.$element );
9176 *
9177 * @class
9178 * @extends OO.ui.Layout
9179 * @mixins OO.ui.mixin.GroupElement
9180 *
9181 * @constructor
9182 * @param {Object} [config] Configuration options
9183 * @cfg {string} [method] HTML form `method` attribute
9184 * @cfg {string} [action] HTML form `action` attribute
9185 * @cfg {string} [enctype] HTML form `enctype` attribute
9186 * @cfg {OO.ui.FieldsetLayout[]} [items] Fieldset layouts to add to the form layout.
9187 */
9188 OO.ui.FormLayout = function OoUiFormLayout( config ) {
9189 var action;
9190
9191 // Configuration initialization
9192 config = config || {};
9193
9194 // Parent constructor
9195 OO.ui.FormLayout.parent.call( this, config );
9196
9197 // Mixin constructors
9198 OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
9199
9200 // Events
9201 this.$element.on( 'submit', this.onFormSubmit.bind( this ) );
9202
9203 // Make sure the action is safe
9204 action = config.action;
9205 if ( action !== undefined && !OO.ui.isSafeUrl( action ) ) {
9206 action = './' + action;
9207 }
9208
9209 // Initialization
9210 this.$element
9211 .addClass( 'oo-ui-formLayout' )
9212 .attr( {
9213 method: config.method,
9214 action: action,
9215 enctype: config.enctype
9216 } );
9217 if ( Array.isArray( config.items ) ) {
9218 this.addItems( config.items );
9219 }
9220 };
9221
9222 /* Setup */
9223
9224 OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout );
9225 OO.mixinClass( OO.ui.FormLayout, OO.ui.mixin.GroupElement );
9226
9227 /* Events */
9228
9229 /**
9230 * A 'submit' event is emitted when the form is submitted.
9231 *
9232 * @event submit
9233 */
9234
9235 /* Static Properties */
9236
9237 OO.ui.FormLayout.static.tagName = 'form';
9238
9239 /* Methods */
9240
9241 /**
9242 * Handle form submit events.
9243 *
9244 * @private
9245 * @param {jQuery.Event} e Submit event
9246 * @fires submit
9247 */
9248 OO.ui.FormLayout.prototype.onFormSubmit = function () {
9249 if ( this.emit( 'submit' ) ) {
9250 return false;
9251 }
9252 };
9253
9254 /**
9255 * PanelLayouts expand to cover the entire area of their parent. They can be configured with scrolling, padding,
9256 * and a frame, and are often used together with {@link OO.ui.StackLayout StackLayouts}.
9257 *
9258 * @example
9259 * // Example of a panel layout
9260 * var panel = new OO.ui.PanelLayout( {
9261 * expanded: false,
9262 * framed: true,
9263 * padded: true,
9264 * $content: $( '<p>A panel layout with padding and a frame.</p>' )
9265 * } );
9266 * $( 'body' ).append( panel.$element );
9267 *
9268 * @class
9269 * @extends OO.ui.Layout
9270 *
9271 * @constructor
9272 * @param {Object} [config] Configuration options
9273 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
9274 * @cfg {boolean} [padded=false] Add padding between the content and the edges of the panel.
9275 * @cfg {boolean} [expanded=true] Expand the panel to fill the entire parent element.
9276 * @cfg {boolean} [framed=false] Render the panel with a frame to visually separate it from outside content.
9277 */
9278 OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
9279 // Configuration initialization
9280 config = $.extend( {
9281 scrollable: false,
9282 padded: false,
9283 expanded: true,
9284 framed: false
9285 }, config );
9286
9287 // Parent constructor
9288 OO.ui.PanelLayout.parent.call( this, config );
9289
9290 // Initialization
9291 this.$element.addClass( 'oo-ui-panelLayout' );
9292 if ( config.scrollable ) {
9293 this.$element.addClass( 'oo-ui-panelLayout-scrollable' );
9294 }
9295 if ( config.padded ) {
9296 this.$element.addClass( 'oo-ui-panelLayout-padded' );
9297 }
9298 if ( config.expanded ) {
9299 this.$element.addClass( 'oo-ui-panelLayout-expanded' );
9300 }
9301 if ( config.framed ) {
9302 this.$element.addClass( 'oo-ui-panelLayout-framed' );
9303 }
9304 };
9305
9306 /* Setup */
9307
9308 OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
9309
9310 /* Methods */
9311
9312 /**
9313 * Focus the panel layout
9314 *
9315 * The default implementation just focuses the first focusable element in the panel
9316 */
9317 OO.ui.PanelLayout.prototype.focus = function () {
9318 OO.ui.findFocusable( this.$element ).focus();
9319 };
9320
9321 /**
9322 * HorizontalLayout arranges its contents in a single line (using `display: inline-block` for its
9323 * items), with small margins between them. Convenient when you need to put a number of block-level
9324 * widgets on a single line next to each other.
9325 *
9326 * Note that inline elements, such as OO.ui.ButtonWidgets, do not need this wrapper.
9327 *
9328 * @example
9329 * // HorizontalLayout with a text input and a label
9330 * var layout = new OO.ui.HorizontalLayout( {
9331 * items: [
9332 * new OO.ui.LabelWidget( { label: 'Label' } ),
9333 * new OO.ui.TextInputWidget( { value: 'Text' } )
9334 * ]
9335 * } );
9336 * $( 'body' ).append( layout.$element );
9337 *
9338 * @class
9339 * @extends OO.ui.Layout
9340 * @mixins OO.ui.mixin.GroupElement
9341 *
9342 * @constructor
9343 * @param {Object} [config] Configuration options
9344 * @cfg {OO.ui.Widget[]|OO.ui.Layout[]} [items] Widgets or other layouts to add to the layout.
9345 */
9346 OO.ui.HorizontalLayout = function OoUiHorizontalLayout( config ) {
9347 // Configuration initialization
9348 config = config || {};
9349
9350 // Parent constructor
9351 OO.ui.HorizontalLayout.parent.call( this, config );
9352
9353 // Mixin constructors
9354 OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
9355
9356 // Initialization
9357 this.$element.addClass( 'oo-ui-horizontalLayout' );
9358 if ( Array.isArray( config.items ) ) {
9359 this.addItems( config.items );
9360 }
9361 };
9362
9363 /* Setup */
9364
9365 OO.inheritClass( OO.ui.HorizontalLayout, OO.ui.Layout );
9366 OO.mixinClass( OO.ui.HorizontalLayout, OO.ui.mixin.GroupElement );
9367
9368 }( OO ) );
9369
9370 /*!
9371 * OOjs UI v0.15.2
9372 * https://www.mediawiki.org/wiki/OOjs_UI
9373 *
9374 * Copyright 2011–2016 OOjs UI Team and other contributors.
9375 * Released under the MIT license
9376 * http://oojs.mit-license.org
9377 *
9378 * Date: 2016-02-02T22:07:00Z
9379 */
9380 ( function ( OO ) {
9381
9382 'use strict';
9383
9384 /**
9385 * DraggableElement is a mixin class used to create elements that can be clicked
9386 * and dragged by a mouse to a new position within a group. This class must be used
9387 * in conjunction with OO.ui.mixin.DraggableGroupElement, which provides a container for
9388 * the draggable elements.
9389 *
9390 * @abstract
9391 * @class
9392 *
9393 * @constructor
9394 */
9395 OO.ui.mixin.DraggableElement = function OoUiMixinDraggableElement() {
9396 // Properties
9397 this.index = null;
9398
9399 // Initialize and events
9400 this.$element
9401 .attr( 'draggable', true )
9402 .addClass( 'oo-ui-draggableElement' )
9403 .on( {
9404 dragstart: this.onDragStart.bind( this ),
9405 dragover: this.onDragOver.bind( this ),
9406 dragend: this.onDragEnd.bind( this ),
9407 drop: this.onDrop.bind( this )
9408 } );
9409 };
9410
9411 OO.initClass( OO.ui.mixin.DraggableElement );
9412
9413 /* Events */
9414
9415 /**
9416 * @event dragstart
9417 *
9418 * A dragstart event is emitted when the user clicks and begins dragging an item.
9419 * @param {OO.ui.mixin.DraggableElement} item The item the user has clicked and is dragging with the mouse.
9420 */
9421
9422 /**
9423 * @event dragend
9424 * A dragend event is emitted when the user drags an item and releases the mouse,
9425 * thus terminating the drag operation.
9426 */
9427
9428 /**
9429 * @event drop
9430 * A drop event is emitted when the user drags an item and then releases the mouse button
9431 * over a valid target.
9432 */
9433
9434 /* Static Properties */
9435
9436 /**
9437 * @inheritdoc OO.ui.mixin.ButtonElement
9438 */
9439 OO.ui.mixin.DraggableElement.static.cancelButtonMouseDownEvents = false;
9440
9441 /* Methods */
9442
9443 /**
9444 * Respond to dragstart event.
9445 *
9446 * @private
9447 * @param {jQuery.Event} event jQuery event
9448 * @fires dragstart
9449 */
9450 OO.ui.mixin.DraggableElement.prototype.onDragStart = function ( e ) {
9451 var dataTransfer = e.originalEvent.dataTransfer;
9452 // Define drop effect
9453 dataTransfer.dropEffect = 'none';
9454 dataTransfer.effectAllowed = 'move';
9455 // Support: Firefox
9456 // We must set up a dataTransfer data property or Firefox seems to
9457 // ignore the fact the element is draggable.
9458 try {
9459 dataTransfer.setData( 'application-x/OOjs-UI-draggable', this.getIndex() );
9460 } catch ( err ) {
9461 // The above is only for Firefox. Move on if it fails.
9462 }
9463 // Add dragging class
9464 this.$element.addClass( 'oo-ui-draggableElement-dragging' );
9465 // Emit event
9466 this.emit( 'dragstart', this );
9467 return true;
9468 };
9469
9470 /**
9471 * Respond to dragend event.
9472 *
9473 * @private
9474 * @fires dragend
9475 */
9476 OO.ui.mixin.DraggableElement.prototype.onDragEnd = function () {
9477 this.$element.removeClass( 'oo-ui-draggableElement-dragging' );
9478 this.emit( 'dragend' );
9479 };
9480
9481 /**
9482 * Handle drop event.
9483 *
9484 * @private
9485 * @param {jQuery.Event} event jQuery event
9486 * @fires drop
9487 */
9488 OO.ui.mixin.DraggableElement.prototype.onDrop = function ( e ) {
9489 e.preventDefault();
9490 this.emit( 'drop', e );
9491 };
9492
9493 /**
9494 * In order for drag/drop to work, the dragover event must
9495 * return false and stop propogation.
9496 *
9497 * @private
9498 */
9499 OO.ui.mixin.DraggableElement.prototype.onDragOver = function ( e ) {
9500 e.preventDefault();
9501 };
9502
9503 /**
9504 * Set item index.
9505 * Store it in the DOM so we can access from the widget drag event
9506 *
9507 * @private
9508 * @param {number} Item index
9509 */
9510 OO.ui.mixin.DraggableElement.prototype.setIndex = function ( index ) {
9511 if ( this.index !== index ) {
9512 this.index = index;
9513 this.$element.data( 'index', index );
9514 }
9515 };
9516
9517 /**
9518 * Get item index
9519 *
9520 * @private
9521 * @return {number} Item index
9522 */
9523 OO.ui.mixin.DraggableElement.prototype.getIndex = function () {
9524 return this.index;
9525 };
9526
9527 /**
9528 * DraggableGroupElement is a mixin class used to create a group element to
9529 * contain draggable elements, which are items that can be clicked and dragged by a mouse.
9530 * The class is used with OO.ui.mixin.DraggableElement.
9531 *
9532 * @abstract
9533 * @class
9534 * @mixins OO.ui.mixin.GroupElement
9535 *
9536 * @constructor
9537 * @param {Object} [config] Configuration options
9538 * @cfg {string} [orientation] Item orientation: 'horizontal' or 'vertical'. The orientation
9539 * should match the layout of the items. Items displayed in a single row
9540 * or in several rows should use horizontal orientation. The vertical orientation should only be
9541 * used when the items are displayed in a single column. Defaults to 'vertical'
9542 */
9543 OO.ui.mixin.DraggableGroupElement = function OoUiMixinDraggableGroupElement( config ) {
9544 // Configuration initialization
9545 config = config || {};
9546
9547 // Parent constructor
9548 OO.ui.mixin.GroupElement.call( this, config );
9549
9550 // Properties
9551 this.orientation = config.orientation || 'vertical';
9552 this.dragItem = null;
9553 this.itemDragOver = null;
9554 this.itemKeys = {};
9555 this.sideInsertion = '';
9556
9557 // Events
9558 this.aggregate( {
9559 dragstart: 'itemDragStart',
9560 dragend: 'itemDragEnd',
9561 drop: 'itemDrop'
9562 } );
9563 this.connect( this, {
9564 itemDragStart: 'onItemDragStart',
9565 itemDrop: 'onItemDrop',
9566 itemDragEnd: 'onItemDragEnd'
9567 } );
9568 this.$element.on( {
9569 dragover: this.onDragOver.bind( this ),
9570 dragleave: this.onDragLeave.bind( this )
9571 } );
9572
9573 // Initialize
9574 if ( Array.isArray( config.items ) ) {
9575 this.addItems( config.items );
9576 }
9577 this.$placeholder = $( '<div>' )
9578 .addClass( 'oo-ui-draggableGroupElement-placeholder' );
9579 this.$element
9580 .addClass( 'oo-ui-draggableGroupElement' )
9581 .append( this.$status )
9582 .toggleClass( 'oo-ui-draggableGroupElement-horizontal', this.orientation === 'horizontal' )
9583 .prepend( this.$placeholder );
9584 };
9585
9586 /* Setup */
9587 OO.mixinClass( OO.ui.mixin.DraggableGroupElement, OO.ui.mixin.GroupElement );
9588
9589 /* Events */
9590
9591 /**
9592 * A 'reorder' event is emitted when the order of items in the group changes.
9593 *
9594 * @event reorder
9595 * @param {OO.ui.mixin.DraggableElement} item Reordered item
9596 * @param {number} [newIndex] New index for the item
9597 */
9598
9599 /* Methods */
9600
9601 /**
9602 * Respond to item drag start event
9603 *
9604 * @private
9605 * @param {OO.ui.mixin.DraggableElement} item Dragged item
9606 */
9607 OO.ui.mixin.DraggableGroupElement.prototype.onItemDragStart = function ( item ) {
9608 var i, len;
9609
9610 // Map the index of each object
9611 for ( i = 0, len = this.items.length; i < len; i++ ) {
9612 this.items[ i ].setIndex( i );
9613 }
9614
9615 if ( this.orientation === 'horizontal' ) {
9616 // Set the height of the indicator
9617 this.$placeholder.css( {
9618 height: item.$element.outerHeight(),
9619 width: 2
9620 } );
9621 } else {
9622 // Set the width of the indicator
9623 this.$placeholder.css( {
9624 height: 2,
9625 width: item.$element.outerWidth()
9626 } );
9627 }
9628 this.setDragItem( item );
9629 };
9630
9631 /**
9632 * Respond to item drag end event
9633 *
9634 * @private
9635 */
9636 OO.ui.mixin.DraggableGroupElement.prototype.onItemDragEnd = function () {
9637 this.unsetDragItem();
9638 return false;
9639 };
9640
9641 /**
9642 * Handle drop event and switch the order of the items accordingly
9643 *
9644 * @private
9645 * @param {OO.ui.mixin.DraggableElement} item Dropped item
9646 * @fires reorder
9647 */
9648 OO.ui.mixin.DraggableGroupElement.prototype.onItemDrop = function ( item ) {
9649 var toIndex = item.getIndex();
9650 // Check if the dropped item is from the current group
9651 // TODO: Figure out a way to configure a list of legally droppable
9652 // elements even if they are not yet in the list
9653 if ( this.getDragItem() ) {
9654 // If the insertion point is 'after', the insertion index
9655 // is shifted to the right (or to the left in RTL, hence 'after')
9656 if ( this.sideInsertion === 'after' ) {
9657 toIndex++;
9658 }
9659 // Emit change event
9660 this.emit( 'reorder', this.getDragItem(), toIndex );
9661 }
9662 this.unsetDragItem();
9663 // Return false to prevent propogation
9664 return false;
9665 };
9666
9667 /**
9668 * Handle dragleave event.
9669 *
9670 * @private
9671 */
9672 OO.ui.mixin.DraggableGroupElement.prototype.onDragLeave = function () {
9673 // This means the item was dragged outside the widget
9674 this.$placeholder
9675 .css( 'left', 0 )
9676 .addClass( 'oo-ui-element-hidden' );
9677 };
9678
9679 /**
9680 * Respond to dragover event
9681 *
9682 * @private
9683 * @param {jQuery.Event} event Event details
9684 */
9685 OO.ui.mixin.DraggableGroupElement.prototype.onDragOver = function ( e ) {
9686 var dragOverObj, $optionWidget, itemOffset, itemMidpoint, itemBoundingRect,
9687 itemSize, cssOutput, dragPosition, itemIndex, itemPosition,
9688 clientX = e.originalEvent.clientX,
9689 clientY = e.originalEvent.clientY;
9690
9691 // Get the OptionWidget item we are dragging over
9692 dragOverObj = this.getElementDocument().elementFromPoint( clientX, clientY );
9693 $optionWidget = $( dragOverObj ).closest( '.oo-ui-draggableElement' );
9694 if ( $optionWidget[ 0 ] ) {
9695 itemOffset = $optionWidget.offset();
9696 itemBoundingRect = $optionWidget[ 0 ].getBoundingClientRect();
9697 itemPosition = $optionWidget.position();
9698 itemIndex = $optionWidget.data( 'index' );
9699 }
9700
9701 if (
9702 itemOffset &&
9703 this.isDragging() &&
9704 itemIndex !== this.getDragItem().getIndex()
9705 ) {
9706 if ( this.orientation === 'horizontal' ) {
9707 // Calculate where the mouse is relative to the item width
9708 itemSize = itemBoundingRect.width;
9709 itemMidpoint = itemBoundingRect.left + itemSize / 2;
9710 dragPosition = clientX;
9711 // Which side of the item we hover over will dictate
9712 // where the placeholder will appear, on the left or
9713 // on the right
9714 cssOutput = {
9715 left: dragPosition < itemMidpoint ? itemPosition.left : itemPosition.left + itemSize,
9716 top: itemPosition.top
9717 };
9718 } else {
9719 // Calculate where the mouse is relative to the item height
9720 itemSize = itemBoundingRect.height;
9721 itemMidpoint = itemBoundingRect.top + itemSize / 2;
9722 dragPosition = clientY;
9723 // Which side of the item we hover over will dictate
9724 // where the placeholder will appear, on the top or
9725 // on the bottom
9726 cssOutput = {
9727 top: dragPosition < itemMidpoint ? itemPosition.top : itemPosition.top + itemSize,
9728 left: itemPosition.left
9729 };
9730 }
9731 // Store whether we are before or after an item to rearrange
9732 // For horizontal layout, we need to account for RTL, as this is flipped
9733 if ( this.orientation === 'horizontal' && this.$element.css( 'direction' ) === 'rtl' ) {
9734 this.sideInsertion = dragPosition < itemMidpoint ? 'after' : 'before';
9735 } else {
9736 this.sideInsertion = dragPosition < itemMidpoint ? 'before' : 'after';
9737 }
9738 // Add drop indicator between objects
9739 this.$placeholder
9740 .css( cssOutput )
9741 .removeClass( 'oo-ui-element-hidden' );
9742 } else {
9743 // This means the item was dragged outside the widget
9744 this.$placeholder
9745 .css( 'left', 0 )
9746 .addClass( 'oo-ui-element-hidden' );
9747 }
9748 // Prevent default
9749 e.preventDefault();
9750 };
9751
9752 /**
9753 * Set a dragged item
9754 *
9755 * @param {OO.ui.mixin.DraggableElement} item Dragged item
9756 */
9757 OO.ui.mixin.DraggableGroupElement.prototype.setDragItem = function ( item ) {
9758 this.dragItem = item;
9759 };
9760
9761 /**
9762 * Unset the current dragged item
9763 */
9764 OO.ui.mixin.DraggableGroupElement.prototype.unsetDragItem = function () {
9765 this.dragItem = null;
9766 this.itemDragOver = null;
9767 this.$placeholder.addClass( 'oo-ui-element-hidden' );
9768 this.sideInsertion = '';
9769 };
9770
9771 /**
9772 * Get the item that is currently being dragged.
9773 *
9774 * @return {OO.ui.mixin.DraggableElement|null} The currently dragged item, or `null` if no item is being dragged
9775 */
9776 OO.ui.mixin.DraggableGroupElement.prototype.getDragItem = function () {
9777 return this.dragItem;
9778 };
9779
9780 /**
9781 * Check if an item in the group is currently being dragged.
9782 *
9783 * @return {Boolean} Item is being dragged
9784 */
9785 OO.ui.mixin.DraggableGroupElement.prototype.isDragging = function () {
9786 return this.getDragItem() !== null;
9787 };
9788
9789 /**
9790 * RequestManager is a mixin that manages the lifecycle of a promise-backed request for a widget, such as
9791 * the {@link OO.ui.mixin.LookupElement}.
9792 *
9793 * @class
9794 * @abstract
9795 *
9796 * @constructor
9797 */
9798 OO.ui.mixin.RequestManager = function OoUiMixinRequestManager() {
9799 this.requestCache = {};
9800 this.requestQuery = null;
9801 this.requestRequest = null;
9802 };
9803
9804 /* Setup */
9805
9806 OO.initClass( OO.ui.mixin.RequestManager );
9807
9808 /**
9809 * Get request results for the current query.
9810 *
9811 * @return {jQuery.Promise} Promise object which will be passed response data as the first argument of
9812 * the done event. If the request was aborted to make way for a subsequent request, this promise
9813 * may not be rejected, depending on what jQuery feels like doing.
9814 */
9815 OO.ui.mixin.RequestManager.prototype.getRequestData = function () {
9816 var widget = this,
9817 value = this.getRequestQuery(),
9818 deferred = $.Deferred(),
9819 ourRequest;
9820
9821 this.abortRequest();
9822 if ( Object.prototype.hasOwnProperty.call( this.requestCache, value ) ) {
9823 deferred.resolve( this.requestCache[ value ] );
9824 } else {
9825 if ( this.pushPending ) {
9826 this.pushPending();
9827 }
9828 this.requestQuery = value;
9829 ourRequest = this.requestRequest = this.getRequest();
9830 ourRequest
9831 .always( function () {
9832 // We need to pop pending even if this is an old request, otherwise
9833 // the widget will remain pending forever.
9834 // TODO: this assumes that an aborted request will fail or succeed soon after
9835 // being aborted, or at least eventually. It would be nice if we could popPending()
9836 // at abort time, but only if we knew that we hadn't already called popPending()
9837 // for that request.
9838 if ( widget.popPending ) {
9839 widget.popPending();
9840 }
9841 } )
9842 .done( function ( response ) {
9843 // If this is an old request (and aborting it somehow caused it to still succeed),
9844 // ignore its success completely
9845 if ( ourRequest === widget.requestRequest ) {
9846 widget.requestQuery = null;
9847 widget.requestRequest = null;
9848 widget.requestCache[ value ] = widget.getRequestCacheDataFromResponse( response );
9849 deferred.resolve( widget.requestCache[ value ] );
9850 }
9851 } )
9852 .fail( function () {
9853 // If this is an old request (or a request failing because it's being aborted),
9854 // ignore its failure completely
9855 if ( ourRequest === widget.requestRequest ) {
9856 widget.requestQuery = null;
9857 widget.requestRequest = null;
9858 deferred.reject();
9859 }
9860 } );
9861 }
9862 return deferred.promise();
9863 };
9864
9865 /**
9866 * Abort the currently pending request, if any.
9867 *
9868 * @private
9869 */
9870 OO.ui.mixin.RequestManager.prototype.abortRequest = function () {
9871 var oldRequest = this.requestRequest;
9872 if ( oldRequest ) {
9873 // First unset this.requestRequest to the fail handler will notice
9874 // that the request is no longer current
9875 this.requestRequest = null;
9876 this.requestQuery = null;
9877 oldRequest.abort();
9878 }
9879 };
9880
9881 /**
9882 * Get the query to be made.
9883 *
9884 * @protected
9885 * @method
9886 * @abstract
9887 * @return {string} query to be used
9888 */
9889 OO.ui.mixin.RequestManager.prototype.getRequestQuery = null;
9890
9891 /**
9892 * Get a new request object of the current query value.
9893 *
9894 * @protected
9895 * @method
9896 * @abstract
9897 * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
9898 */
9899 OO.ui.mixin.RequestManager.prototype.getRequest = null;
9900
9901 /**
9902 * Pre-process data returned by the request from #getRequest.
9903 *
9904 * The return value of this function will be cached, and any further queries for the given value
9905 * will use the cache rather than doing API requests.
9906 *
9907 * @protected
9908 * @method
9909 * @abstract
9910 * @param {Mixed} response Response from server
9911 * @return {Mixed} Cached result data
9912 */
9913 OO.ui.mixin.RequestManager.prototype.getRequestCacheDataFromResponse = null;
9914
9915 /**
9916 * LookupElement is a mixin that creates a {@link OO.ui.FloatingMenuSelectWidget menu} of suggested values for
9917 * a {@link OO.ui.TextInputWidget text input widget}. Suggested values are based on the characters the user types
9918 * into the text input field and, in general, the menu is only displayed when the user types. If a suggested value is chosen
9919 * from the lookup menu, that value becomes the value of the input field.
9920 *
9921 * Note that a new menu of suggested items is displayed when a value is chosen from the lookup menu. If this is
9922 * not the desired behavior, disable lookup menus with the #setLookupsDisabled method, then set the value, then
9923 * re-enable lookups.
9924 *
9925 * See the [OOjs UI demos][1] for an example.
9926 *
9927 * [1]: https://tools.wmflabs.org/oojs-ui/oojs-ui/demos/index.html#widgets-apex-vector-ltr
9928 *
9929 * @class
9930 * @abstract
9931 *
9932 * @constructor
9933 * @param {Object} [config] Configuration options
9934 * @cfg {jQuery} [$overlay] Overlay for the lookup menu; defaults to relative positioning
9935 * @cfg {jQuery} [$container=this.$element] The container element. The lookup menu is rendered beneath the specified element.
9936 * @cfg {boolean} [allowSuggestionsWhenEmpty=false] Request and display a lookup menu when the text input is empty.
9937 * By default, the lookup menu is not generated and displayed until the user begins to type.
9938 * @cfg {boolean} [highlightFirst=true] Whether the first lookup result should be highlighted (so, that the user can
9939 * take it over into the input with simply pressing return) automatically or not.
9940 */
9941 OO.ui.mixin.LookupElement = function OoUiMixinLookupElement( config ) {
9942 // Configuration initialization
9943 config = $.extend( { highlightFirst: true }, config );
9944
9945 // Mixin constructors
9946 OO.ui.mixin.RequestManager.call( this, config );
9947
9948 // Properties
9949 this.$overlay = config.$overlay || this.$element;
9950 this.lookupMenu = new OO.ui.FloatingMenuSelectWidget( {
9951 widget: this,
9952 input: this,
9953 $container: config.$container || this.$element
9954 } );
9955
9956 this.allowSuggestionsWhenEmpty = config.allowSuggestionsWhenEmpty || false;
9957
9958 this.lookupsDisabled = false;
9959 this.lookupInputFocused = false;
9960 this.lookupHighlightFirstItem = config.highlightFirst;
9961
9962 // Events
9963 this.$input.on( {
9964 focus: this.onLookupInputFocus.bind( this ),
9965 blur: this.onLookupInputBlur.bind( this ),
9966 mousedown: this.onLookupInputMouseDown.bind( this )
9967 } );
9968 this.connect( this, { change: 'onLookupInputChange' } );
9969 this.lookupMenu.connect( this, {
9970 toggle: 'onLookupMenuToggle',
9971 choose: 'onLookupMenuItemChoose'
9972 } );
9973
9974 // Initialization
9975 this.$element.addClass( 'oo-ui-lookupElement' );
9976 this.lookupMenu.$element.addClass( 'oo-ui-lookupElement-menu' );
9977 this.$overlay.append( this.lookupMenu.$element );
9978 };
9979
9980 /* Setup */
9981
9982 OO.mixinClass( OO.ui.mixin.LookupElement, OO.ui.mixin.RequestManager );
9983
9984 /* Methods */
9985
9986 /**
9987 * Handle input focus event.
9988 *
9989 * @protected
9990 * @param {jQuery.Event} e Input focus event
9991 */
9992 OO.ui.mixin.LookupElement.prototype.onLookupInputFocus = function () {
9993 this.lookupInputFocused = true;
9994 this.populateLookupMenu();
9995 };
9996
9997 /**
9998 * Handle input blur event.
9999 *
10000 * @protected
10001 * @param {jQuery.Event} e Input blur event
10002 */
10003 OO.ui.mixin.LookupElement.prototype.onLookupInputBlur = function () {
10004 this.closeLookupMenu();
10005 this.lookupInputFocused = false;
10006 };
10007
10008 /**
10009 * Handle input mouse down event.
10010 *
10011 * @protected
10012 * @param {jQuery.Event} e Input mouse down event
10013 */
10014 OO.ui.mixin.LookupElement.prototype.onLookupInputMouseDown = function () {
10015 // Only open the menu if the input was already focused.
10016 // This way we allow the user to open the menu again after closing it with Esc
10017 // by clicking in the input. Opening (and populating) the menu when initially
10018 // clicking into the input is handled by the focus handler.
10019 if ( this.lookupInputFocused && !this.lookupMenu.isVisible() ) {
10020 this.populateLookupMenu();
10021 }
10022 };
10023
10024 /**
10025 * Handle input change event.
10026 *
10027 * @protected
10028 * @param {string} value New input value
10029 */
10030 OO.ui.mixin.LookupElement.prototype.onLookupInputChange = function () {
10031 if ( this.lookupInputFocused ) {
10032 this.populateLookupMenu();
10033 }
10034 };
10035
10036 /**
10037 * Handle the lookup menu being shown/hidden.
10038 *
10039 * @protected
10040 * @param {boolean} visible Whether the lookup menu is now visible.
10041 */
10042 OO.ui.mixin.LookupElement.prototype.onLookupMenuToggle = function ( visible ) {
10043 if ( !visible ) {
10044 // When the menu is hidden, abort any active request and clear the menu.
10045 // This has to be done here in addition to closeLookupMenu(), because
10046 // MenuSelectWidget will close itself when the user presses Esc.
10047 this.abortLookupRequest();
10048 this.lookupMenu.clearItems();
10049 }
10050 };
10051
10052 /**
10053 * Handle menu item 'choose' event, updating the text input value to the value of the clicked item.
10054 *
10055 * @protected
10056 * @param {OO.ui.MenuOptionWidget} item Selected item
10057 */
10058 OO.ui.mixin.LookupElement.prototype.onLookupMenuItemChoose = function ( item ) {
10059 this.setValue( item.getData() );
10060 };
10061
10062 /**
10063 * Get lookup menu.
10064 *
10065 * @private
10066 * @return {OO.ui.FloatingMenuSelectWidget}
10067 */
10068 OO.ui.mixin.LookupElement.prototype.getLookupMenu = function () {
10069 return this.lookupMenu;
10070 };
10071
10072 /**
10073 * Disable or re-enable lookups.
10074 *
10075 * When lookups are disabled, calls to #populateLookupMenu will be ignored.
10076 *
10077 * @param {boolean} disabled Disable lookups
10078 */
10079 OO.ui.mixin.LookupElement.prototype.setLookupsDisabled = function ( disabled ) {
10080 this.lookupsDisabled = !!disabled;
10081 };
10082
10083 /**
10084 * Open the menu. If there are no entries in the menu, this does nothing.
10085 *
10086 * @private
10087 * @chainable
10088 */
10089 OO.ui.mixin.LookupElement.prototype.openLookupMenu = function () {
10090 if ( !this.lookupMenu.isEmpty() ) {
10091 this.lookupMenu.toggle( true );
10092 }
10093 return this;
10094 };
10095
10096 /**
10097 * Close the menu, empty it, and abort any pending request.
10098 *
10099 * @private
10100 * @chainable
10101 */
10102 OO.ui.mixin.LookupElement.prototype.closeLookupMenu = function () {
10103 this.lookupMenu.toggle( false );
10104 this.abortLookupRequest();
10105 this.lookupMenu.clearItems();
10106 return this;
10107 };
10108
10109 /**
10110 * Request menu items based on the input's current value, and when they arrive,
10111 * populate the menu with these items and show the menu.
10112 *
10113 * If lookups have been disabled with #setLookupsDisabled, this function does nothing.
10114 *
10115 * @private
10116 * @chainable
10117 */
10118 OO.ui.mixin.LookupElement.prototype.populateLookupMenu = function () {
10119 var widget = this,
10120 value = this.getValue();
10121
10122 if ( this.lookupsDisabled || this.isReadOnly() ) {
10123 return;
10124 }
10125
10126 // If the input is empty, clear the menu, unless suggestions when empty are allowed.
10127 if ( !this.allowSuggestionsWhenEmpty && value === '' ) {
10128 this.closeLookupMenu();
10129 // Skip population if there is already a request pending for the current value
10130 } else if ( value !== this.lookupQuery ) {
10131 this.getLookupMenuItems()
10132 .done( function ( items ) {
10133 widget.lookupMenu.clearItems();
10134 if ( items.length ) {
10135 widget.lookupMenu
10136 .addItems( items )
10137 .toggle( true );
10138 widget.initializeLookupMenuSelection();
10139 } else {
10140 widget.lookupMenu.toggle( false );
10141 }
10142 } )
10143 .fail( function () {
10144 widget.lookupMenu.clearItems();
10145 } );
10146 }
10147
10148 return this;
10149 };
10150
10151 /**
10152 * Highlight the first selectable item in the menu, if configured.
10153 *
10154 * @private
10155 * @chainable
10156 */
10157 OO.ui.mixin.LookupElement.prototype.initializeLookupMenuSelection = function () {
10158 if ( this.lookupHighlightFirstItem && !this.lookupMenu.getSelectedItem() ) {
10159 this.lookupMenu.highlightItem( this.lookupMenu.getFirstSelectableItem() );
10160 }
10161 };
10162
10163 /**
10164 * Get lookup menu items for the current query.
10165 *
10166 * @private
10167 * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument of
10168 * the done event. If the request was aborted to make way for a subsequent request, this promise
10169 * will not be rejected: it will remain pending forever.
10170 */
10171 OO.ui.mixin.LookupElement.prototype.getLookupMenuItems = function () {
10172 return this.getRequestData().then( function ( data ) {
10173 return this.getLookupMenuOptionsFromData( data );
10174 }.bind( this ) );
10175 };
10176
10177 /**
10178 * Abort the currently pending lookup request, if any.
10179 *
10180 * @private
10181 */
10182 OO.ui.mixin.LookupElement.prototype.abortLookupRequest = function () {
10183 this.abortRequest();
10184 };
10185
10186 /**
10187 * Get a new request object of the current lookup query value.
10188 *
10189 * @protected
10190 * @method
10191 * @abstract
10192 * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
10193 */
10194 OO.ui.mixin.LookupElement.prototype.getLookupRequest = null;
10195
10196 /**
10197 * Pre-process data returned by the request from #getLookupRequest.
10198 *
10199 * The return value of this function will be cached, and any further queries for the given value
10200 * will use the cache rather than doing API requests.
10201 *
10202 * @protected
10203 * @method
10204 * @abstract
10205 * @param {Mixed} response Response from server
10206 * @return {Mixed} Cached result data
10207 */
10208 OO.ui.mixin.LookupElement.prototype.getLookupCacheDataFromResponse = null;
10209
10210 /**
10211 * Get a list of menu option widgets from the (possibly cached) data returned by
10212 * #getLookupCacheDataFromResponse.
10213 *
10214 * @protected
10215 * @method
10216 * @abstract
10217 * @param {Mixed} data Cached result data, usually an array
10218 * @return {OO.ui.MenuOptionWidget[]} Menu items
10219 */
10220 OO.ui.mixin.LookupElement.prototype.getLookupMenuOptionsFromData = null;
10221
10222 /**
10223 * Set the read-only state of the widget.
10224 *
10225 * This will also disable/enable the lookups functionality.
10226 *
10227 * @param {boolean} readOnly Make input read-only
10228 * @chainable
10229 */
10230 OO.ui.mixin.LookupElement.prototype.setReadOnly = function ( readOnly ) {
10231 // Parent method
10232 // Note: Calling #setReadOnly this way assumes this is mixed into an OO.ui.TextInputWidget
10233 OO.ui.TextInputWidget.prototype.setReadOnly.call( this, readOnly );
10234
10235 // During construction, #setReadOnly is called before the OO.ui.mixin.LookupElement constructor
10236 if ( this.isReadOnly() && this.lookupMenu ) {
10237 this.closeLookupMenu();
10238 }
10239
10240 return this;
10241 };
10242
10243 /**
10244 * @inheritdoc OO.ui.mixin.RequestManager
10245 */
10246 OO.ui.mixin.LookupElement.prototype.getRequestQuery = function () {
10247 return this.getValue();
10248 };
10249
10250 /**
10251 * @inheritdoc OO.ui.mixin.RequestManager
10252 */
10253 OO.ui.mixin.LookupElement.prototype.getRequest = function () {
10254 return this.getLookupRequest();
10255 };
10256
10257 /**
10258 * @inheritdoc OO.ui.mixin.RequestManager
10259 */
10260 OO.ui.mixin.LookupElement.prototype.getRequestCacheDataFromResponse = function ( response ) {
10261 return this.getLookupCacheDataFromResponse( response );
10262 };
10263
10264 /**
10265 * CardLayouts are used within {@link OO.ui.IndexLayout index layouts} to create cards that users can select and display
10266 * from the index's optional {@link OO.ui.TabSelectWidget tab} navigation. Cards are usually not instantiated directly,
10267 * rather extended to include the required content and functionality.
10268 *
10269 * Each card must have a unique symbolic name, which is passed to the constructor. In addition, the card's tab
10270 * item is customized (with a label) using the #setupTabItem method. See
10271 * {@link OO.ui.IndexLayout IndexLayout} for an example.
10272 *
10273 * @class
10274 * @extends OO.ui.PanelLayout
10275 *
10276 * @constructor
10277 * @param {string} name Unique symbolic name of card
10278 * @param {Object} [config] Configuration options
10279 * @cfg {jQuery|string|Function|OO.ui.HtmlSnippet} [label] Label for card's tab
10280 */
10281 OO.ui.CardLayout = function OoUiCardLayout( name, config ) {
10282 // Allow passing positional parameters inside the config object
10283 if ( OO.isPlainObject( name ) && config === undefined ) {
10284 config = name;
10285 name = config.name;
10286 }
10287
10288 // Configuration initialization
10289 config = $.extend( { scrollable: true }, config );
10290
10291 // Parent constructor
10292 OO.ui.CardLayout.parent.call( this, config );
10293
10294 // Properties
10295 this.name = name;
10296 this.label = config.label;
10297 this.tabItem = null;
10298 this.active = false;
10299
10300 // Initialization
10301 this.$element.addClass( 'oo-ui-cardLayout' );
10302 };
10303
10304 /* Setup */
10305
10306 OO.inheritClass( OO.ui.CardLayout, OO.ui.PanelLayout );
10307
10308 /* Events */
10309
10310 /**
10311 * An 'active' event is emitted when the card becomes active. Cards become active when they are
10312 * shown in a index layout that is configured to display only one card at a time.
10313 *
10314 * @event active
10315 * @param {boolean} active Card is active
10316 */
10317
10318 /* Methods */
10319
10320 /**
10321 * Get the symbolic name of the card.
10322 *
10323 * @return {string} Symbolic name of card
10324 */
10325 OO.ui.CardLayout.prototype.getName = function () {
10326 return this.name;
10327 };
10328
10329 /**
10330 * Check if card is active.
10331 *
10332 * Cards become active when they are shown in a {@link OO.ui.IndexLayout index layout} that is configured to display
10333 * only one card at a time. Additional CSS is applied to the card's tab item to reflect the active state.
10334 *
10335 * @return {boolean} Card is active
10336 */
10337 OO.ui.CardLayout.prototype.isActive = function () {
10338 return this.active;
10339 };
10340
10341 /**
10342 * Get tab item.
10343 *
10344 * The tab item allows users to access the card from the index's tab
10345 * navigation. The tab item itself can be customized (with a label, level, etc.) using the #setupTabItem method.
10346 *
10347 * @return {OO.ui.TabOptionWidget|null} Tab option widget
10348 */
10349 OO.ui.CardLayout.prototype.getTabItem = function () {
10350 return this.tabItem;
10351 };
10352
10353 /**
10354 * Set or unset the tab item.
10355 *
10356 * Specify a {@link OO.ui.TabOptionWidget tab option} to set it,
10357 * or `null` to clear the tab item. To customize the tab item itself (e.g., to set a label or tab
10358 * level), use #setupTabItem instead of this method.
10359 *
10360 * @param {OO.ui.TabOptionWidget|null} tabItem Tab option widget, null to clear
10361 * @chainable
10362 */
10363 OO.ui.CardLayout.prototype.setTabItem = function ( tabItem ) {
10364 this.tabItem = tabItem || null;
10365 if ( tabItem ) {
10366 this.setupTabItem();
10367 }
10368 return this;
10369 };
10370
10371 /**
10372 * Set up the tab item.
10373 *
10374 * Use this method to customize the tab item (e.g., to add a label or tab level). To set or unset
10375 * the tab item itself (with a {@link OO.ui.TabOptionWidget tab option} or `null`), use
10376 * the #setTabItem method instead.
10377 *
10378 * @param {OO.ui.TabOptionWidget} tabItem Tab option widget to set up
10379 * @chainable
10380 */
10381 OO.ui.CardLayout.prototype.setupTabItem = function () {
10382 if ( this.label ) {
10383 this.tabItem.setLabel( this.label );
10384 }
10385 return this;
10386 };
10387
10388 /**
10389 * Set the card to its 'active' state.
10390 *
10391 * Cards become active when they are shown in a index layout that is configured to display only one card at a time. Additional
10392 * CSS is applied to the tab item to reflect the card's active state. Outside of the index
10393 * context, setting the active state on a card does nothing.
10394 *
10395 * @param {boolean} value Card is active
10396 * @fires active
10397 */
10398 OO.ui.CardLayout.prototype.setActive = function ( active ) {
10399 active = !!active;
10400
10401 if ( active !== this.active ) {
10402 this.active = active;
10403 this.$element.toggleClass( 'oo-ui-cardLayout-active', this.active );
10404 this.emit( 'active', this.active );
10405 }
10406 };
10407
10408 /**
10409 * PageLayouts are used within {@link OO.ui.BookletLayout booklet layouts} to create pages that users can select and display
10410 * from the booklet's optional {@link OO.ui.OutlineSelectWidget outline} navigation. Pages are usually not instantiated directly,
10411 * rather extended to include the required content and functionality.
10412 *
10413 * Each page must have a unique symbolic name, which is passed to the constructor. In addition, the page's outline
10414 * item is customized (with a label, outline level, etc.) using the #setupOutlineItem method. See
10415 * {@link OO.ui.BookletLayout BookletLayout} for an example.
10416 *
10417 * @class
10418 * @extends OO.ui.PanelLayout
10419 *
10420 * @constructor
10421 * @param {string} name Unique symbolic name of page
10422 * @param {Object} [config] Configuration options
10423 */
10424 OO.ui.PageLayout = function OoUiPageLayout( name, config ) {
10425 // Allow passing positional parameters inside the config object
10426 if ( OO.isPlainObject( name ) && config === undefined ) {
10427 config = name;
10428 name = config.name;
10429 }
10430
10431 // Configuration initialization
10432 config = $.extend( { scrollable: true }, config );
10433
10434 // Parent constructor
10435 OO.ui.PageLayout.parent.call( this, config );
10436
10437 // Properties
10438 this.name = name;
10439 this.outlineItem = null;
10440 this.active = false;
10441
10442 // Initialization
10443 this.$element.addClass( 'oo-ui-pageLayout' );
10444 };
10445
10446 /* Setup */
10447
10448 OO.inheritClass( OO.ui.PageLayout, OO.ui.PanelLayout );
10449
10450 /* Events */
10451
10452 /**
10453 * An 'active' event is emitted when the page becomes active. Pages become active when they are
10454 * shown in a booklet layout that is configured to display only one page at a time.
10455 *
10456 * @event active
10457 * @param {boolean} active Page is active
10458 */
10459
10460 /* Methods */
10461
10462 /**
10463 * Get the symbolic name of the page.
10464 *
10465 * @return {string} Symbolic name of page
10466 */
10467 OO.ui.PageLayout.prototype.getName = function () {
10468 return this.name;
10469 };
10470
10471 /**
10472 * Check if page is active.
10473 *
10474 * Pages become active when they are shown in a {@link OO.ui.BookletLayout booklet layout} that is configured to display
10475 * only one page at a time. Additional CSS is applied to the page's outline item to reflect the active state.
10476 *
10477 * @return {boolean} Page is active
10478 */
10479 OO.ui.PageLayout.prototype.isActive = function () {
10480 return this.active;
10481 };
10482
10483 /**
10484 * Get outline item.
10485 *
10486 * The outline item allows users to access the page from the booklet's outline
10487 * navigation. The outline item itself can be customized (with a label, level, etc.) using the #setupOutlineItem method.
10488 *
10489 * @return {OO.ui.OutlineOptionWidget|null} Outline option widget
10490 */
10491 OO.ui.PageLayout.prototype.getOutlineItem = function () {
10492 return this.outlineItem;
10493 };
10494
10495 /**
10496 * Set or unset the outline item.
10497 *
10498 * Specify an {@link OO.ui.OutlineOptionWidget outline option} to set it,
10499 * or `null` to clear the outline item. To customize the outline item itself (e.g., to set a label or outline
10500 * level), use #setupOutlineItem instead of this method.
10501 *
10502 * @param {OO.ui.OutlineOptionWidget|null} outlineItem Outline option widget, null to clear
10503 * @chainable
10504 */
10505 OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) {
10506 this.outlineItem = outlineItem || null;
10507 if ( outlineItem ) {
10508 this.setupOutlineItem();
10509 }
10510 return this;
10511 };
10512
10513 /**
10514 * Set up the outline item.
10515 *
10516 * Use this method to customize the outline item (e.g., to add a label or outline level). To set or unset
10517 * the outline item itself (with an {@link OO.ui.OutlineOptionWidget outline option} or `null`), use
10518 * the #setOutlineItem method instead.
10519 *
10520 * @param {OO.ui.OutlineOptionWidget} outlineItem Outline option widget to set up
10521 * @chainable
10522 */
10523 OO.ui.PageLayout.prototype.setupOutlineItem = function () {
10524 return this;
10525 };
10526
10527 /**
10528 * Set the page to its 'active' state.
10529 *
10530 * Pages become active when they are shown in a booklet layout that is configured to display only one page at a time. Additional
10531 * CSS is applied to the outline item to reflect the page's active state. Outside of the booklet
10532 * context, setting the active state on a page does nothing.
10533 *
10534 * @param {boolean} value Page is active
10535 * @fires active
10536 */
10537 OO.ui.PageLayout.prototype.setActive = function ( active ) {
10538 active = !!active;
10539
10540 if ( active !== this.active ) {
10541 this.active = active;
10542 this.$element.toggleClass( 'oo-ui-pageLayout-active', active );
10543 this.emit( 'active', this.active );
10544 }
10545 };
10546
10547 /**
10548 * StackLayouts contain a series of {@link OO.ui.PanelLayout panel layouts}. By default, only one panel is displayed
10549 * at a time, though the stack layout can also be configured to show all contained panels, one after another,
10550 * by setting the #continuous option to 'true'.
10551 *
10552 * @example
10553 * // A stack layout with two panels, configured to be displayed continously
10554 * var myStack = new OO.ui.StackLayout( {
10555 * items: [
10556 * new OO.ui.PanelLayout( {
10557 * $content: $( '<p>Panel One</p>' ),
10558 * padded: true,
10559 * framed: true
10560 * } ),
10561 * new OO.ui.PanelLayout( {
10562 * $content: $( '<p>Panel Two</p>' ),
10563 * padded: true,
10564 * framed: true
10565 * } )
10566 * ],
10567 * continuous: true
10568 * } );
10569 * $( 'body' ).append( myStack.$element );
10570 *
10571 * @class
10572 * @extends OO.ui.PanelLayout
10573 * @mixins OO.ui.mixin.GroupElement
10574 *
10575 * @constructor
10576 * @param {Object} [config] Configuration options
10577 * @cfg {boolean} [continuous=false] Show all panels, one after another. By default, only one panel is displayed at a time.
10578 * @cfg {OO.ui.Layout[]} [items] Panel layouts to add to the stack layout.
10579 */
10580 OO.ui.StackLayout = function OoUiStackLayout( config ) {
10581 // Configuration initialization
10582 config = $.extend( { scrollable: true }, config );
10583
10584 // Parent constructor
10585 OO.ui.StackLayout.parent.call( this, config );
10586
10587 // Mixin constructors
10588 OO.ui.mixin.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
10589
10590 // Properties
10591 this.currentItem = null;
10592 this.continuous = !!config.continuous;
10593
10594 // Initialization
10595 this.$element.addClass( 'oo-ui-stackLayout' );
10596 if ( this.continuous ) {
10597 this.$element.addClass( 'oo-ui-stackLayout-continuous' );
10598 this.$element.on( 'scroll', OO.ui.debounce( this.onScroll.bind( this ), 250 ) );
10599 }
10600 if ( Array.isArray( config.items ) ) {
10601 this.addItems( config.items );
10602 }
10603 };
10604
10605 /* Setup */
10606
10607 OO.inheritClass( OO.ui.StackLayout, OO.ui.PanelLayout );
10608 OO.mixinClass( OO.ui.StackLayout, OO.ui.mixin.GroupElement );
10609
10610 /* Events */
10611
10612 /**
10613 * A 'set' event is emitted when panels are {@link #addItems added}, {@link #removeItems removed},
10614 * {@link #clearItems cleared} or {@link #setItem displayed}.
10615 *
10616 * @event set
10617 * @param {OO.ui.Layout|null} item Current panel or `null` if no panel is shown
10618 */
10619
10620 /**
10621 * When used in continuous mode, this event is emitted when the user scrolls down
10622 * far enough such that currentItem is no longer visible.
10623 *
10624 * @event visibleItemChange
10625 * @param {OO.ui.PanelLayout} panel The next visible item in the layout
10626 */
10627
10628 /* Methods */
10629
10630 /**
10631 * Handle scroll events from the layout element
10632 *
10633 * @param {jQuery.Event} e
10634 * @fires visibleItemChange
10635 */
10636 OO.ui.StackLayout.prototype.onScroll = function () {
10637 var currentRect,
10638 len = this.items.length,
10639 currentIndex = this.items.indexOf( this.currentItem ),
10640 newIndex = currentIndex,
10641 containerRect = this.$element[ 0 ].getBoundingClientRect();
10642
10643 if ( !containerRect || ( !containerRect.top && !containerRect.bottom ) ) {
10644 // Can't get bounding rect, possibly not attached.
10645 return;
10646 }
10647
10648 function getRect( item ) {
10649 return item.$element[ 0 ].getBoundingClientRect();
10650 }
10651
10652 function isVisible( item ) {
10653 var rect = getRect( item );
10654 return rect.bottom > containerRect.top && rect.top < containerRect.bottom;
10655 }
10656
10657 currentRect = getRect( this.currentItem );
10658
10659 if ( currentRect.bottom < containerRect.top ) {
10660 // Scrolled down past current item
10661 while ( ++newIndex < len ) {
10662 if ( isVisible( this.items[ newIndex ] ) ) {
10663 break;
10664 }
10665 }
10666 } else if ( currentRect.top > containerRect.bottom ) {
10667 // Scrolled up past current item
10668 while ( --newIndex >= 0 ) {
10669 if ( isVisible( this.items[ newIndex ] ) ) {
10670 break;
10671 }
10672 }
10673 }
10674
10675 if ( newIndex !== currentIndex ) {
10676 this.emit( 'visibleItemChange', this.items[ newIndex ] );
10677 }
10678 };
10679
10680 /**
10681 * Get the current panel.
10682 *
10683 * @return {OO.ui.Layout|null}
10684 */
10685 OO.ui.StackLayout.prototype.getCurrentItem = function () {
10686 return this.currentItem;
10687 };
10688
10689 /**
10690 * Unset the current item.
10691 *
10692 * @private
10693 * @param {OO.ui.StackLayout} layout
10694 * @fires set
10695 */
10696 OO.ui.StackLayout.prototype.unsetCurrentItem = function () {
10697 var prevItem = this.currentItem;
10698 if ( prevItem === null ) {
10699 return;
10700 }
10701
10702 this.currentItem = null;
10703 this.emit( 'set', null );
10704 };
10705
10706 /**
10707 * Add panel layouts to the stack layout.
10708 *
10709 * Panels will be added to the end of the stack layout array unless the optional index parameter specifies a different
10710 * insertion point. Adding a panel that is already in the stack will move it to the end of the array or the point specified
10711 * by the index.
10712 *
10713 * @param {OO.ui.Layout[]} items Panels to add
10714 * @param {number} [index] Index of the insertion point
10715 * @chainable
10716 */
10717 OO.ui.StackLayout.prototype.addItems = function ( items, index ) {
10718 // Update the visibility
10719 this.updateHiddenState( items, this.currentItem );
10720
10721 // Mixin method
10722 OO.ui.mixin.GroupElement.prototype.addItems.call( this, items, index );
10723
10724 if ( !this.currentItem && items.length ) {
10725 this.setItem( items[ 0 ] );
10726 }
10727
10728 return this;
10729 };
10730
10731 /**
10732 * Remove the specified panels from the stack layout.
10733 *
10734 * Removed panels are detached from the DOM, not removed, so that they may be reused. To remove all panels,
10735 * you may wish to use the #clearItems method instead.
10736 *
10737 * @param {OO.ui.Layout[]} items Panels to remove
10738 * @chainable
10739 * @fires set
10740 */
10741 OO.ui.StackLayout.prototype.removeItems = function ( items ) {
10742 // Mixin method
10743 OO.ui.mixin.GroupElement.prototype.removeItems.call( this, items );
10744
10745 if ( items.indexOf( this.currentItem ) !== -1 ) {
10746 if ( this.items.length ) {
10747 this.setItem( this.items[ 0 ] );
10748 } else {
10749 this.unsetCurrentItem();
10750 }
10751 }
10752
10753 return this;
10754 };
10755
10756 /**
10757 * Clear all panels from the stack layout.
10758 *
10759 * Cleared panels are detached from the DOM, not removed, so that they may be reused. To remove only
10760 * a subset of panels, use the #removeItems method.
10761 *
10762 * @chainable
10763 * @fires set
10764 */
10765 OO.ui.StackLayout.prototype.clearItems = function () {
10766 this.unsetCurrentItem();
10767 OO.ui.mixin.GroupElement.prototype.clearItems.call( this );
10768
10769 return this;
10770 };
10771
10772 /**
10773 * Show the specified panel.
10774 *
10775 * If another panel is currently displayed, it will be hidden.
10776 *
10777 * @param {OO.ui.Layout} item Panel to show
10778 * @chainable
10779 * @fires set
10780 */
10781 OO.ui.StackLayout.prototype.setItem = function ( item ) {
10782 if ( item !== this.currentItem ) {
10783 this.updateHiddenState( this.items, item );
10784
10785 if ( this.items.indexOf( item ) !== -1 ) {
10786 this.currentItem = item;
10787 this.emit( 'set', item );
10788 } else {
10789 this.unsetCurrentItem();
10790 }
10791 }
10792
10793 return this;
10794 };
10795
10796 /**
10797 * Update the visibility of all items in case of non-continuous view.
10798 *
10799 * Ensure all items are hidden except for the selected one.
10800 * This method does nothing when the stack is continuous.
10801 *
10802 * @private
10803 * @param {OO.ui.Layout[]} items Item list iterate over
10804 * @param {OO.ui.Layout} [selectedItem] Selected item to show
10805 */
10806 OO.ui.StackLayout.prototype.updateHiddenState = function ( items, selectedItem ) {
10807 var i, len;
10808
10809 if ( !this.continuous ) {
10810 for ( i = 0, len = items.length; i < len; i++ ) {
10811 if ( !selectedItem || selectedItem !== items[ i ] ) {
10812 items[ i ].$element.addClass( 'oo-ui-element-hidden' );
10813 }
10814 }
10815 if ( selectedItem ) {
10816 selectedItem.$element.removeClass( 'oo-ui-element-hidden' );
10817 }
10818 }
10819 };
10820
10821 /**
10822 * 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)
10823 * and its size is customized with the #menuSize config. The content area will fill all remaining space.
10824 *
10825 * @example
10826 * var menuLayout = new OO.ui.MenuLayout( {
10827 * position: 'top'
10828 * } ),
10829 * menuPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
10830 * contentPanel = new OO.ui.PanelLayout( { padded: true, expanded: true, scrollable: true } ),
10831 * select = new OO.ui.SelectWidget( {
10832 * items: [
10833 * new OO.ui.OptionWidget( {
10834 * data: 'before',
10835 * label: 'Before',
10836 * } ),
10837 * new OO.ui.OptionWidget( {
10838 * data: 'after',
10839 * label: 'After',
10840 * } ),
10841 * new OO.ui.OptionWidget( {
10842 * data: 'top',
10843 * label: 'Top',
10844 * } ),
10845 * new OO.ui.OptionWidget( {
10846 * data: 'bottom',
10847 * label: 'Bottom',
10848 * } )
10849 * ]
10850 * } ).on( 'select', function ( item ) {
10851 * menuLayout.setMenuPosition( item.getData() );
10852 * } );
10853 *
10854 * menuLayout.$menu.append(
10855 * menuPanel.$element.append( '<b>Menu panel</b>', select.$element )
10856 * );
10857 * menuLayout.$content.append(
10858 * contentPanel.$element.append( '<b>Content panel</b>', '<p>Note that the menu is positioned relative to the content panel: top, bottom, after, before.</p>')
10859 * );
10860 * $( 'body' ).append( menuLayout.$element );
10861 *
10862 * If menu size needs to be overridden, it can be accomplished using CSS similar to the snippet
10863 * below. MenuLayout's CSS will override the appropriate values with 'auto' or '0' to display the
10864 * menu correctly. If `menuPosition` is known beforehand, CSS rules corresponding to other positions
10865 * may be omitted.
10866 *
10867 * .oo-ui-menuLayout-menu {
10868 * height: 200px;
10869 * width: 200px;
10870 * }
10871 * .oo-ui-menuLayout-content {
10872 * top: 200px;
10873 * left: 200px;
10874 * right: 200px;
10875 * bottom: 200px;
10876 * }
10877 *
10878 * @class
10879 * @extends OO.ui.Layout
10880 *
10881 * @constructor
10882 * @param {Object} [config] Configuration options
10883 * @cfg {boolean} [showMenu=true] Show menu
10884 * @cfg {string} [menuPosition='before'] Position of menu: `top`, `after`, `bottom` or `before`
10885 */
10886 OO.ui.MenuLayout = function OoUiMenuLayout( config ) {
10887 // Configuration initialization
10888 config = $.extend( {
10889 showMenu: true,
10890 menuPosition: 'before'
10891 }, config );
10892
10893 // Parent constructor
10894 OO.ui.MenuLayout.parent.call( this, config );
10895
10896 /**
10897 * Menu DOM node
10898 *
10899 * @property {jQuery}
10900 */
10901 this.$menu = $( '<div>' );
10902 /**
10903 * Content DOM node
10904 *
10905 * @property {jQuery}
10906 */
10907 this.$content = $( '<div>' );
10908
10909 // Initialization
10910 this.$menu
10911 .addClass( 'oo-ui-menuLayout-menu' );
10912 this.$content.addClass( 'oo-ui-menuLayout-content' );
10913 this.$element
10914 .addClass( 'oo-ui-menuLayout' )
10915 .append( this.$content, this.$menu );
10916 this.setMenuPosition( config.menuPosition );
10917 this.toggleMenu( config.showMenu );
10918 };
10919
10920 /* Setup */
10921
10922 OO.inheritClass( OO.ui.MenuLayout, OO.ui.Layout );
10923
10924 /* Methods */
10925
10926 /**
10927 * Toggle menu.
10928 *
10929 * @param {boolean} showMenu Show menu, omit to toggle
10930 * @chainable
10931 */
10932 OO.ui.MenuLayout.prototype.toggleMenu = function ( showMenu ) {
10933 showMenu = showMenu === undefined ? !this.showMenu : !!showMenu;
10934
10935 if ( this.showMenu !== showMenu ) {
10936 this.showMenu = showMenu;
10937 this.$element
10938 .toggleClass( 'oo-ui-menuLayout-showMenu', this.showMenu )
10939 .toggleClass( 'oo-ui-menuLayout-hideMenu', !this.showMenu );
10940 }
10941
10942 return this;
10943 };
10944
10945 /**
10946 * Check if menu is visible
10947 *
10948 * @return {boolean} Menu is visible
10949 */
10950 OO.ui.MenuLayout.prototype.isMenuVisible = function () {
10951 return this.showMenu;
10952 };
10953
10954 /**
10955 * Set menu position.
10956 *
10957 * @param {string} position Position of menu, either `top`, `after`, `bottom` or `before`
10958 * @throws {Error} If position value is not supported
10959 * @chainable
10960 */
10961 OO.ui.MenuLayout.prototype.setMenuPosition = function ( position ) {
10962 this.$element.removeClass( 'oo-ui-menuLayout-' + this.menuPosition );
10963 this.menuPosition = position;
10964 this.$element.addClass( 'oo-ui-menuLayout-' + position );
10965
10966 return this;
10967 };
10968
10969 /**
10970 * Get menu position.
10971 *
10972 * @return {string} Menu position
10973 */
10974 OO.ui.MenuLayout.prototype.getMenuPosition = function () {
10975 return this.menuPosition;
10976 };
10977
10978 /**
10979 * BookletLayouts contain {@link OO.ui.PageLayout page layouts} as well as
10980 * an {@link OO.ui.OutlineSelectWidget outline} that allows users to easily navigate
10981 * through the pages and select which one to display. By default, only one page is
10982 * displayed at a time and the outline is hidden. When a user navigates to a new page,
10983 * the booklet layout automatically focuses on the first focusable element, unless the
10984 * default setting is changed. Optionally, booklets can be configured to show
10985 * {@link OO.ui.OutlineControlsWidget controls} for adding, moving, and removing items.
10986 *
10987 * @example
10988 * // Example of a BookletLayout that contains two PageLayouts.
10989 *
10990 * function PageOneLayout( name, config ) {
10991 * PageOneLayout.parent.call( this, name, config );
10992 * this.$element.append( '<p>First page</p><p>(This booklet has an outline, displayed on the left)</p>' );
10993 * }
10994 * OO.inheritClass( PageOneLayout, OO.ui.PageLayout );
10995 * PageOneLayout.prototype.setupOutlineItem = function () {
10996 * this.outlineItem.setLabel( 'Page One' );
10997 * };
10998 *
10999 * function PageTwoLayout( name, config ) {
11000 * PageTwoLayout.parent.call( this, name, config );
11001 * this.$element.append( '<p>Second page</p>' );
11002 * }
11003 * OO.inheritClass( PageTwoLayout, OO.ui.PageLayout );
11004 * PageTwoLayout.prototype.setupOutlineItem = function () {
11005 * this.outlineItem.setLabel( 'Page Two' );
11006 * };
11007 *
11008 * var page1 = new PageOneLayout( 'one' ),
11009 * page2 = new PageTwoLayout( 'two' );
11010 *
11011 * var booklet = new OO.ui.BookletLayout( {
11012 * outlined: true
11013 * } );
11014 *
11015 * booklet.addPages ( [ page1, page2 ] );
11016 * $( 'body' ).append( booklet.$element );
11017 *
11018 * @class
11019 * @extends OO.ui.MenuLayout
11020 *
11021 * @constructor
11022 * @param {Object} [config] Configuration options
11023 * @cfg {boolean} [continuous=false] Show all pages, one after another
11024 * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new page is displayed.
11025 * @cfg {boolean} [outlined=false] Show the outline. The outline is used to navigate through the pages of the booklet.
11026 * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
11027 */
11028 OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
11029 // Configuration initialization
11030 config = config || {};
11031
11032 // Parent constructor
11033 OO.ui.BookletLayout.parent.call( this, config );
11034
11035 // Properties
11036 this.currentPageName = null;
11037 this.pages = {};
11038 this.ignoreFocus = false;
11039 this.stackLayout = new OO.ui.StackLayout( { continuous: !!config.continuous } );
11040 this.$content.append( this.stackLayout.$element );
11041 this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
11042 this.outlineVisible = false;
11043 this.outlined = !!config.outlined;
11044 if ( this.outlined ) {
11045 this.editable = !!config.editable;
11046 this.outlineControlsWidget = null;
11047 this.outlineSelectWidget = new OO.ui.OutlineSelectWidget();
11048 this.outlinePanel = new OO.ui.PanelLayout( { scrollable: true } );
11049 this.$menu.append( this.outlinePanel.$element );
11050 this.outlineVisible = true;
11051 if ( this.editable ) {
11052 this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
11053 this.outlineSelectWidget
11054 );
11055 }
11056 }
11057 this.toggleMenu( this.outlined );
11058
11059 // Events
11060 this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
11061 if ( this.outlined ) {
11062 this.outlineSelectWidget.connect( this, { select: 'onOutlineSelectWidgetSelect' } );
11063 this.scrolling = false;
11064 this.stackLayout.connect( this, { visibleItemChange: 'onStackLayoutVisibleItemChange' } );
11065 }
11066 if ( this.autoFocus ) {
11067 // Event 'focus' does not bubble, but 'focusin' does
11068 this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
11069 }
11070
11071 // Initialization
11072 this.$element.addClass( 'oo-ui-bookletLayout' );
11073 this.stackLayout.$element.addClass( 'oo-ui-bookletLayout-stackLayout' );
11074 if ( this.outlined ) {
11075 this.outlinePanel.$element
11076 .addClass( 'oo-ui-bookletLayout-outlinePanel' )
11077 .append( this.outlineSelectWidget.$element );
11078 if ( this.editable ) {
11079 this.outlinePanel.$element
11080 .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
11081 .append( this.outlineControlsWidget.$element );
11082 }
11083 }
11084 };
11085
11086 /* Setup */
11087
11088 OO.inheritClass( OO.ui.BookletLayout, OO.ui.MenuLayout );
11089
11090 /* Events */
11091
11092 /**
11093 * A 'set' event is emitted when a page is {@link #setPage set} to be displayed by the booklet layout.
11094 * @event set
11095 * @param {OO.ui.PageLayout} page Current page
11096 */
11097
11098 /**
11099 * An 'add' event is emitted when pages are {@link #addPages added} to the booklet layout.
11100 *
11101 * @event add
11102 * @param {OO.ui.PageLayout[]} page Added pages
11103 * @param {number} index Index pages were added at
11104 */
11105
11106 /**
11107 * A 'remove' event is emitted when pages are {@link #clearPages cleared} or
11108 * {@link #removePages removed} from the booklet.
11109 *
11110 * @event remove
11111 * @param {OO.ui.PageLayout[]} pages Removed pages
11112 */
11113
11114 /* Methods */
11115
11116 /**
11117 * Handle stack layout focus.
11118 *
11119 * @private
11120 * @param {jQuery.Event} e Focusin event
11121 */
11122 OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) {
11123 var name, $target;
11124
11125 // Find the page that an element was focused within
11126 $target = $( e.target ).closest( '.oo-ui-pageLayout' );
11127 for ( name in this.pages ) {
11128 // Check for page match, exclude current page to find only page changes
11129 if ( this.pages[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentPageName ) {
11130 this.setPage( name );
11131 break;
11132 }
11133 }
11134 };
11135
11136 /**
11137 * Handle visibleItemChange events from the stackLayout
11138 *
11139 * The next visible page is set as the current page by selecting it
11140 * in the outline
11141 *
11142 * @param {OO.ui.PageLayout} page The next visible page in the layout
11143 */
11144 OO.ui.BookletLayout.prototype.onStackLayoutVisibleItemChange = function ( page ) {
11145 // Set a flag to so that the resulting call to #onStackLayoutSet doesn't
11146 // try and scroll the item into view again.
11147 this.scrolling = true;
11148 this.outlineSelectWidget.selectItemByData( page.getName() );
11149 this.scrolling = false;
11150 };
11151
11152 /**
11153 * Handle stack layout set events.
11154 *
11155 * @private
11156 * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
11157 */
11158 OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
11159 var layout = this;
11160 if ( !this.scrolling && page ) {
11161 page.scrollElementIntoView( { complete: function () {
11162 if ( layout.autoFocus ) {
11163 layout.focus();
11164 }
11165 } } );
11166 }
11167 };
11168
11169 /**
11170 * Focus the first input in the current page.
11171 *
11172 * If no page is selected, the first selectable page will be selected.
11173 * If the focus is already in an element on the current page, nothing will happen.
11174 * @param {number} [itemIndex] A specific item to focus on
11175 */
11176 OO.ui.BookletLayout.prototype.focus = function ( itemIndex ) {
11177 var page,
11178 items = this.stackLayout.getItems();
11179
11180 if ( itemIndex !== undefined && items[ itemIndex ] ) {
11181 page = items[ itemIndex ];
11182 } else {
11183 page = this.stackLayout.getCurrentItem();
11184 }
11185
11186 if ( !page && this.outlined ) {
11187 this.selectFirstSelectablePage();
11188 page = this.stackLayout.getCurrentItem();
11189 }
11190 if ( !page ) {
11191 return;
11192 }
11193 // Only change the focus if is not already in the current page
11194 if ( !OO.ui.contains( page.$element[ 0 ], this.getElementDocument().activeElement, true ) ) {
11195 page.focus();
11196 }
11197 };
11198
11199 /**
11200 * Find the first focusable input in the booklet layout and focus
11201 * on it.
11202 */
11203 OO.ui.BookletLayout.prototype.focusFirstFocusable = function () {
11204 OO.ui.findFocusable( this.stackLayout.$element ).focus();
11205 };
11206
11207 /**
11208 * Handle outline widget select events.
11209 *
11210 * @private
11211 * @param {OO.ui.OptionWidget|null} item Selected item
11212 */
11213 OO.ui.BookletLayout.prototype.onOutlineSelectWidgetSelect = function ( item ) {
11214 if ( item ) {
11215 this.setPage( item.getData() );
11216 }
11217 };
11218
11219 /**
11220 * Check if booklet has an outline.
11221 *
11222 * @return {boolean} Booklet has an outline
11223 */
11224 OO.ui.BookletLayout.prototype.isOutlined = function () {
11225 return this.outlined;
11226 };
11227
11228 /**
11229 * Check if booklet has editing controls.
11230 *
11231 * @return {boolean} Booklet is editable
11232 */
11233 OO.ui.BookletLayout.prototype.isEditable = function () {
11234 return this.editable;
11235 };
11236
11237 /**
11238 * Check if booklet has a visible outline.
11239 *
11240 * @return {boolean} Outline is visible
11241 */
11242 OO.ui.BookletLayout.prototype.isOutlineVisible = function () {
11243 return this.outlined && this.outlineVisible;
11244 };
11245
11246 /**
11247 * Hide or show the outline.
11248 *
11249 * @param {boolean} [show] Show outline, omit to invert current state
11250 * @chainable
11251 */
11252 OO.ui.BookletLayout.prototype.toggleOutline = function ( show ) {
11253 if ( this.outlined ) {
11254 show = show === undefined ? !this.outlineVisible : !!show;
11255 this.outlineVisible = show;
11256 this.toggleMenu( show );
11257 }
11258
11259 return this;
11260 };
11261
11262 /**
11263 * Get the page closest to the specified page.
11264 *
11265 * @param {OO.ui.PageLayout} page Page to use as a reference point
11266 * @return {OO.ui.PageLayout|null} Page closest to the specified page
11267 */
11268 OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) {
11269 var next, prev, level,
11270 pages = this.stackLayout.getItems(),
11271 index = pages.indexOf( page );
11272
11273 if ( index !== -1 ) {
11274 next = pages[ index + 1 ];
11275 prev = pages[ index - 1 ];
11276 // Prefer adjacent pages at the same level
11277 if ( this.outlined ) {
11278 level = this.outlineSelectWidget.getItemFromData( page.getName() ).getLevel();
11279 if (
11280 prev &&
11281 level === this.outlineSelectWidget.getItemFromData( prev.getName() ).getLevel()
11282 ) {
11283 return prev;
11284 }
11285 if (
11286 next &&
11287 level === this.outlineSelectWidget.getItemFromData( next.getName() ).getLevel()
11288 ) {
11289 return next;
11290 }
11291 }
11292 }
11293 return prev || next || null;
11294 };
11295
11296 /**
11297 * Get the outline widget.
11298 *
11299 * If the booklet is not outlined, the method will return `null`.
11300 *
11301 * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if the booklet is not outlined
11302 */
11303 OO.ui.BookletLayout.prototype.getOutline = function () {
11304 return this.outlineSelectWidget;
11305 };
11306
11307 /**
11308 * Get the outline controls widget.
11309 *
11310 * If the outline is not editable, the method will return `null`.
11311 *
11312 * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
11313 */
11314 OO.ui.BookletLayout.prototype.getOutlineControls = function () {
11315 return this.outlineControlsWidget;
11316 };
11317
11318 /**
11319 * Get a page by its symbolic name.
11320 *
11321 * @param {string} name Symbolic name of page
11322 * @return {OO.ui.PageLayout|undefined} Page, if found
11323 */
11324 OO.ui.BookletLayout.prototype.getPage = function ( name ) {
11325 return this.pages[ name ];
11326 };
11327
11328 /**
11329 * Get the current page.
11330 *
11331 * @return {OO.ui.PageLayout|undefined} Current page, if found
11332 */
11333 OO.ui.BookletLayout.prototype.getCurrentPage = function () {
11334 var name = this.getCurrentPageName();
11335 return name ? this.getPage( name ) : undefined;
11336 };
11337
11338 /**
11339 * Get the symbolic name of the current page.
11340 *
11341 * @return {string|null} Symbolic name of the current page
11342 */
11343 OO.ui.BookletLayout.prototype.getCurrentPageName = function () {
11344 return this.currentPageName;
11345 };
11346
11347 /**
11348 * Add pages to the booklet layout
11349 *
11350 * When pages are added with the same names as existing pages, the existing pages will be
11351 * automatically removed before the new pages are added.
11352 *
11353 * @param {OO.ui.PageLayout[]} pages Pages to add
11354 * @param {number} index Index of the insertion point
11355 * @fires add
11356 * @chainable
11357 */
11358 OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) {
11359 var i, len, name, page, item, currentIndex,
11360 stackLayoutPages = this.stackLayout.getItems(),
11361 remove = [],
11362 items = [];
11363
11364 // Remove pages with same names
11365 for ( i = 0, len = pages.length; i < len; i++ ) {
11366 page = pages[ i ];
11367 name = page.getName();
11368
11369 if ( Object.prototype.hasOwnProperty.call( this.pages, name ) ) {
11370 // Correct the insertion index
11371 currentIndex = stackLayoutPages.indexOf( this.pages[ name ] );
11372 if ( currentIndex !== -1 && currentIndex + 1 < index ) {
11373 index--;
11374 }
11375 remove.push( this.pages[ name ] );
11376 }
11377 }
11378 if ( remove.length ) {
11379 this.removePages( remove );
11380 }
11381
11382 // Add new pages
11383 for ( i = 0, len = pages.length; i < len; i++ ) {
11384 page = pages[ i ];
11385 name = page.getName();
11386 this.pages[ page.getName() ] = page;
11387 if ( this.outlined ) {
11388 item = new OO.ui.OutlineOptionWidget( { data: name } );
11389 page.setOutlineItem( item );
11390 items.push( item );
11391 }
11392 }
11393
11394 if ( this.outlined && items.length ) {
11395 this.outlineSelectWidget.addItems( items, index );
11396 this.selectFirstSelectablePage();
11397 }
11398 this.stackLayout.addItems( pages, index );
11399 this.emit( 'add', pages, index );
11400
11401 return this;
11402 };
11403
11404 /**
11405 * Remove the specified pages from the booklet layout.
11406 *
11407 * To remove all pages from the booklet, you may wish to use the #clearPages method instead.
11408 *
11409 * @param {OO.ui.PageLayout[]} pages An array of pages to remove
11410 * @fires remove
11411 * @chainable
11412 */
11413 OO.ui.BookletLayout.prototype.removePages = function ( pages ) {
11414 var i, len, name, page,
11415 items = [];
11416
11417 for ( i = 0, len = pages.length; i < len; i++ ) {
11418 page = pages[ i ];
11419 name = page.getName();
11420 delete this.pages[ name ];
11421 if ( this.outlined ) {
11422 items.push( this.outlineSelectWidget.getItemFromData( name ) );
11423 page.setOutlineItem( null );
11424 }
11425 }
11426 if ( this.outlined && items.length ) {
11427 this.outlineSelectWidget.removeItems( items );
11428 this.selectFirstSelectablePage();
11429 }
11430 this.stackLayout.removeItems( pages );
11431 this.emit( 'remove', pages );
11432
11433 return this;
11434 };
11435
11436 /**
11437 * Clear all pages from the booklet layout.
11438 *
11439 * To remove only a subset of pages from the booklet, use the #removePages method.
11440 *
11441 * @fires remove
11442 * @chainable
11443 */
11444 OO.ui.BookletLayout.prototype.clearPages = function () {
11445 var i, len,
11446 pages = this.stackLayout.getItems();
11447
11448 this.pages = {};
11449 this.currentPageName = null;
11450 if ( this.outlined ) {
11451 this.outlineSelectWidget.clearItems();
11452 for ( i = 0, len = pages.length; i < len; i++ ) {
11453 pages[ i ].setOutlineItem( null );
11454 }
11455 }
11456 this.stackLayout.clearItems();
11457
11458 this.emit( 'remove', pages );
11459
11460 return this;
11461 };
11462
11463 /**
11464 * Set the current page by symbolic name.
11465 *
11466 * @fires set
11467 * @param {string} name Symbolic name of page
11468 */
11469 OO.ui.BookletLayout.prototype.setPage = function ( name ) {
11470 var selectedItem,
11471 $focused,
11472 page = this.pages[ name ],
11473 previousPage = this.currentPageName && this.pages[ this.currentPageName ];
11474
11475 if ( name !== this.currentPageName ) {
11476 if ( this.outlined ) {
11477 selectedItem = this.outlineSelectWidget.getSelectedItem();
11478 if ( selectedItem && selectedItem.getData() !== name ) {
11479 this.outlineSelectWidget.selectItemByData( name );
11480 }
11481 }
11482 if ( page ) {
11483 if ( previousPage ) {
11484 previousPage.setActive( false );
11485 // Blur anything focused if the next page doesn't have anything focusable.
11486 // This is not needed if the next page has something focusable (because once it is focused
11487 // this blur happens automatically). If the layout is non-continuous, this check is
11488 // meaningless because the next page is not visible yet and thus can't hold focus.
11489 if (
11490 this.autoFocus &&
11491 this.stackLayout.continuous &&
11492 OO.ui.findFocusable( page.$element ).length !== 0
11493 ) {
11494 $focused = previousPage.$element.find( ':focus' );
11495 if ( $focused.length ) {
11496 $focused[ 0 ].blur();
11497 }
11498 }
11499 }
11500 this.currentPageName = name;
11501 page.setActive( true );
11502 this.stackLayout.setItem( page );
11503 if ( !this.stackLayout.continuous && previousPage ) {
11504 // This should not be necessary, since any inputs on the previous page should have been
11505 // blurred when it was hidden, but browsers are not very consistent about this.
11506 $focused = previousPage.$element.find( ':focus' );
11507 if ( $focused.length ) {
11508 $focused[ 0 ].blur();
11509 }
11510 }
11511 this.emit( 'set', page );
11512 }
11513 }
11514 };
11515
11516 /**
11517 * Select the first selectable page.
11518 *
11519 * @chainable
11520 */
11521 OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () {
11522 if ( !this.outlineSelectWidget.getSelectedItem() ) {
11523 this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getFirstSelectableItem() );
11524 }
11525
11526 return this;
11527 };
11528
11529 /**
11530 * IndexLayouts contain {@link OO.ui.CardLayout card layouts} as well as
11531 * {@link OO.ui.TabSelectWidget tabs} that allow users to easily navigate through the cards and
11532 * select which one to display. By default, only one card is displayed at a time. When a user
11533 * navigates to a new card, the index layout automatically focuses on the first focusable element,
11534 * unless the default setting is changed.
11535 *
11536 * TODO: This class is similar to BookletLayout, we may want to refactor to reduce duplication
11537 *
11538 * @example
11539 * // Example of a IndexLayout that contains two CardLayouts.
11540 *
11541 * function CardOneLayout( name, config ) {
11542 * CardOneLayout.parent.call( this, name, config );
11543 * this.$element.append( '<p>First card</p>' );
11544 * }
11545 * OO.inheritClass( CardOneLayout, OO.ui.CardLayout );
11546 * CardOneLayout.prototype.setupTabItem = function () {
11547 * this.tabItem.setLabel( 'Card one' );
11548 * };
11549 *
11550 * var card1 = new CardOneLayout( 'one' ),
11551 * card2 = new CardLayout( 'two', { label: 'Card two' } );
11552 *
11553 * card2.$element.append( '<p>Second card</p>' );
11554 *
11555 * var index = new OO.ui.IndexLayout();
11556 *
11557 * index.addCards ( [ card1, card2 ] );
11558 * $( 'body' ).append( index.$element );
11559 *
11560 * @class
11561 * @extends OO.ui.MenuLayout
11562 *
11563 * @constructor
11564 * @param {Object} [config] Configuration options
11565 * @cfg {boolean} [continuous=false] Show all cards, one after another
11566 * @cfg {boolean} [expanded=true] Expand the content panel to fill the entire parent element.
11567 * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when a new card is displayed.
11568 */
11569 OO.ui.IndexLayout = function OoUiIndexLayout( config ) {
11570 // Configuration initialization
11571 config = $.extend( {}, config, { menuPosition: 'top' } );
11572
11573 // Parent constructor
11574 OO.ui.IndexLayout.parent.call( this, config );
11575
11576 // Properties
11577 this.currentCardName = null;
11578 this.cards = {};
11579 this.ignoreFocus = false;
11580 this.stackLayout = new OO.ui.StackLayout( {
11581 continuous: !!config.continuous,
11582 expanded: config.expanded
11583 } );
11584 this.$content.append( this.stackLayout.$element );
11585 this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
11586
11587 this.tabSelectWidget = new OO.ui.TabSelectWidget();
11588 this.tabPanel = new OO.ui.PanelLayout();
11589 this.$menu.append( this.tabPanel.$element );
11590
11591 this.toggleMenu( true );
11592
11593 // Events
11594 this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
11595 this.tabSelectWidget.connect( this, { select: 'onTabSelectWidgetSelect' } );
11596 if ( this.autoFocus ) {
11597 // Event 'focus' does not bubble, but 'focusin' does
11598 this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
11599 }
11600
11601 // Initialization
11602 this.$element.addClass( 'oo-ui-indexLayout' );
11603 this.stackLayout.$element.addClass( 'oo-ui-indexLayout-stackLayout' );
11604 this.tabPanel.$element
11605 .addClass( 'oo-ui-indexLayout-tabPanel' )
11606 .append( this.tabSelectWidget.$element );
11607 };
11608
11609 /* Setup */
11610
11611 OO.inheritClass( OO.ui.IndexLayout, OO.ui.MenuLayout );
11612
11613 /* Events */
11614
11615 /**
11616 * A 'set' event is emitted when a card is {@link #setCard set} to be displayed by the index layout.
11617 * @event set
11618 * @param {OO.ui.CardLayout} card Current card
11619 */
11620
11621 /**
11622 * An 'add' event is emitted when cards are {@link #addCards added} to the index layout.
11623 *
11624 * @event add
11625 * @param {OO.ui.CardLayout[]} card Added cards
11626 * @param {number} index Index cards were added at
11627 */
11628
11629 /**
11630 * A 'remove' event is emitted when cards are {@link #clearCards cleared} or
11631 * {@link #removeCards removed} from the index.
11632 *
11633 * @event remove
11634 * @param {OO.ui.CardLayout[]} cards Removed cards
11635 */
11636
11637 /* Methods */
11638
11639 /**
11640 * Handle stack layout focus.
11641 *
11642 * @private
11643 * @param {jQuery.Event} e Focusin event
11644 */
11645 OO.ui.IndexLayout.prototype.onStackLayoutFocus = function ( e ) {
11646 var name, $target;
11647
11648 // Find the card that an element was focused within
11649 $target = $( e.target ).closest( '.oo-ui-cardLayout' );
11650 for ( name in this.cards ) {
11651 // Check for card match, exclude current card to find only card changes
11652 if ( this.cards[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentCardName ) {
11653 this.setCard( name );
11654 break;
11655 }
11656 }
11657 };
11658
11659 /**
11660 * Handle stack layout set events.
11661 *
11662 * @private
11663 * @param {OO.ui.PanelLayout|null} card The card panel that is now the current panel
11664 */
11665 OO.ui.IndexLayout.prototype.onStackLayoutSet = function ( card ) {
11666 var layout = this;
11667 if ( card ) {
11668 card.scrollElementIntoView( { complete: function () {
11669 if ( layout.autoFocus ) {
11670 layout.focus();
11671 }
11672 } } );
11673 }
11674 };
11675
11676 /**
11677 * Focus the first input in the current card.
11678 *
11679 * If no card is selected, the first selectable card will be selected.
11680 * If the focus is already in an element on the current card, nothing will happen.
11681 * @param {number} [itemIndex] A specific item to focus on
11682 */
11683 OO.ui.IndexLayout.prototype.focus = function ( itemIndex ) {
11684 var card,
11685 items = this.stackLayout.getItems();
11686
11687 if ( itemIndex !== undefined && items[ itemIndex ] ) {
11688 card = items[ itemIndex ];
11689 } else {
11690 card = this.stackLayout.getCurrentItem();
11691 }
11692
11693 if ( !card ) {
11694 this.selectFirstSelectableCard();
11695 card = this.stackLayout.getCurrentItem();
11696 }
11697 if ( !card ) {
11698 return;
11699 }
11700 // Only change the focus if is not already in the current page
11701 if ( !OO.ui.contains( card.$element[ 0 ], this.getElementDocument().activeElement, true ) ) {
11702 card.focus();
11703 }
11704 };
11705
11706 /**
11707 * Find the first focusable input in the index layout and focus
11708 * on it.
11709 */
11710 OO.ui.IndexLayout.prototype.focusFirstFocusable = function () {
11711 OO.ui.findFocusable( this.stackLayout.$element ).focus();
11712 };
11713
11714 /**
11715 * Handle tab widget select events.
11716 *
11717 * @private
11718 * @param {OO.ui.OptionWidget|null} item Selected item
11719 */
11720 OO.ui.IndexLayout.prototype.onTabSelectWidgetSelect = function ( item ) {
11721 if ( item ) {
11722 this.setCard( item.getData() );
11723 }
11724 };
11725
11726 /**
11727 * Get the card closest to the specified card.
11728 *
11729 * @param {OO.ui.CardLayout} card Card to use as a reference point
11730 * @return {OO.ui.CardLayout|null} Card closest to the specified card
11731 */
11732 OO.ui.IndexLayout.prototype.getClosestCard = function ( card ) {
11733 var next, prev, level,
11734 cards = this.stackLayout.getItems(),
11735 index = cards.indexOf( card );
11736
11737 if ( index !== -1 ) {
11738 next = cards[ index + 1 ];
11739 prev = cards[ index - 1 ];
11740 // Prefer adjacent cards at the same level
11741 level = this.tabSelectWidget.getItemFromData( card.getName() ).getLevel();
11742 if (
11743 prev &&
11744 level === this.tabSelectWidget.getItemFromData( prev.getName() ).getLevel()
11745 ) {
11746 return prev;
11747 }
11748 if (
11749 next &&
11750 level === this.tabSelectWidget.getItemFromData( next.getName() ).getLevel()
11751 ) {
11752 return next;
11753 }
11754 }
11755 return prev || next || null;
11756 };
11757
11758 /**
11759 * Get the tabs widget.
11760 *
11761 * @return {OO.ui.TabSelectWidget} Tabs widget
11762 */
11763 OO.ui.IndexLayout.prototype.getTabs = function () {
11764 return this.tabSelectWidget;
11765 };
11766
11767 /**
11768 * Get a card by its symbolic name.
11769 *
11770 * @param {string} name Symbolic name of card
11771 * @return {OO.ui.CardLayout|undefined} Card, if found
11772 */
11773 OO.ui.IndexLayout.prototype.getCard = function ( name ) {
11774 return this.cards[ name ];
11775 };
11776
11777 /**
11778 * Get the current card.
11779 *
11780 * @return {OO.ui.CardLayout|undefined} Current card, if found
11781 */
11782 OO.ui.IndexLayout.prototype.getCurrentCard = function () {
11783 var name = this.getCurrentCardName();
11784 return name ? this.getCard( name ) : undefined;
11785 };
11786
11787 /**
11788 * Get the symbolic name of the current card.
11789 *
11790 * @return {string|null} Symbolic name of the current card
11791 */
11792 OO.ui.IndexLayout.prototype.getCurrentCardName = function () {
11793 return this.currentCardName;
11794 };
11795
11796 /**
11797 * Add cards to the index layout
11798 *
11799 * When cards are added with the same names as existing cards, the existing cards will be
11800 * automatically removed before the new cards are added.
11801 *
11802 * @param {OO.ui.CardLayout[]} cards Cards to add
11803 * @param {number} index Index of the insertion point
11804 * @fires add
11805 * @chainable
11806 */
11807 OO.ui.IndexLayout.prototype.addCards = function ( cards, index ) {
11808 var i, len, name, card, item, currentIndex,
11809 stackLayoutCards = this.stackLayout.getItems(),
11810 remove = [],
11811 items = [];
11812
11813 // Remove cards with same names
11814 for ( i = 0, len = cards.length; i < len; i++ ) {
11815 card = cards[ i ];
11816 name = card.getName();
11817
11818 if ( Object.prototype.hasOwnProperty.call( this.cards, name ) ) {
11819 // Correct the insertion index
11820 currentIndex = stackLayoutCards.indexOf( this.cards[ name ] );
11821 if ( currentIndex !== -1 && currentIndex + 1 < index ) {
11822 index--;
11823 }
11824 remove.push( this.cards[ name ] );
11825 }
11826 }
11827 if ( remove.length ) {
11828 this.removeCards( remove );
11829 }
11830
11831 // Add new cards
11832 for ( i = 0, len = cards.length; i < len; i++ ) {
11833 card = cards[ i ];
11834 name = card.getName();
11835 this.cards[ card.getName() ] = card;
11836 item = new OO.ui.TabOptionWidget( { data: name } );
11837 card.setTabItem( item );
11838 items.push( item );
11839 }
11840
11841 if ( items.length ) {
11842 this.tabSelectWidget.addItems( items, index );
11843 this.selectFirstSelectableCard();
11844 }
11845 this.stackLayout.addItems( cards, index );
11846 this.emit( 'add', cards, index );
11847
11848 return this;
11849 };
11850
11851 /**
11852 * Remove the specified cards from the index layout.
11853 *
11854 * To remove all cards from the index, you may wish to use the #clearCards method instead.
11855 *
11856 * @param {OO.ui.CardLayout[]} cards An array of cards to remove
11857 * @fires remove
11858 * @chainable
11859 */
11860 OO.ui.IndexLayout.prototype.removeCards = function ( cards ) {
11861 var i, len, name, card,
11862 items = [];
11863
11864 for ( i = 0, len = cards.length; i < len; i++ ) {
11865 card = cards[ i ];
11866 name = card.getName();
11867 delete this.cards[ name ];
11868 items.push( this.tabSelectWidget.getItemFromData( name ) );
11869 card.setTabItem( null );
11870 }
11871 if ( items.length ) {
11872 this.tabSelectWidget.removeItems( items );
11873 this.selectFirstSelectableCard();
11874 }
11875 this.stackLayout.removeItems( cards );
11876 this.emit( 'remove', cards );
11877
11878 return this;
11879 };
11880
11881 /**
11882 * Clear all cards from the index layout.
11883 *
11884 * To remove only a subset of cards from the index, use the #removeCards method.
11885 *
11886 * @fires remove
11887 * @chainable
11888 */
11889 OO.ui.IndexLayout.prototype.clearCards = function () {
11890 var i, len,
11891 cards = this.stackLayout.getItems();
11892
11893 this.cards = {};
11894 this.currentCardName = null;
11895 this.tabSelectWidget.clearItems();
11896 for ( i = 0, len = cards.length; i < len; i++ ) {
11897 cards[ i ].setTabItem( null );
11898 }
11899 this.stackLayout.clearItems();
11900
11901 this.emit( 'remove', cards );
11902
11903 return this;
11904 };
11905
11906 /**
11907 * Set the current card by symbolic name.
11908 *
11909 * @fires set
11910 * @param {string} name Symbolic name of card
11911 */
11912 OO.ui.IndexLayout.prototype.setCard = function ( name ) {
11913 var selectedItem,
11914 $focused,
11915 card = this.cards[ name ],
11916 previousCard = this.currentCardName && this.cards[ this.currentCardName ];
11917
11918 if ( name !== this.currentCardName ) {
11919 selectedItem = this.tabSelectWidget.getSelectedItem();
11920 if ( selectedItem && selectedItem.getData() !== name ) {
11921 this.tabSelectWidget.selectItemByData( name );
11922 }
11923 if ( card ) {
11924 if ( previousCard ) {
11925 previousCard.setActive( false );
11926 // Blur anything focused if the next card doesn't have anything focusable.
11927 // This is not needed if the next card has something focusable (because once it is focused
11928 // this blur happens automatically). If the layout is non-continuous, this check is
11929 // meaningless because the next card is not visible yet and thus can't hold focus.
11930 if (
11931 this.autoFocus &&
11932 this.stackLayout.continuous &&
11933 OO.ui.findFocusable( card.$element ).length !== 0
11934 ) {
11935 $focused = previousCard.$element.find( ':focus' );
11936 if ( $focused.length ) {
11937 $focused[ 0 ].blur();
11938 }
11939 }
11940 }
11941 this.currentCardName = name;
11942 card.setActive( true );
11943 this.stackLayout.setItem( card );
11944 if ( !this.stackLayout.continuous && previousCard ) {
11945 // This should not be necessary, since any inputs on the previous card should have been
11946 // blurred when it was hidden, but browsers are not very consistent about this.
11947 $focused = previousCard.$element.find( ':focus' );
11948 if ( $focused.length ) {
11949 $focused[ 0 ].blur();
11950 }
11951 }
11952 this.emit( 'set', card );
11953 }
11954 }
11955 };
11956
11957 /**
11958 * Select the first selectable card.
11959 *
11960 * @chainable
11961 */
11962 OO.ui.IndexLayout.prototype.selectFirstSelectableCard = function () {
11963 if ( !this.tabSelectWidget.getSelectedItem() ) {
11964 this.tabSelectWidget.selectItem( this.tabSelectWidget.getFirstSelectableItem() );
11965 }
11966
11967 return this;
11968 };
11969
11970 /**
11971 * ToggleWidget implements basic behavior of widgets with an on/off state.
11972 * Please see OO.ui.ToggleButtonWidget and OO.ui.ToggleSwitchWidget for examples.
11973 *
11974 * @abstract
11975 * @class
11976 * @extends OO.ui.Widget
11977 *
11978 * @constructor
11979 * @param {Object} [config] Configuration options
11980 * @cfg {boolean} [value=false] The toggle’s initial on/off state.
11981 * By default, the toggle is in the 'off' state.
11982 */
11983 OO.ui.ToggleWidget = function OoUiToggleWidget( config ) {
11984 // Configuration initialization
11985 config = config || {};
11986
11987 // Parent constructor
11988 OO.ui.ToggleWidget.parent.call( this, config );
11989
11990 // Properties
11991 this.value = null;
11992
11993 // Initialization
11994 this.$element.addClass( 'oo-ui-toggleWidget' );
11995 this.setValue( !!config.value );
11996 };
11997
11998 /* Setup */
11999
12000 OO.inheritClass( OO.ui.ToggleWidget, OO.ui.Widget );
12001
12002 /* Events */
12003
12004 /**
12005 * @event change
12006 *
12007 * A change event is emitted when the on/off state of the toggle changes.
12008 *
12009 * @param {boolean} value Value representing the new state of the toggle
12010 */
12011
12012 /* Methods */
12013
12014 /**
12015 * Get the value representing the toggle’s state.
12016 *
12017 * @return {boolean} The on/off state of the toggle
12018 */
12019 OO.ui.ToggleWidget.prototype.getValue = function () {
12020 return this.value;
12021 };
12022
12023 /**
12024 * Set the state of the toggle: `true` for 'on', `false' for 'off'.
12025 *
12026 * @param {boolean} value The state of the toggle
12027 * @fires change
12028 * @chainable
12029 */
12030 OO.ui.ToggleWidget.prototype.setValue = function ( value ) {
12031 value = !!value;
12032 if ( this.value !== value ) {
12033 this.value = value;
12034 this.emit( 'change', value );
12035 this.$element.toggleClass( 'oo-ui-toggleWidget-on', value );
12036 this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value );
12037 this.$element.attr( 'aria-checked', value.toString() );
12038 }
12039 return this;
12040 };
12041
12042 /**
12043 * ToggleButtons are buttons that have a state (‘on’ or ‘off’) that is represented by a
12044 * Boolean value. Like other {@link OO.ui.ButtonWidget buttons}, toggle buttons can be
12045 * configured with {@link OO.ui.mixin.IconElement icons}, {@link OO.ui.mixin.IndicatorElement indicators},
12046 * {@link OO.ui.mixin.TitledElement titles}, {@link OO.ui.mixin.FlaggedElement styling flags},
12047 * and {@link OO.ui.mixin.LabelElement labels}. Please see
12048 * the [OOjs UI documentation][1] on MediaWiki for more information.
12049 *
12050 * @example
12051 * // Toggle buttons in the 'off' and 'on' state.
12052 * var toggleButton1 = new OO.ui.ToggleButtonWidget( {
12053 * label: 'Toggle Button off'
12054 * } );
12055 * var toggleButton2 = new OO.ui.ToggleButtonWidget( {
12056 * label: 'Toggle Button on',
12057 * value: true
12058 * } );
12059 * // Append the buttons to the DOM.
12060 * $( 'body' ).append( toggleButton1.$element, toggleButton2.$element );
12061 *
12062 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Buttons_and_Switches#Toggle_buttons
12063 *
12064 * @class
12065 * @extends OO.ui.ToggleWidget
12066 * @mixins OO.ui.mixin.ButtonElement
12067 * @mixins OO.ui.mixin.IconElement
12068 * @mixins OO.ui.mixin.IndicatorElement
12069 * @mixins OO.ui.mixin.LabelElement
12070 * @mixins OO.ui.mixin.TitledElement
12071 * @mixins OO.ui.mixin.FlaggedElement
12072 * @mixins OO.ui.mixin.TabIndexedElement
12073 *
12074 * @constructor
12075 * @param {Object} [config] Configuration options
12076 * @cfg {boolean} [value=false] The toggle button’s initial on/off
12077 * state. By default, the button is in the 'off' state.
12078 */
12079 OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) {
12080 // Configuration initialization
12081 config = config || {};
12082
12083 // Parent constructor
12084 OO.ui.ToggleButtonWidget.parent.call( this, config );
12085
12086 // Mixin constructors
12087 OO.ui.mixin.ButtonElement.call( this, config );
12088 OO.ui.mixin.IconElement.call( this, config );
12089 OO.ui.mixin.IndicatorElement.call( this, config );
12090 OO.ui.mixin.LabelElement.call( this, config );
12091 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
12092 OO.ui.mixin.FlaggedElement.call( this, config );
12093 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
12094
12095 // Events
12096 this.connect( this, { click: 'onAction' } );
12097
12098 // Initialization
12099 this.$button.append( this.$icon, this.$label, this.$indicator );
12100 this.$element
12101 .addClass( 'oo-ui-toggleButtonWidget' )
12102 .append( this.$button );
12103 };
12104
12105 /* Setup */
12106
12107 OO.inheritClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget );
12108 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.ButtonElement );
12109 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.IconElement );
12110 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.IndicatorElement );
12111 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.LabelElement );
12112 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.TitledElement );
12113 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.FlaggedElement );
12114 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.mixin.TabIndexedElement );
12115
12116 /* Methods */
12117
12118 /**
12119 * Handle the button action being triggered.
12120 *
12121 * @private
12122 */
12123 OO.ui.ToggleButtonWidget.prototype.onAction = function () {
12124 this.setValue( !this.value );
12125 };
12126
12127 /**
12128 * @inheritdoc
12129 */
12130 OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
12131 value = !!value;
12132 if ( value !== this.value ) {
12133 // Might be called from parent constructor before ButtonElement constructor
12134 if ( this.$button ) {
12135 this.$button.attr( 'aria-pressed', value.toString() );
12136 }
12137 this.setActive( value );
12138 }
12139
12140 // Parent method
12141 OO.ui.ToggleButtonWidget.parent.prototype.setValue.call( this, value );
12142
12143 return this;
12144 };
12145
12146 /**
12147 * @inheritdoc
12148 */
12149 OO.ui.ToggleButtonWidget.prototype.setButtonElement = function ( $button ) {
12150 if ( this.$button ) {
12151 this.$button.removeAttr( 'aria-pressed' );
12152 }
12153 OO.ui.mixin.ButtonElement.prototype.setButtonElement.call( this, $button );
12154 this.$button.attr( 'aria-pressed', this.value.toString() );
12155 };
12156
12157 /**
12158 * ToggleSwitches are switches that slide on and off. Their state is represented by a Boolean
12159 * value (`true` for ‘on’, and `false` otherwise, the default). The ‘off’ state is represented
12160 * visually by a slider in the leftmost position.
12161 *
12162 * @example
12163 * // Toggle switches in the 'off' and 'on' position.
12164 * var toggleSwitch1 = new OO.ui.ToggleSwitchWidget();
12165 * var toggleSwitch2 = new OO.ui.ToggleSwitchWidget( {
12166 * value: true
12167 * } );
12168 *
12169 * // Create a FieldsetLayout to layout and label switches
12170 * var fieldset = new OO.ui.FieldsetLayout( {
12171 * label: 'Toggle switches'
12172 * } );
12173 * fieldset.addItems( [
12174 * new OO.ui.FieldLayout( toggleSwitch1, { label: 'Off', align: 'top' } ),
12175 * new OO.ui.FieldLayout( toggleSwitch2, { label: 'On', align: 'top' } )
12176 * ] );
12177 * $( 'body' ).append( fieldset.$element );
12178 *
12179 * @class
12180 * @extends OO.ui.ToggleWidget
12181 * @mixins OO.ui.mixin.TabIndexedElement
12182 *
12183 * @constructor
12184 * @param {Object} [config] Configuration options
12185 * @cfg {boolean} [value=false] The toggle switch’s initial on/off state.
12186 * By default, the toggle switch is in the 'off' position.
12187 */
12188 OO.ui.ToggleSwitchWidget = function OoUiToggleSwitchWidget( config ) {
12189 // Parent constructor
12190 OO.ui.ToggleSwitchWidget.parent.call( this, config );
12191
12192 // Mixin constructors
12193 OO.ui.mixin.TabIndexedElement.call( this, config );
12194
12195 // Properties
12196 this.dragging = false;
12197 this.dragStart = null;
12198 this.sliding = false;
12199 this.$glow = $( '<span>' );
12200 this.$grip = $( '<span>' );
12201
12202 // Events
12203 this.$element.on( {
12204 click: this.onClick.bind( this ),
12205 keypress: this.onKeyPress.bind( this )
12206 } );
12207
12208 // Initialization
12209 this.$glow.addClass( 'oo-ui-toggleSwitchWidget-glow' );
12210 this.$grip.addClass( 'oo-ui-toggleSwitchWidget-grip' );
12211 this.$element
12212 .addClass( 'oo-ui-toggleSwitchWidget' )
12213 .attr( 'role', 'checkbox' )
12214 .append( this.$glow, this.$grip );
12215 };
12216
12217 /* Setup */
12218
12219 OO.inheritClass( OO.ui.ToggleSwitchWidget, OO.ui.ToggleWidget );
12220 OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.mixin.TabIndexedElement );
12221
12222 /* Methods */
12223
12224 /**
12225 * Handle mouse click events.
12226 *
12227 * @private
12228 * @param {jQuery.Event} e Mouse click event
12229 */
12230 OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
12231 if ( !this.isDisabled() && e.which === OO.ui.MouseButtons.LEFT ) {
12232 this.setValue( !this.value );
12233 }
12234 return false;
12235 };
12236
12237 /**
12238 * Handle key press events.
12239 *
12240 * @private
12241 * @param {jQuery.Event} e Key press event
12242 */
12243 OO.ui.ToggleSwitchWidget.prototype.onKeyPress = function ( e ) {
12244 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
12245 this.setValue( !this.value );
12246 return false;
12247 }
12248 };
12249
12250 /**
12251 * OutlineControlsWidget is a set of controls for an {@link OO.ui.OutlineSelectWidget outline select widget}.
12252 * Controls include moving items up and down, removing items, and adding different kinds of items.
12253 *
12254 * **Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.**
12255 *
12256 * @class
12257 * @extends OO.ui.Widget
12258 * @mixins OO.ui.mixin.GroupElement
12259 * @mixins OO.ui.mixin.IconElement
12260 *
12261 * @constructor
12262 * @param {OO.ui.OutlineSelectWidget} outline Outline to control
12263 * @param {Object} [config] Configuration options
12264 * @cfg {Object} [abilities] List of abilties
12265 * @cfg {boolean} [abilities.move=true] Allow moving movable items
12266 * @cfg {boolean} [abilities.remove=true] Allow removing removable items
12267 */
12268 OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
12269 // Allow passing positional parameters inside the config object
12270 if ( OO.isPlainObject( outline ) && config === undefined ) {
12271 config = outline;
12272 outline = config.outline;
12273 }
12274
12275 // Configuration initialization
12276 config = $.extend( { icon: 'add' }, config );
12277
12278 // Parent constructor
12279 OO.ui.OutlineControlsWidget.parent.call( this, config );
12280
12281 // Mixin constructors
12282 OO.ui.mixin.GroupElement.call( this, config );
12283 OO.ui.mixin.IconElement.call( this, config );
12284
12285 // Properties
12286 this.outline = outline;
12287 this.$movers = $( '<div>' );
12288 this.upButton = new OO.ui.ButtonWidget( {
12289 framed: false,
12290 icon: 'collapse',
12291 title: OO.ui.msg( 'ooui-outline-control-move-up' )
12292 } );
12293 this.downButton = new OO.ui.ButtonWidget( {
12294 framed: false,
12295 icon: 'expand',
12296 title: OO.ui.msg( 'ooui-outline-control-move-down' )
12297 } );
12298 this.removeButton = new OO.ui.ButtonWidget( {
12299 framed: false,
12300 icon: 'remove',
12301 title: OO.ui.msg( 'ooui-outline-control-remove' )
12302 } );
12303 this.abilities = { move: true, remove: true };
12304
12305 // Events
12306 outline.connect( this, {
12307 select: 'onOutlineChange',
12308 add: 'onOutlineChange',
12309 remove: 'onOutlineChange'
12310 } );
12311 this.upButton.connect( this, { click: [ 'emit', 'move', -1 ] } );
12312 this.downButton.connect( this, { click: [ 'emit', 'move', 1 ] } );
12313 this.removeButton.connect( this, { click: [ 'emit', 'remove' ] } );
12314
12315 // Initialization
12316 this.$element.addClass( 'oo-ui-outlineControlsWidget' );
12317 this.$group.addClass( 'oo-ui-outlineControlsWidget-items' );
12318 this.$movers
12319 .addClass( 'oo-ui-outlineControlsWidget-movers' )
12320 .append( this.removeButton.$element, this.upButton.$element, this.downButton.$element );
12321 this.$element.append( this.$icon, this.$group, this.$movers );
12322 this.setAbilities( config.abilities || {} );
12323 };
12324
12325 /* Setup */
12326
12327 OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget );
12328 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.mixin.GroupElement );
12329 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.mixin.IconElement );
12330
12331 /* Events */
12332
12333 /**
12334 * @event move
12335 * @param {number} places Number of places to move
12336 */
12337
12338 /**
12339 * @event remove
12340 */
12341
12342 /* Methods */
12343
12344 /**
12345 * Set abilities.
12346 *
12347 * @param {Object} abilities List of abilties
12348 * @param {boolean} [abilities.move] Allow moving movable items
12349 * @param {boolean} [abilities.remove] Allow removing removable items
12350 */
12351 OO.ui.OutlineControlsWidget.prototype.setAbilities = function ( abilities ) {
12352 var ability;
12353
12354 for ( ability in this.abilities ) {
12355 if ( abilities[ ability ] !== undefined ) {
12356 this.abilities[ ability ] = !!abilities[ ability ];
12357 }
12358 }
12359
12360 this.onOutlineChange();
12361 };
12362
12363 /**
12364 * @private
12365 * Handle outline change events.
12366 */
12367 OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () {
12368 var i, len, firstMovable, lastMovable,
12369 items = this.outline.getItems(),
12370 selectedItem = this.outline.getSelectedItem(),
12371 movable = this.abilities.move && selectedItem && selectedItem.isMovable(),
12372 removable = this.abilities.remove && selectedItem && selectedItem.isRemovable();
12373
12374 if ( movable ) {
12375 i = -1;
12376 len = items.length;
12377 while ( ++i < len ) {
12378 if ( items[ i ].isMovable() ) {
12379 firstMovable = items[ i ];
12380 break;
12381 }
12382 }
12383 i = len;
12384 while ( i-- ) {
12385 if ( items[ i ].isMovable() ) {
12386 lastMovable = items[ i ];
12387 break;
12388 }
12389 }
12390 }
12391 this.upButton.setDisabled( !movable || selectedItem === firstMovable );
12392 this.downButton.setDisabled( !movable || selectedItem === lastMovable );
12393 this.removeButton.setDisabled( !removable );
12394 };
12395
12396 /**
12397 * OutlineOptionWidget is an item in an {@link OO.ui.OutlineSelectWidget OutlineSelectWidget}.
12398 *
12399 * Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}, which contain
12400 * {@link OO.ui.PageLayout page layouts}. See {@link OO.ui.BookletLayout BookletLayout}
12401 * for an example.
12402 *
12403 * @class
12404 * @extends OO.ui.DecoratedOptionWidget
12405 *
12406 * @constructor
12407 * @param {Object} [config] Configuration options
12408 * @cfg {number} [level] Indentation level
12409 * @cfg {boolean} [movable] Allow modification from {@link OO.ui.OutlineControlsWidget outline controls}.
12410 */
12411 OO.ui.OutlineOptionWidget = function OoUiOutlineOptionWidget( config ) {
12412 // Configuration initialization
12413 config = config || {};
12414
12415 // Parent constructor
12416 OO.ui.OutlineOptionWidget.parent.call( this, config );
12417
12418 // Properties
12419 this.level = 0;
12420 this.movable = !!config.movable;
12421 this.removable = !!config.removable;
12422
12423 // Initialization
12424 this.$element.addClass( 'oo-ui-outlineOptionWidget' );
12425 this.setLevel( config.level );
12426 };
12427
12428 /* Setup */
12429
12430 OO.inheritClass( OO.ui.OutlineOptionWidget, OO.ui.DecoratedOptionWidget );
12431
12432 /* Static Properties */
12433
12434 OO.ui.OutlineOptionWidget.static.highlightable = false;
12435
12436 OO.ui.OutlineOptionWidget.static.scrollIntoViewOnSelect = true;
12437
12438 OO.ui.OutlineOptionWidget.static.levelClass = 'oo-ui-outlineOptionWidget-level-';
12439
12440 OO.ui.OutlineOptionWidget.static.levels = 3;
12441
12442 /* Methods */
12443
12444 /**
12445 * Check if item is movable.
12446 *
12447 * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
12448 *
12449 * @return {boolean} Item is movable
12450 */
12451 OO.ui.OutlineOptionWidget.prototype.isMovable = function () {
12452 return this.movable;
12453 };
12454
12455 /**
12456 * Check if item is removable.
12457 *
12458 * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
12459 *
12460 * @return {boolean} Item is removable
12461 */
12462 OO.ui.OutlineOptionWidget.prototype.isRemovable = function () {
12463 return this.removable;
12464 };
12465
12466 /**
12467 * Get indentation level.
12468 *
12469 * @return {number} Indentation level
12470 */
12471 OO.ui.OutlineOptionWidget.prototype.getLevel = function () {
12472 return this.level;
12473 };
12474
12475 /**
12476 * Set movability.
12477 *
12478 * Movability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
12479 *
12480 * @param {boolean} movable Item is movable
12481 * @chainable
12482 */
12483 OO.ui.OutlineOptionWidget.prototype.setMovable = function ( movable ) {
12484 this.movable = !!movable;
12485 this.updateThemeClasses();
12486 return this;
12487 };
12488
12489 /**
12490 * Set removability.
12491 *
12492 * Removability is used by {@link OO.ui.OutlineControlsWidget outline controls}.
12493 *
12494 * @param {boolean} removable Item is removable
12495 * @chainable
12496 */
12497 OO.ui.OutlineOptionWidget.prototype.setRemovable = function ( removable ) {
12498 this.removable = !!removable;
12499 this.updateThemeClasses();
12500 return this;
12501 };
12502
12503 /**
12504 * Set indentation level.
12505 *
12506 * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
12507 * @chainable
12508 */
12509 OO.ui.OutlineOptionWidget.prototype.setLevel = function ( level ) {
12510 var levels = this.constructor.static.levels,
12511 levelClass = this.constructor.static.levelClass,
12512 i = levels;
12513
12514 this.level = level ? Math.max( 0, Math.min( levels - 1, level ) ) : 0;
12515 while ( i-- ) {
12516 if ( this.level === i ) {
12517 this.$element.addClass( levelClass + i );
12518 } else {
12519 this.$element.removeClass( levelClass + i );
12520 }
12521 }
12522 this.updateThemeClasses();
12523
12524 return this;
12525 };
12526
12527 /**
12528 * OutlineSelectWidget is a structured list that contains {@link OO.ui.OutlineOptionWidget outline options}
12529 * A set of controls can be provided with an {@link OO.ui.OutlineControlsWidget outline controls} widget.
12530 *
12531 * **Currently, this class is only used by {@link OO.ui.BookletLayout booklet layouts}.**
12532 *
12533 * @class
12534 * @extends OO.ui.SelectWidget
12535 * @mixins OO.ui.mixin.TabIndexedElement
12536 *
12537 * @constructor
12538 * @param {Object} [config] Configuration options
12539 */
12540 OO.ui.OutlineSelectWidget = function OoUiOutlineSelectWidget( config ) {
12541 // Parent constructor
12542 OO.ui.OutlineSelectWidget.parent.call( this, config );
12543
12544 // Mixin constructors
12545 OO.ui.mixin.TabIndexedElement.call( this, config );
12546
12547 // Events
12548 this.$element.on( {
12549 focus: this.bindKeyDownListener.bind( this ),
12550 blur: this.unbindKeyDownListener.bind( this )
12551 } );
12552
12553 // Initialization
12554 this.$element.addClass( 'oo-ui-outlineSelectWidget' );
12555 };
12556
12557 /* Setup */
12558
12559 OO.inheritClass( OO.ui.OutlineSelectWidget, OO.ui.SelectWidget );
12560 OO.mixinClass( OO.ui.OutlineSelectWidget, OO.ui.mixin.TabIndexedElement );
12561
12562 /**
12563 * ButtonOptionWidget is a special type of {@link OO.ui.mixin.ButtonElement button element} that
12564 * can be selected and configured with data. The class is
12565 * used with OO.ui.ButtonSelectWidget to create a selection of button options. Please see the
12566 * [OOjs UI documentation on MediaWiki] [1] for more information.
12567 *
12568 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Button_selects_and_options
12569 *
12570 * @class
12571 * @extends OO.ui.DecoratedOptionWidget
12572 * @mixins OO.ui.mixin.ButtonElement
12573 * @mixins OO.ui.mixin.TabIndexedElement
12574 * @mixins OO.ui.mixin.TitledElement
12575 *
12576 * @constructor
12577 * @param {Object} [config] Configuration options
12578 */
12579 OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( config ) {
12580 // Configuration initialization
12581 config = config || {};
12582
12583 // Parent constructor
12584 OO.ui.ButtonOptionWidget.parent.call( this, config );
12585
12586 // Mixin constructors
12587 OO.ui.mixin.ButtonElement.call( this, config );
12588 OO.ui.mixin.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
12589 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, {
12590 $tabIndexed: this.$button,
12591 tabIndex: -1
12592 } ) );
12593
12594 // Initialization
12595 this.$element.addClass( 'oo-ui-buttonOptionWidget' );
12596 this.$button.append( this.$element.contents() );
12597 this.$element.append( this.$button );
12598 };
12599
12600 /* Setup */
12601
12602 OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.DecoratedOptionWidget );
12603 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.mixin.ButtonElement );
12604 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.mixin.TitledElement );
12605 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.mixin.TabIndexedElement );
12606
12607 /* Static Properties */
12608
12609 // Allow button mouse down events to pass through so they can be handled by the parent select widget
12610 OO.ui.ButtonOptionWidget.static.cancelButtonMouseDownEvents = false;
12611
12612 OO.ui.ButtonOptionWidget.static.highlightable = false;
12613
12614 /* Methods */
12615
12616 /**
12617 * @inheritdoc
12618 */
12619 OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) {
12620 OO.ui.ButtonOptionWidget.parent.prototype.setSelected.call( this, state );
12621
12622 if ( this.constructor.static.selectable ) {
12623 this.setActive( state );
12624 }
12625
12626 return this;
12627 };
12628
12629 /**
12630 * ButtonSelectWidget is a {@link OO.ui.SelectWidget select widget} that contains
12631 * button options and is used together with
12632 * OO.ui.ButtonOptionWidget. The ButtonSelectWidget provides an interface for
12633 * highlighting, choosing, and selecting mutually exclusive options. Please see
12634 * the [OOjs UI documentation on MediaWiki] [1] for more information.
12635 *
12636 * @example
12637 * // Example: A ButtonSelectWidget that contains three ButtonOptionWidgets
12638 * var option1 = new OO.ui.ButtonOptionWidget( {
12639 * data: 1,
12640 * label: 'Option 1',
12641 * title: 'Button option 1'
12642 * } );
12643 *
12644 * var option2 = new OO.ui.ButtonOptionWidget( {
12645 * data: 2,
12646 * label: 'Option 2',
12647 * title: 'Button option 2'
12648 * } );
12649 *
12650 * var option3 = new OO.ui.ButtonOptionWidget( {
12651 * data: 3,
12652 * label: 'Option 3',
12653 * title: 'Button option 3'
12654 * } );
12655 *
12656 * var buttonSelect=new OO.ui.ButtonSelectWidget( {
12657 * items: [ option1, option2, option3 ]
12658 * } );
12659 * $( 'body' ).append( buttonSelect.$element );
12660 *
12661 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options
12662 *
12663 * @class
12664 * @extends OO.ui.SelectWidget
12665 * @mixins OO.ui.mixin.TabIndexedElement
12666 *
12667 * @constructor
12668 * @param {Object} [config] Configuration options
12669 */
12670 OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
12671 // Parent constructor
12672 OO.ui.ButtonSelectWidget.parent.call( this, config );
12673
12674 // Mixin constructors
12675 OO.ui.mixin.TabIndexedElement.call( this, config );
12676
12677 // Events
12678 this.$element.on( {
12679 focus: this.bindKeyDownListener.bind( this ),
12680 blur: this.unbindKeyDownListener.bind( this )
12681 } );
12682
12683 // Initialization
12684 this.$element.addClass( 'oo-ui-buttonSelectWidget' );
12685 };
12686
12687 /* Setup */
12688
12689 OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
12690 OO.mixinClass( OO.ui.ButtonSelectWidget, OO.ui.mixin.TabIndexedElement );
12691
12692 /**
12693 * TabOptionWidget is an item in a {@link OO.ui.TabSelectWidget TabSelectWidget}.
12694 *
12695 * Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}, which contain
12696 * {@link OO.ui.CardLayout card layouts}. See {@link OO.ui.IndexLayout IndexLayout}
12697 * for an example.
12698 *
12699 * @class
12700 * @extends OO.ui.OptionWidget
12701 *
12702 * @constructor
12703 * @param {Object} [config] Configuration options
12704 */
12705 OO.ui.TabOptionWidget = function OoUiTabOptionWidget( config ) {
12706 // Configuration initialization
12707 config = config || {};
12708
12709 // Parent constructor
12710 OO.ui.TabOptionWidget.parent.call( this, config );
12711
12712 // Initialization
12713 this.$element.addClass( 'oo-ui-tabOptionWidget' );
12714 };
12715
12716 /* Setup */
12717
12718 OO.inheritClass( OO.ui.TabOptionWidget, OO.ui.OptionWidget );
12719
12720 /* Static Properties */
12721
12722 OO.ui.TabOptionWidget.static.highlightable = false;
12723
12724 /**
12725 * TabSelectWidget is a list that contains {@link OO.ui.TabOptionWidget tab options}
12726 *
12727 * **Currently, this class is only used by {@link OO.ui.IndexLayout index layouts}.**
12728 *
12729 * @class
12730 * @extends OO.ui.SelectWidget
12731 * @mixins OO.ui.mixin.TabIndexedElement
12732 *
12733 * @constructor
12734 * @param {Object} [config] Configuration options
12735 */
12736 OO.ui.TabSelectWidget = function OoUiTabSelectWidget( config ) {
12737 // Parent constructor
12738 OO.ui.TabSelectWidget.parent.call( this, config );
12739
12740 // Mixin constructors
12741 OO.ui.mixin.TabIndexedElement.call( this, config );
12742
12743 // Events
12744 this.$element.on( {
12745 focus: this.bindKeyDownListener.bind( this ),
12746 blur: this.unbindKeyDownListener.bind( this )
12747 } );
12748
12749 // Initialization
12750 this.$element.addClass( 'oo-ui-tabSelectWidget' );
12751 };
12752
12753 /* Setup */
12754
12755 OO.inheritClass( OO.ui.TabSelectWidget, OO.ui.SelectWidget );
12756 OO.mixinClass( OO.ui.TabSelectWidget, OO.ui.mixin.TabIndexedElement );
12757
12758 /**
12759 * CapsuleItemWidgets are used within a {@link OO.ui.CapsuleMultiSelectWidget
12760 * CapsuleMultiSelectWidget} to display the selected items.
12761 *
12762 * @class
12763 * @extends OO.ui.Widget
12764 * @mixins OO.ui.mixin.ItemWidget
12765 * @mixins OO.ui.mixin.IndicatorElement
12766 * @mixins OO.ui.mixin.LabelElement
12767 * @mixins OO.ui.mixin.FlaggedElement
12768 * @mixins OO.ui.mixin.TabIndexedElement
12769 *
12770 * @constructor
12771 * @param {Object} [config] Configuration options
12772 */
12773 OO.ui.CapsuleItemWidget = function OoUiCapsuleItemWidget( config ) {
12774 // Configuration initialization
12775 config = config || {};
12776
12777 // Parent constructor
12778 OO.ui.CapsuleItemWidget.parent.call( this, config );
12779
12780 // Properties (must be set before mixin constructor calls)
12781 this.$indicator = $( '<span>' );
12782
12783 // Mixin constructors
12784 OO.ui.mixin.ItemWidget.call( this );
12785 OO.ui.mixin.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$indicator, indicator: 'clear' } ) );
12786 OO.ui.mixin.LabelElement.call( this, config );
12787 OO.ui.mixin.FlaggedElement.call( this, config );
12788 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$indicator } ) );
12789
12790 // Events
12791 this.$indicator.on( {
12792 keydown: this.onCloseKeyDown.bind( this ),
12793 click: this.onCloseClick.bind( this )
12794 } );
12795
12796 // Initialization
12797 this.$element
12798 .addClass( 'oo-ui-capsuleItemWidget' )
12799 .append( this.$indicator, this.$label );
12800 };
12801
12802 /* Setup */
12803
12804 OO.inheritClass( OO.ui.CapsuleItemWidget, OO.ui.Widget );
12805 OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.ItemWidget );
12806 OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.IndicatorElement );
12807 OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.LabelElement );
12808 OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.FlaggedElement );
12809 OO.mixinClass( OO.ui.CapsuleItemWidget, OO.ui.mixin.TabIndexedElement );
12810
12811 /* Methods */
12812
12813 /**
12814 * Handle close icon clicks
12815 * @param {jQuery.Event} event
12816 */
12817 OO.ui.CapsuleItemWidget.prototype.onCloseClick = function () {
12818 var element = this.getElementGroup();
12819
12820 if ( !this.isDisabled() && element && $.isFunction( element.removeItems ) ) {
12821 element.removeItems( [ this ] );
12822 element.focus();
12823 }
12824 };
12825
12826 /**
12827 * Handle close keyboard events
12828 * @param {jQuery.Event} event Key down event
12829 */
12830 OO.ui.CapsuleItemWidget.prototype.onCloseKeyDown = function ( e ) {
12831 if ( !this.isDisabled() && $.isFunction( this.getElementGroup().removeItems ) ) {
12832 switch ( e.which ) {
12833 case OO.ui.Keys.ENTER:
12834 case OO.ui.Keys.BACKSPACE:
12835 case OO.ui.Keys.SPACE:
12836 this.getElementGroup().removeItems( [ this ] );
12837 return false;
12838 }
12839 }
12840 };
12841
12842 /**
12843 * CapsuleMultiSelectWidgets are something like a {@link OO.ui.ComboBoxInputWidget combo box widget}
12844 * that allows for selecting multiple values.
12845 *
12846 * For more information about menus and options, please see the [OOjs UI documentation on MediaWiki][1].
12847 *
12848 * @example
12849 * // Example: A CapsuleMultiSelectWidget.
12850 * var capsule = new OO.ui.CapsuleMultiSelectWidget( {
12851 * label: 'CapsuleMultiSelectWidget',
12852 * selected: [ 'Option 1', 'Option 3' ],
12853 * menu: {
12854 * items: [
12855 * new OO.ui.MenuOptionWidget( {
12856 * data: 'Option 1',
12857 * label: 'Option One'
12858 * } ),
12859 * new OO.ui.MenuOptionWidget( {
12860 * data: 'Option 2',
12861 * label: 'Option Two'
12862 * } ),
12863 * new OO.ui.MenuOptionWidget( {
12864 * data: 'Option 3',
12865 * label: 'Option Three'
12866 * } ),
12867 * new OO.ui.MenuOptionWidget( {
12868 * data: 'Option 4',
12869 * label: 'Option Four'
12870 * } ),
12871 * new OO.ui.MenuOptionWidget( {
12872 * data: 'Option 5',
12873 * label: 'Option Five'
12874 * } )
12875 * ]
12876 * }
12877 * } );
12878 * $( 'body' ).append( capsule.$element );
12879 *
12880 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets/Selects_and_Options#Menu_selects_and_options
12881 *
12882 * @class
12883 * @extends OO.ui.Widget
12884 * @mixins OO.ui.mixin.TabIndexedElement
12885 * @mixins OO.ui.mixin.GroupElement
12886 *
12887 * @constructor
12888 * @param {Object} [config] Configuration options
12889 * @cfg {boolean} [allowArbitrary=false] Allow data items to be added even if not present in the menu.
12890 * @cfg {Object} [menu] Configuration options to pass to the {@link OO.ui.MenuSelectWidget menu select widget}.
12891 * @cfg {Object} [popup] Configuration options to pass to the {@link OO.ui.PopupWidget popup widget}.
12892 * If specified, this popup will be shown instead of the menu (but the menu
12893 * will still be used for item labels and allowArbitrary=false). The widgets
12894 * in the popup should use this.addItemsFromData() or this.addItems() as necessary.
12895 * @cfg {jQuery} [$overlay] Render the menu or popup into a separate layer.
12896 * This configuration is useful in cases where the expanded menu is larger than
12897 * its containing `<div>`. The specified overlay layer is usually on top of
12898 * the containing `<div>` and has a larger area. By default, the menu uses
12899 * relative positioning.
12900 */
12901 OO.ui.CapsuleMultiSelectWidget = function OoUiCapsuleMultiSelectWidget( config ) {
12902 var $tabFocus;
12903
12904 // Configuration initialization
12905 config = config || {};
12906
12907 // Parent constructor
12908 OO.ui.CapsuleMultiSelectWidget.parent.call( this, config );
12909
12910 // Properties (must be set before mixin constructor calls)
12911 this.$input = config.popup ? null : $( '<input>' );
12912 this.$handle = $( '<div>' );
12913
12914 // Mixin constructors
12915 OO.ui.mixin.GroupElement.call( this, config );
12916 if ( config.popup ) {
12917 config.popup = $.extend( {}, config.popup, {
12918 align: 'forwards',
12919 anchor: false
12920 } );
12921 OO.ui.mixin.PopupElement.call( this, config );
12922 $tabFocus = $( '<span>' );
12923 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: $tabFocus } ) );
12924 } else {
12925 this.popup = null;
12926 $tabFocus = null;
12927 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$input } ) );
12928 }
12929 OO.ui.mixin.IndicatorElement.call( this, config );
12930 OO.ui.mixin.IconElement.call( this, config );
12931
12932 // Properties
12933 this.$content = $( '<div>' );
12934 this.allowArbitrary = !!config.allowArbitrary;
12935 this.$overlay = config.$overlay || this.$element;
12936 this.menu = new OO.ui.FloatingMenuSelectWidget( $.extend(
12937 {
12938 widget: this,
12939 $input: this.$input,
12940 $container: this.$element,
12941 filterFromInput: true,
12942 disabled: this.isDisabled()
12943 },
12944 config.menu
12945 ) );
12946
12947 // Events
12948 if ( this.popup ) {
12949 $tabFocus.on( {
12950 focus: this.onFocusForPopup.bind( this )
12951 } );
12952 this.popup.$element.on( 'focusout', this.onPopupFocusOut.bind( this ) );
12953 if ( this.popup.$autoCloseIgnore ) {
12954 this.popup.$autoCloseIgnore.on( 'focusout', this.onPopupFocusOut.bind( this ) );
12955 }
12956 this.popup.connect( this, {
12957 toggle: function ( visible ) {
12958 $tabFocus.toggle( !visible );
12959 }
12960 } );
12961 } else {
12962 this.$input.on( {
12963 focus: this.onInputFocus.bind( this ),
12964 blur: this.onInputBlur.bind( this ),
12965 'propertychange change click mouseup keydown keyup input cut paste select focus':
12966 OO.ui.debounce( this.updateInputSize.bind( this ) ),
12967 keydown: this.onKeyDown.bind( this ),
12968 keypress: this.onKeyPress.bind( this )
12969 } );
12970 }
12971 this.menu.connect( this, {
12972 choose: 'onMenuChoose',
12973 add: 'onMenuItemsChange',
12974 remove: 'onMenuItemsChange'
12975 } );
12976 this.$handle.on( {
12977 mousedown: this.onMouseDown.bind( this )
12978 } );
12979
12980 // Initialization
12981 if ( this.$input ) {
12982 this.$input.prop( 'disabled', this.isDisabled() );
12983 this.$input.attr( {
12984 role: 'combobox',
12985 'aria-autocomplete': 'list'
12986 } );
12987 this.updateInputSize();
12988 }
12989 if ( config.data ) {
12990 this.setItemsFromData( config.data );
12991 }
12992 this.$content.addClass( 'oo-ui-capsuleMultiSelectWidget-content' )
12993 .append( this.$group );
12994 this.$group.addClass( 'oo-ui-capsuleMultiSelectWidget-group' );
12995 this.$handle.addClass( 'oo-ui-capsuleMultiSelectWidget-handle' )
12996 .append( this.$indicator, this.$icon, this.$content );
12997 this.$element.addClass( 'oo-ui-capsuleMultiSelectWidget' )
12998 .append( this.$handle );
12999 if ( this.popup ) {
13000 this.$content.append( $tabFocus );
13001 this.$overlay.append( this.popup.$element );
13002 } else {
13003 this.$content.append( this.$input );
13004 this.$overlay.append( this.menu.$element );
13005 }
13006 this.onMenuItemsChange();
13007 };
13008
13009 /* Setup */
13010
13011 OO.inheritClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.Widget );
13012 OO.mixinClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.mixin.GroupElement );
13013 OO.mixinClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.mixin.PopupElement );
13014 OO.mixinClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.mixin.TabIndexedElement );
13015 OO.mixinClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.mixin.IndicatorElement );
13016 OO.mixinClass( OO.ui.CapsuleMultiSelectWidget, OO.ui.mixin.IconElement );
13017
13018 /* Events */
13019
13020 /**
13021 * @event change
13022 *
13023 * A change event is emitted when the set of selected items changes.
13024 *
13025 * @param {Mixed[]} datas Data of the now-selected items
13026 */
13027
13028 /* Methods */
13029
13030 /**
13031 * Construct a OO.ui.CapsuleItemWidget (or a subclass thereof) from given label and data.
13032 *
13033 * @protected
13034 * @param {Mixed} data Custom data of any type.
13035 * @param {string} label The label text.
13036 * @return {OO.ui.CapsuleItemWidget}
13037 */
13038 OO.ui.CapsuleMultiSelectWidget.prototype.createItemWidget = function ( data, label ) {
13039 return new OO.ui.CapsuleItemWidget( { data: data, label: label } );
13040 };
13041
13042 /**
13043 * Get the data of the items in the capsule
13044 * @return {Mixed[]}
13045 */
13046 OO.ui.CapsuleMultiSelectWidget.prototype.getItemsData = function () {
13047 return $.map( this.getItems(), function ( e ) { return e.data; } );
13048 };
13049
13050 /**
13051 * Set the items in the capsule by providing data
13052 * @chainable
13053 * @param {Mixed[]} datas
13054 * @return {OO.ui.CapsuleMultiSelectWidget}
13055 */
13056 OO.ui.CapsuleMultiSelectWidget.prototype.setItemsFromData = function ( datas ) {
13057 var widget = this,
13058 menu = this.menu,
13059 items = this.getItems();
13060
13061 $.each( datas, function ( i, data ) {
13062 var j, label,
13063 item = menu.getItemFromData( data );
13064
13065 if ( item ) {
13066 label = item.label;
13067 } else if ( widget.allowArbitrary ) {
13068 label = String( data );
13069 } else {
13070 return;
13071 }
13072
13073 item = null;
13074 for ( j = 0; j < items.length; j++ ) {
13075 if ( items[ j ].data === data && items[ j ].label === label ) {
13076 item = items[ j ];
13077 items.splice( j, 1 );
13078 break;
13079 }
13080 }
13081 if ( !item ) {
13082 item = widget.createItemWidget( data, label );
13083 }
13084 widget.addItems( [ item ], i );
13085 } );
13086
13087 if ( items.length ) {
13088 widget.removeItems( items );
13089 }
13090
13091 return this;
13092 };
13093
13094 /**
13095 * Add items to the capsule by providing their data
13096 * @chainable
13097 * @param {Mixed[]} datas
13098 * @return {OO.ui.CapsuleMultiSelectWidget}
13099 */
13100 OO.ui.CapsuleMultiSelectWidget.prototype.addItemsFromData = function ( datas ) {
13101 var widget = this,
13102 menu = this.menu,
13103 items = [];
13104
13105 $.each( datas, function ( i, data ) {
13106 var item;
13107
13108 if ( !widget.getItemFromData( data ) ) {
13109 item = menu.getItemFromData( data );
13110 if ( item ) {
13111 items.push( widget.createItemWidget( data, item.label ) );
13112 } else if ( widget.allowArbitrary ) {
13113 items.push( widget.createItemWidget( data, String( data ) ) );
13114 }
13115 }
13116 } );
13117
13118 if ( items.length ) {
13119 this.addItems( items );
13120 }
13121
13122 return this;
13123 };
13124
13125 /**
13126 * Remove items by data
13127 * @chainable
13128 * @param {Mixed[]} datas
13129 * @return {OO.ui.CapsuleMultiSelectWidget}
13130 */
13131 OO.ui.CapsuleMultiSelectWidget.prototype.removeItemsFromData = function ( datas ) {
13132 var widget = this,
13133 items = [];
13134
13135 $.each( datas, function ( i, data ) {
13136 var item = widget.getItemFromData( data );
13137 if ( item ) {
13138 items.push( item );
13139 }
13140 } );
13141
13142 if ( items.length ) {
13143 this.removeItems( items );
13144 }
13145
13146 return this;
13147 };
13148
13149 /**
13150 * @inheritdoc
13151 */
13152 OO.ui.CapsuleMultiSelectWidget.prototype.addItems = function ( items ) {
13153 var same, i, l,
13154 oldItems = this.items.slice();
13155
13156 OO.ui.mixin.GroupElement.prototype.addItems.call( this, items );
13157
13158 if ( this.items.length !== oldItems.length ) {
13159 same = false;
13160 } else {
13161 same = true;
13162 for ( i = 0, l = oldItems.length; same && i < l; i++ ) {
13163 same = same && this.items[ i ] === oldItems[ i ];
13164 }
13165 }
13166 if ( !same ) {
13167 this.emit( 'change', this.getItemsData() );
13168 this.menu.position();
13169 }
13170
13171 return this;
13172 };
13173
13174 /**
13175 * @inheritdoc
13176 */
13177 OO.ui.CapsuleMultiSelectWidget.prototype.removeItems = function ( items ) {
13178 var same, i, l,
13179 oldItems = this.items.slice();
13180
13181 OO.ui.mixin.GroupElement.prototype.removeItems.call( this, items );
13182
13183 if ( this.items.length !== oldItems.length ) {
13184 same = false;
13185 } else {
13186 same = true;
13187 for ( i = 0, l = oldItems.length; same && i < l; i++ ) {
13188 same = same && this.items[ i ] === oldItems[ i ];
13189 }
13190 }
13191 if ( !same ) {
13192 this.emit( 'change', this.getItemsData() );
13193 this.menu.position();
13194 }
13195
13196 return this;
13197 };
13198
13199 /**
13200 * @inheritdoc
13201 */
13202 OO.ui.CapsuleMultiSelectWidget.prototype.clearItems = function () {
13203 if ( this.items.length ) {
13204 OO.ui.mixin.GroupElement.prototype.clearItems.call( this );
13205 this.emit( 'change', this.getItemsData() );
13206 this.menu.position();
13207 }
13208 return this;
13209 };
13210
13211 /**
13212 * Get the capsule widget's menu.
13213 * @return {OO.ui.MenuSelectWidget} Menu widget
13214 */
13215 OO.ui.CapsuleMultiSelectWidget.prototype.getMenu = function () {
13216 return this.menu;
13217 };
13218
13219 /**
13220 * Handle focus events
13221 *
13222 * @private
13223 * @param {jQuery.Event} event
13224 */
13225 OO.ui.CapsuleMultiSelectWidget.prototype.onInputFocus = function () {
13226 if ( !this.isDisabled() ) {
13227 this.menu.toggle( true );
13228 }
13229 };
13230
13231 /**
13232 * Handle blur events
13233 *
13234 * @private
13235 * @param {jQuery.Event} event
13236 */
13237 OO.ui.CapsuleMultiSelectWidget.prototype.onInputBlur = function () {
13238 if ( this.allowArbitrary && this.$input.val().trim() !== '' ) {
13239 this.addItemsFromData( [ this.$input.val() ] );
13240 }
13241 this.clearInput();
13242 };
13243
13244 /**
13245 * Handle focus events
13246 *
13247 * @private
13248 * @param {jQuery.Event} event
13249 */
13250 OO.ui.CapsuleMultiSelectWidget.prototype.onFocusForPopup = function () {
13251 if ( !this.isDisabled() ) {
13252 this.popup.setSize( this.$handle.width() );
13253 this.popup.toggle( true );
13254 this.popup.$element.find( '*' )
13255 .filter( function () { return OO.ui.isFocusableElement( $( this ), true ); } )
13256 .first()
13257 .focus();
13258 }
13259 };
13260
13261 /**
13262 * Handles popup focus out events.
13263 *
13264 * @private
13265 * @param {Event} e Focus out event
13266 */
13267 OO.ui.CapsuleMultiSelectWidget.prototype.onPopupFocusOut = function () {
13268 var widget = this.popup;
13269
13270 setTimeout( function () {
13271 if (
13272 widget.isVisible() &&
13273 !OO.ui.contains( widget.$element[ 0 ], document.activeElement, true ) &&
13274 ( !widget.$autoCloseIgnore || !widget.$autoCloseIgnore.has( document.activeElement ).length )
13275 ) {
13276 widget.toggle( false );
13277 }
13278 } );
13279 };
13280
13281 /**
13282 * Handle mouse down events.
13283 *
13284 * @private
13285 * @param {jQuery.Event} e Mouse down event
13286 */
13287 OO.ui.CapsuleMultiSelectWidget.prototype.onMouseDown = function ( e ) {
13288 if ( e.which === OO.ui.MouseButtons.LEFT ) {
13289 this.focus();
13290 return false;
13291 } else {
13292 this.updateInputSize();
13293 }
13294 };
13295
13296 /**
13297 * Handle key press events.
13298 *
13299 * @private
13300 * @param {jQuery.Event} e Key press event
13301 */
13302 OO.ui.CapsuleMultiSelectWidget.prototype.onKeyPress = function ( e ) {
13303 var item;
13304
13305 if ( !this.isDisabled() ) {
13306 if ( e.which === OO.ui.Keys.ESCAPE ) {
13307 this.clearInput();
13308 return false;
13309 }
13310
13311 if ( !this.popup ) {
13312 this.menu.toggle( true );
13313 if ( e.which === OO.ui.Keys.ENTER ) {
13314 item = this.menu.getItemFromLabel( this.$input.val(), true );
13315 if ( item ) {
13316 this.addItemsFromData( [ item.data ] );
13317 this.clearInput();
13318 } else if ( this.allowArbitrary && this.$input.val().trim() !== '' ) {
13319 this.addItemsFromData( [ this.$input.val() ] );
13320 this.clearInput();
13321 }
13322 return false;
13323 }
13324
13325 // Make sure the input gets resized.
13326 setTimeout( this.updateInputSize.bind( this ), 0 );
13327 }
13328 }
13329 };
13330
13331 /**
13332 * Handle key down events.
13333 *
13334 * @private
13335 * @param {jQuery.Event} e Key down event
13336 */
13337 OO.ui.CapsuleMultiSelectWidget.prototype.onKeyDown = function ( e ) {
13338 if ( !this.isDisabled() ) {
13339 // 'keypress' event is not triggered for Backspace
13340 if ( e.keyCode === OO.ui.Keys.BACKSPACE && this.$input.val() === '' ) {
13341 if ( this.items.length ) {
13342 this.removeItems( this.items.slice( -1 ) );
13343 }
13344 return false;
13345 }
13346 }
13347 };
13348
13349 /**
13350 * Update the dimensions of the text input field to encompass all available area.
13351 *
13352 * @private
13353 * @param {jQuery.Event} e Event of some sort
13354 */
13355 OO.ui.CapsuleMultiSelectWidget.prototype.updateInputSize = function () {
13356 var $lastItem, direction, contentWidth, currentWidth, bestWidth;
13357 if ( !this.isDisabled() ) {
13358 this.$input.css( 'width', '1em' );
13359 $lastItem = this.$group.children().last();
13360 direction = OO.ui.Element.static.getDir( this.$handle );
13361 contentWidth = this.$input[ 0 ].scrollWidth;
13362 currentWidth = this.$input.width();
13363
13364 if ( contentWidth < currentWidth ) {
13365 // All is fine, don't perform expensive calculations
13366 return;
13367 }
13368
13369 if ( !$lastItem.length ) {
13370 bestWidth = this.$content.innerWidth();
13371 } else {
13372 bestWidth = direction === 'ltr' ?
13373 this.$content.innerWidth() - $lastItem.position().left - $lastItem.outerWidth() :
13374 $lastItem.position().left;
13375 }
13376 // Some safety margin for sanity, because I *really* don't feel like finding out where the few
13377 // pixels this is off by are coming from.
13378 bestWidth -= 10;
13379 if ( contentWidth > bestWidth ) {
13380 // This will result in the input getting shifted to the next line
13381 bestWidth = this.$content.innerWidth() - 10;
13382 }
13383 this.$input.width( Math.floor( bestWidth ) );
13384
13385 this.menu.position();
13386 }
13387 };
13388
13389 /**
13390 * Handle menu choose events.
13391 *
13392 * @private
13393 * @param {OO.ui.OptionWidget} item Chosen item
13394 */
13395 OO.ui.CapsuleMultiSelectWidget.prototype.onMenuChoose = function ( item ) {
13396 if ( item && item.isVisible() ) {
13397 this.addItemsFromData( [ item.getData() ] );
13398 this.clearInput();
13399 }
13400 };
13401
13402 /**
13403 * Handle menu item change events.
13404 *
13405 * @private
13406 */
13407 OO.ui.CapsuleMultiSelectWidget.prototype.onMenuItemsChange = function () {
13408 this.setItemsFromData( this.getItemsData() );
13409 this.$element.toggleClass( 'oo-ui-capsuleMultiSelectWidget-empty', this.menu.isEmpty() );
13410 };
13411
13412 /**
13413 * Clear the input field
13414 * @private
13415 */
13416 OO.ui.CapsuleMultiSelectWidget.prototype.clearInput = function () {
13417 if ( this.$input ) {
13418 this.$input.val( '' );
13419 this.updateInputSize();
13420 }
13421 if ( this.popup ) {
13422 this.popup.toggle( false );
13423 }
13424 this.menu.toggle( false );
13425 this.menu.selectItem();
13426 this.menu.highlightItem();
13427 };
13428
13429 /**
13430 * @inheritdoc
13431 */
13432 OO.ui.CapsuleMultiSelectWidget.prototype.setDisabled = function ( disabled ) {
13433 var i, len;
13434
13435 // Parent method
13436 OO.ui.CapsuleMultiSelectWidget.parent.prototype.setDisabled.call( this, disabled );
13437
13438 if ( this.$input ) {
13439 this.$input.prop( 'disabled', this.isDisabled() );
13440 }
13441 if ( this.menu ) {
13442 this.menu.setDisabled( this.isDisabled() );
13443 }
13444 if ( this.popup ) {
13445 this.popup.setDisabled( this.isDisabled() );
13446 }
13447
13448 if ( this.items ) {
13449 for ( i = 0, len = this.items.length; i < len; i++ ) {
13450 this.items[ i ].updateDisabled();
13451 }
13452 }
13453
13454 return this;
13455 };
13456
13457 /**
13458 * Focus the widget
13459 * @chainable
13460 * @return {OO.ui.CapsuleMultiSelectWidget}
13461 */
13462 OO.ui.CapsuleMultiSelectWidget.prototype.focus = function () {
13463 if ( !this.isDisabled() ) {
13464 if ( this.popup ) {
13465 this.popup.setSize( this.$handle.width() );
13466 this.popup.toggle( true );
13467 this.popup.$element.find( '*' )
13468 .filter( function () { return OO.ui.isFocusableElement( $( this ), true ); } )
13469 .first()
13470 .focus();
13471 } else {
13472 this.updateInputSize();
13473 this.menu.toggle( true );
13474 this.$input.focus();
13475 }
13476 }
13477 return this;
13478 };
13479
13480 /**
13481 * SelectFileWidgets allow for selecting files, using the HTML5 File API. These
13482 * widgets can be configured with {@link OO.ui.mixin.IconElement icons} and {@link
13483 * OO.ui.mixin.IndicatorElement indicators}.
13484 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information and examples.
13485 *
13486 * @example
13487 * // Example of a file select widget
13488 * var selectFile = new OO.ui.SelectFileWidget();
13489 * $( 'body' ).append( selectFile.$element );
13490 *
13491 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Widgets
13492 *
13493 * @class
13494 * @extends OO.ui.Widget
13495 * @mixins OO.ui.mixin.IconElement
13496 * @mixins OO.ui.mixin.IndicatorElement
13497 * @mixins OO.ui.mixin.PendingElement
13498 * @mixins OO.ui.mixin.LabelElement
13499 *
13500 * @constructor
13501 * @param {Object} [config] Configuration options
13502 * @cfg {string[]|null} [accept=null] MIME types to accept. null accepts all types.
13503 * @cfg {string} [placeholder] Text to display when no file is selected.
13504 * @cfg {string} [notsupported] Text to display when file support is missing in the browser.
13505 * @cfg {boolean} [droppable=true] Whether to accept files by drag and drop.
13506 * @cfg {boolean} [showDropTarget=false] Whether to show a drop target. Requires droppable to be true.
13507 * @cfg {boolean} [dragDropUI=false] Deprecated alias for showDropTarget
13508 */
13509 OO.ui.SelectFileWidget = function OoUiSelectFileWidget( config ) {
13510 var dragHandler;
13511
13512 // TODO: Remove in next release
13513 if ( config && config.dragDropUI ) {
13514 config.showDropTarget = true;
13515 }
13516
13517 // Configuration initialization
13518 config = $.extend( {
13519 accept: null,
13520 placeholder: OO.ui.msg( 'ooui-selectfile-placeholder' ),
13521 notsupported: OO.ui.msg( 'ooui-selectfile-not-supported' ),
13522 droppable: true,
13523 showDropTarget: false
13524 }, config );
13525
13526 // Parent constructor
13527 OO.ui.SelectFileWidget.parent.call( this, config );
13528
13529 // Mixin constructors
13530 OO.ui.mixin.IconElement.call( this, config );
13531 OO.ui.mixin.IndicatorElement.call( this, config );
13532 OO.ui.mixin.PendingElement.call( this, $.extend( {}, config, { $pending: this.$info } ) );
13533 OO.ui.mixin.LabelElement.call( this, $.extend( {}, config, { autoFitLabel: true } ) );
13534
13535 // Properties
13536 this.$info = $( '<span>' );
13537
13538 // Properties
13539 this.showDropTarget = config.showDropTarget;
13540 this.isSupported = this.constructor.static.isSupported();
13541 this.currentFile = null;
13542 if ( Array.isArray( config.accept ) ) {
13543 this.accept = config.accept;
13544 } else {
13545 this.accept = null;
13546 }
13547 this.placeholder = config.placeholder;
13548 this.notsupported = config.notsupported;
13549 this.onFileSelectedHandler = this.onFileSelected.bind( this );
13550
13551 this.selectButton = new OO.ui.ButtonWidget( {
13552 classes: [ 'oo-ui-selectFileWidget-selectButton' ],
13553 label: OO.ui.msg( 'ooui-selectfile-button-select' ),
13554 disabled: this.disabled || !this.isSupported
13555 } );
13556
13557 this.clearButton = new OO.ui.ButtonWidget( {
13558 classes: [ 'oo-ui-selectFileWidget-clearButton' ],
13559 framed: false,
13560 icon: 'remove',
13561 disabled: this.disabled
13562 } );
13563
13564 // Events
13565 this.selectButton.$button.on( {
13566 keypress: this.onKeyPress.bind( this )
13567 } );
13568 this.clearButton.connect( this, {
13569 click: 'onClearClick'
13570 } );
13571 if ( config.droppable ) {
13572 dragHandler = this.onDragEnterOrOver.bind( this );
13573 this.$element.on( {
13574 dragenter: dragHandler,
13575 dragover: dragHandler,
13576 dragleave: this.onDragLeave.bind( this ),
13577 drop: this.onDrop.bind( this )
13578 } );
13579 }
13580
13581 // Initialization
13582 this.addInput();
13583 this.updateUI();
13584 this.$label.addClass( 'oo-ui-selectFileWidget-label' );
13585 this.$info
13586 .addClass( 'oo-ui-selectFileWidget-info' )
13587 .append( this.$icon, this.$label, this.clearButton.$element, this.$indicator );
13588 this.$element
13589 .addClass( 'oo-ui-selectFileWidget' )
13590 .append( this.$info, this.selectButton.$element );
13591 if ( config.droppable && config.showDropTarget ) {
13592 this.$dropTarget = $( '<div>' )
13593 .addClass( 'oo-ui-selectFileWidget-dropTarget' )
13594 .text( OO.ui.msg( 'ooui-selectfile-dragdrop-placeholder' ) )
13595 .on( {
13596 click: this.onDropTargetClick.bind( this )
13597 } );
13598 this.$element.prepend( this.$dropTarget );
13599 }
13600 };
13601
13602 /* Setup */
13603
13604 OO.inheritClass( OO.ui.SelectFileWidget, OO.ui.Widget );
13605 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.IconElement );
13606 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.IndicatorElement );
13607 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.PendingElement );
13608 OO.mixinClass( OO.ui.SelectFileWidget, OO.ui.mixin.LabelElement );
13609
13610 /* Static Properties */
13611
13612 /**
13613 * Check if this widget is supported
13614 *
13615 * @static
13616 * @return {boolean}
13617 */
13618 OO.ui.SelectFileWidget.static.isSupported = function () {
13619 var $input;
13620 if ( OO.ui.SelectFileWidget.static.isSupportedCache === null ) {
13621 $input = $( '<input type="file">' );
13622 OO.ui.SelectFileWidget.static.isSupportedCache = $input[ 0 ].files !== undefined;
13623 }
13624 return OO.ui.SelectFileWidget.static.isSupportedCache;
13625 };
13626
13627 OO.ui.SelectFileWidget.static.isSupportedCache = null;
13628
13629 /* Events */
13630
13631 /**
13632 * @event change
13633 *
13634 * A change event is emitted when the on/off state of the toggle changes.
13635 *
13636 * @param {File|null} value New value
13637 */
13638
13639 /* Methods */
13640
13641 /**
13642 * Get the current value of the field
13643 *
13644 * @return {File|null}
13645 */
13646 OO.ui.SelectFileWidget.prototype.getValue = function () {
13647 return this.currentFile;
13648 };
13649
13650 /**
13651 * Set the current value of the field
13652 *
13653 * @param {File|null} file File to select
13654 */
13655 OO.ui.SelectFileWidget.prototype.setValue = function ( file ) {
13656 if ( this.currentFile !== file ) {
13657 this.currentFile = file;
13658 this.updateUI();
13659 this.emit( 'change', this.currentFile );
13660 }
13661 };
13662
13663 /**
13664 * Focus the widget.
13665 *
13666 * Focusses the select file button.
13667 *
13668 * @chainable
13669 */
13670 OO.ui.SelectFileWidget.prototype.focus = function () {
13671 this.selectButton.$button[ 0 ].focus();
13672 return this;
13673 };
13674
13675 /**
13676 * Update the user interface when a file is selected or unselected
13677 *
13678 * @protected
13679 */
13680 OO.ui.SelectFileWidget.prototype.updateUI = function () {
13681 var $label;
13682 if ( !this.isSupported ) {
13683 this.$element.addClass( 'oo-ui-selectFileWidget-notsupported' );
13684 this.$element.removeClass( 'oo-ui-selectFileWidget-empty' );
13685 this.setLabel( this.notsupported );
13686 } else {
13687 this.$element.addClass( 'oo-ui-selectFileWidget-supported' );
13688 if ( this.currentFile ) {
13689 this.$element.removeClass( 'oo-ui-selectFileWidget-empty' );
13690 $label = $( [] );
13691 $label = $label.add(
13692 $( '<span>' )
13693 .addClass( 'oo-ui-selectFileWidget-fileName' )
13694 .text( this.currentFile.name )
13695 );
13696 if ( this.currentFile.type !== '' ) {
13697 $label = $label.add(
13698 $( '<span>' )
13699 .addClass( 'oo-ui-selectFileWidget-fileType' )
13700 .text( this.currentFile.type )
13701 );
13702 }
13703 this.setLabel( $label );
13704 } else {
13705 this.$element.addClass( 'oo-ui-selectFileWidget-empty' );
13706 this.setLabel( this.placeholder );
13707 }
13708 }
13709 };
13710
13711 /**
13712 * Add the input to the widget
13713 *
13714 * @private
13715 */
13716 OO.ui.SelectFileWidget.prototype.addInput = function () {
13717 if ( this.$input ) {
13718 this.$input.remove();
13719 }
13720
13721 if ( !this.isSupported ) {
13722 this.$input = null;
13723 return;
13724 }
13725
13726 this.$input = $( '<input type="file">' );
13727 this.$input.on( 'change', this.onFileSelectedHandler );
13728 this.$input.attr( {
13729 tabindex: -1
13730 } );
13731 if ( this.accept ) {
13732 this.$input.attr( 'accept', this.accept.join( ', ' ) );
13733 }
13734 this.selectButton.$button.append( this.$input );
13735 };
13736
13737 /**
13738 * Determine if we should accept this file
13739 *
13740 * @private
13741 * @param {string} File MIME type
13742 * @return {boolean}
13743 */
13744 OO.ui.SelectFileWidget.prototype.isAllowedType = function ( mimeType ) {
13745 var i, mimeTest;
13746
13747 if ( !this.accept || !mimeType ) {
13748 return true;
13749 }
13750
13751 for ( i = 0; i < this.accept.length; i++ ) {
13752 mimeTest = this.accept[ i ];
13753 if ( mimeTest === mimeType ) {
13754 return true;
13755 } else if ( mimeTest.substr( -2 ) === '/*' ) {
13756 mimeTest = mimeTest.substr( 0, mimeTest.length - 1 );
13757 if ( mimeType.substr( 0, mimeTest.length ) === mimeTest ) {
13758 return true;
13759 }
13760 }
13761 }
13762
13763 return false;
13764 };
13765
13766 /**
13767 * Handle file selection from the input
13768 *
13769 * @private
13770 * @param {jQuery.Event} e
13771 */
13772 OO.ui.SelectFileWidget.prototype.onFileSelected = function ( e ) {
13773 var file = OO.getProp( e.target, 'files', 0 ) || null;
13774
13775 if ( file && !this.isAllowedType( file.type ) ) {
13776 file = null;
13777 }
13778
13779 this.setValue( file );
13780 this.addInput();
13781 };
13782
13783 /**
13784 * Handle clear button click events.
13785 *
13786 * @private
13787 */
13788 OO.ui.SelectFileWidget.prototype.onClearClick = function () {
13789 this.setValue( null );
13790 return false;
13791 };
13792
13793 /**
13794 * Handle key press events.
13795 *
13796 * @private
13797 * @param {jQuery.Event} e Key press event
13798 */
13799 OO.ui.SelectFileWidget.prototype.onKeyPress = function ( e ) {
13800 if ( this.isSupported && !this.isDisabled() && this.$input &&
13801 ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
13802 ) {
13803 this.$input.click();
13804 return false;
13805 }
13806 };
13807
13808 /**
13809 * Handle drop target click events.
13810 *
13811 * @private
13812 * @param {jQuery.Event} e Key press event
13813 */
13814 OO.ui.SelectFileWidget.prototype.onDropTargetClick = function () {
13815 if ( this.isSupported && !this.isDisabled() && this.$input ) {
13816 this.$input.click();
13817 return false;
13818 }
13819 };
13820
13821 /**
13822 * Handle drag enter and over events
13823 *
13824 * @private
13825 * @param {jQuery.Event} e Drag event
13826 */
13827 OO.ui.SelectFileWidget.prototype.onDragEnterOrOver = function ( e ) {
13828 var itemOrFile,
13829 droppableFile = false,
13830 dt = e.originalEvent.dataTransfer;
13831
13832 e.preventDefault();
13833 e.stopPropagation();
13834
13835 if ( this.isDisabled() || !this.isSupported ) {
13836 this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
13837 dt.dropEffect = 'none';
13838 return false;
13839 }
13840
13841 // DataTransferItem and File both have a type property, but in Chrome files
13842 // have no information at this point.
13843 itemOrFile = OO.getProp( dt, 'items', 0 ) || OO.getProp( dt, 'files', 0 );
13844 if ( itemOrFile ) {
13845 if ( this.isAllowedType( itemOrFile.type ) ) {
13846 droppableFile = true;
13847 }
13848 // dt.types is Array-like, but not an Array
13849 } else if ( Array.prototype.indexOf.call( OO.getProp( dt, 'types' ) || [], 'Files' ) !== -1 ) {
13850 // File information is not available at this point for security so just assume
13851 // it is acceptable for now.
13852 // https://bugzilla.mozilla.org/show_bug.cgi?id=640534
13853 droppableFile = true;
13854 }
13855
13856 this.$element.toggleClass( 'oo-ui-selectFileWidget-canDrop', droppableFile );
13857 if ( !droppableFile ) {
13858 dt.dropEffect = 'none';
13859 }
13860
13861 return false;
13862 };
13863
13864 /**
13865 * Handle drag leave events
13866 *
13867 * @private
13868 * @param {jQuery.Event} e Drag event
13869 */
13870 OO.ui.SelectFileWidget.prototype.onDragLeave = function () {
13871 this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
13872 };
13873
13874 /**
13875 * Handle drop events
13876 *
13877 * @private
13878 * @param {jQuery.Event} e Drop event
13879 */
13880 OO.ui.SelectFileWidget.prototype.onDrop = function ( e ) {
13881 var file = null,
13882 dt = e.originalEvent.dataTransfer;
13883
13884 e.preventDefault();
13885 e.stopPropagation();
13886 this.$element.removeClass( 'oo-ui-selectFileWidget-canDrop' );
13887
13888 if ( this.isDisabled() || !this.isSupported ) {
13889 return false;
13890 }
13891
13892 file = OO.getProp( dt, 'files', 0 );
13893 if ( file && !this.isAllowedType( file.type ) ) {
13894 file = null;
13895 }
13896 if ( file ) {
13897 this.setValue( file );
13898 }
13899
13900 return false;
13901 };
13902
13903 /**
13904 * @inheritdoc
13905 */
13906 OO.ui.SelectFileWidget.prototype.setDisabled = function ( disabled ) {
13907 OO.ui.SelectFileWidget.parent.prototype.setDisabled.call( this, disabled );
13908 if ( this.selectButton ) {
13909 this.selectButton.setDisabled( disabled );
13910 }
13911 if ( this.clearButton ) {
13912 this.clearButton.setDisabled( disabled );
13913 }
13914 return this;
13915 };
13916
13917 /**
13918 * Progress bars visually display the status of an operation, such as a download,
13919 * and can be either determinate or indeterminate:
13920 *
13921 * - **determinate** process bars show the percent of an operation that is complete.
13922 *
13923 * - **indeterminate** process bars use a visual display of motion to indicate that an operation
13924 * is taking place. Because the extent of an indeterminate operation is unknown, the bar does
13925 * not use percentages.
13926 *
13927 * The value of the `progress` configuration determines whether the bar is determinate or indeterminate.
13928 *
13929 * @example
13930 * // Examples of determinate and indeterminate progress bars.
13931 * var progressBar1 = new OO.ui.ProgressBarWidget( {
13932 * progress: 33
13933 * } );
13934 * var progressBar2 = new OO.ui.ProgressBarWidget();
13935 *
13936 * // Create a FieldsetLayout to layout progress bars
13937 * var fieldset = new OO.ui.FieldsetLayout;
13938 * fieldset.addItems( [
13939 * new OO.ui.FieldLayout( progressBar1, {label: 'Determinate', align: 'top'}),
13940 * new OO.ui.FieldLayout( progressBar2, {label: 'Indeterminate', align: 'top'})
13941 * ] );
13942 * $( 'body' ).append( fieldset.$element );
13943 *
13944 * @class
13945 * @extends OO.ui.Widget
13946 *
13947 * @constructor
13948 * @param {Object} [config] Configuration options
13949 * @cfg {number|boolean} [progress=false] The type of progress bar (determinate or indeterminate).
13950 * To create a determinate progress bar, specify a number that reflects the initial percent complete.
13951 * By default, the progress bar is indeterminate.
13952 */
13953 OO.ui.ProgressBarWidget = function OoUiProgressBarWidget( config ) {
13954 // Configuration initialization
13955 config = config || {};
13956
13957 // Parent constructor
13958 OO.ui.ProgressBarWidget.parent.call( this, config );
13959
13960 // Properties
13961 this.$bar = $( '<div>' );
13962 this.progress = null;
13963
13964 // Initialization
13965 this.setProgress( config.progress !== undefined ? config.progress : false );
13966 this.$bar.addClass( 'oo-ui-progressBarWidget-bar' );
13967 this.$element
13968 .attr( {
13969 role: 'progressbar',
13970 'aria-valuemin': 0,
13971 'aria-valuemax': 100
13972 } )
13973 .addClass( 'oo-ui-progressBarWidget' )
13974 .append( this.$bar );
13975 };
13976
13977 /* Setup */
13978
13979 OO.inheritClass( OO.ui.ProgressBarWidget, OO.ui.Widget );
13980
13981 /* Static Properties */
13982
13983 OO.ui.ProgressBarWidget.static.tagName = 'div';
13984
13985 /* Methods */
13986
13987 /**
13988 * Get the percent of the progress that has been completed. Indeterminate progresses will return `false`.
13989 *
13990 * @return {number|boolean} Progress percent
13991 */
13992 OO.ui.ProgressBarWidget.prototype.getProgress = function () {
13993 return this.progress;
13994 };
13995
13996 /**
13997 * Set the percent of the process completed or `false` for an indeterminate process.
13998 *
13999 * @param {number|boolean} progress Progress percent or `false` for indeterminate
14000 */
14001 OO.ui.ProgressBarWidget.prototype.setProgress = function ( progress ) {
14002 this.progress = progress;
14003
14004 if ( progress !== false ) {
14005 this.$bar.css( 'width', this.progress + '%' );
14006 this.$element.attr( 'aria-valuenow', this.progress );
14007 } else {
14008 this.$bar.css( 'width', '' );
14009 this.$element.removeAttr( 'aria-valuenow' );
14010 }
14011 this.$element.toggleClass( 'oo-ui-progressBarWidget-indeterminate', !progress );
14012 };
14013
14014 /**
14015 * SearchWidgets combine a {@link OO.ui.TextInputWidget text input field}, where users can type a search query,
14016 * and a menu of search results, which is displayed beneath the query
14017 * field. Unlike {@link OO.ui.mixin.LookupElement lookup menus}, search result menus are always visible to the user.
14018 * Users can choose an item from the menu or type a query into the text field to search for a matching result item.
14019 * In general, search widgets are used inside a separate {@link OO.ui.Dialog dialog} window.
14020 *
14021 * Each time the query is changed, the search result menu is cleared and repopulated. Please see
14022 * the [OOjs UI demos][1] for an example.
14023 *
14024 * [1]: https://tools.wmflabs.org/oojs-ui/oojs-ui/demos/#dialogs-mediawiki-vector-ltr
14025 *
14026 * @class
14027 * @extends OO.ui.Widget
14028 *
14029 * @constructor
14030 * @param {Object} [config] Configuration options
14031 * @cfg {string|jQuery} [placeholder] Placeholder text for query input
14032 * @cfg {string} [value] Initial query value
14033 */
14034 OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
14035 // Configuration initialization
14036 config = config || {};
14037
14038 // Parent constructor
14039 OO.ui.SearchWidget.parent.call( this, config );
14040
14041 // Properties
14042 this.query = new OO.ui.TextInputWidget( {
14043 icon: 'search',
14044 placeholder: config.placeholder,
14045 value: config.value
14046 } );
14047 this.results = new OO.ui.SelectWidget();
14048 this.$query = $( '<div>' );
14049 this.$results = $( '<div>' );
14050
14051 // Events
14052 this.query.connect( this, {
14053 change: 'onQueryChange',
14054 enter: 'onQueryEnter'
14055 } );
14056 this.query.$input.on( 'keydown', this.onQueryKeydown.bind( this ) );
14057
14058 // Initialization
14059 this.$query
14060 .addClass( 'oo-ui-searchWidget-query' )
14061 .append( this.query.$element );
14062 this.$results
14063 .addClass( 'oo-ui-searchWidget-results' )
14064 .append( this.results.$element );
14065 this.$element
14066 .addClass( 'oo-ui-searchWidget' )
14067 .append( this.$results, this.$query );
14068 };
14069
14070 /* Setup */
14071
14072 OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget );
14073
14074 /* Methods */
14075
14076 /**
14077 * Handle query key down events.
14078 *
14079 * @private
14080 * @param {jQuery.Event} e Key down event
14081 */
14082 OO.ui.SearchWidget.prototype.onQueryKeydown = function ( e ) {
14083 var highlightedItem, nextItem,
14084 dir = e.which === OO.ui.Keys.DOWN ? 1 : ( e.which === OO.ui.Keys.UP ? -1 : 0 );
14085
14086 if ( dir ) {
14087 highlightedItem = this.results.getHighlightedItem();
14088 if ( !highlightedItem ) {
14089 highlightedItem = this.results.getSelectedItem();
14090 }
14091 nextItem = this.results.getRelativeSelectableItem( highlightedItem, dir );
14092 this.results.highlightItem( nextItem );
14093 nextItem.scrollElementIntoView();
14094 }
14095 };
14096
14097 /**
14098 * Handle select widget select events.
14099 *
14100 * Clears existing results. Subclasses should repopulate items according to new query.
14101 *
14102 * @private
14103 * @param {string} value New value
14104 */
14105 OO.ui.SearchWidget.prototype.onQueryChange = function () {
14106 // Reset
14107 this.results.clearItems();
14108 };
14109
14110 /**
14111 * Handle select widget enter key events.
14112 *
14113 * Chooses highlighted item.
14114 *
14115 * @private
14116 * @param {string} value New value
14117 */
14118 OO.ui.SearchWidget.prototype.onQueryEnter = function () {
14119 var highlightedItem = this.results.getHighlightedItem();
14120 if ( highlightedItem ) {
14121 this.results.chooseItem( highlightedItem );
14122 }
14123 };
14124
14125 /**
14126 * Get the query input.
14127 *
14128 * @return {OO.ui.TextInputWidget} Query input
14129 */
14130 OO.ui.SearchWidget.prototype.getQuery = function () {
14131 return this.query;
14132 };
14133
14134 /**
14135 * Get the search results menu.
14136 *
14137 * @return {OO.ui.SelectWidget} Menu of search results
14138 */
14139 OO.ui.SearchWidget.prototype.getResults = function () {
14140 return this.results;
14141 };
14142
14143 /**
14144 * NumberInputWidgets combine a {@link OO.ui.TextInputWidget text input} (where a value
14145 * can be entered manually) and two {@link OO.ui.ButtonWidget button widgets}
14146 * (to adjust the value in increments) to allow the user to enter a number.
14147 *
14148 * @example
14149 * // Example: A NumberInputWidget.
14150 * var numberInput = new OO.ui.NumberInputWidget( {
14151 * label: 'NumberInputWidget',
14152 * input: { value: 5, min: 1, max: 10 }
14153 * } );
14154 * $( 'body' ).append( numberInput.$element );
14155 *
14156 * @class
14157 * @extends OO.ui.Widget
14158 *
14159 * @constructor
14160 * @param {Object} [config] Configuration options
14161 * @cfg {Object} [input] Configuration options to pass to the {@link OO.ui.TextInputWidget text input widget}.
14162 * @cfg {Object} [minusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget decrementing button widget}.
14163 * @cfg {Object} [plusButton] Configuration options to pass to the {@link OO.ui.ButtonWidget incrementing button widget}.
14164 * @cfg {boolean} [isInteger=false] Whether the field accepts only integer values.
14165 * @cfg {number} [min=-Infinity] Minimum allowed value
14166 * @cfg {number} [max=Infinity] Maximum allowed value
14167 * @cfg {number} [step=1] Delta when using the buttons or up/down arrow keys
14168 * @cfg {number|null} [pageStep] Delta when using the page-up/page-down keys. Defaults to 10 times #step.
14169 */
14170 OO.ui.NumberInputWidget = function OoUiNumberInputWidget( config ) {
14171 // Configuration initialization
14172 config = $.extend( {
14173 isInteger: false,
14174 min: -Infinity,
14175 max: Infinity,
14176 step: 1,
14177 pageStep: null
14178 }, config );
14179
14180 // Parent constructor
14181 OO.ui.NumberInputWidget.parent.call( this, config );
14182
14183 // Properties
14184 this.input = new OO.ui.TextInputWidget( $.extend(
14185 {
14186 disabled: this.isDisabled()
14187 },
14188 config.input
14189 ) );
14190 this.minusButton = new OO.ui.ButtonWidget( $.extend(
14191 {
14192 disabled: this.isDisabled(),
14193 tabIndex: -1
14194 },
14195 config.minusButton,
14196 {
14197 classes: [ 'oo-ui-numberInputWidget-minusButton' ],
14198 label: '−'
14199 }
14200 ) );
14201 this.plusButton = new OO.ui.ButtonWidget( $.extend(
14202 {
14203 disabled: this.isDisabled(),
14204 tabIndex: -1
14205 },
14206 config.plusButton,
14207 {
14208 classes: [ 'oo-ui-numberInputWidget-plusButton' ],
14209 label: '+'
14210 }
14211 ) );
14212
14213 // Events
14214 this.input.connect( this, {
14215 change: this.emit.bind( this, 'change' ),
14216 enter: this.emit.bind( this, 'enter' )
14217 } );
14218 this.input.$input.on( {
14219 keydown: this.onKeyDown.bind( this ),
14220 'wheel mousewheel DOMMouseScroll': this.onWheel.bind( this )
14221 } );
14222 this.plusButton.connect( this, {
14223 click: [ 'onButtonClick', +1 ]
14224 } );
14225 this.minusButton.connect( this, {
14226 click: [ 'onButtonClick', -1 ]
14227 } );
14228
14229 // Initialization
14230 this.setIsInteger( !!config.isInteger );
14231 this.setRange( config.min, config.max );
14232 this.setStep( config.step, config.pageStep );
14233
14234 this.$field = $( '<div>' ).addClass( 'oo-ui-numberInputWidget-field' )
14235 .append(
14236 this.minusButton.$element,
14237 this.input.$element,
14238 this.plusButton.$element
14239 );
14240 this.$element.addClass( 'oo-ui-numberInputWidget' ).append( this.$field );
14241 this.input.setValidation( this.validateNumber.bind( this ) );
14242 };
14243
14244 /* Setup */
14245
14246 OO.inheritClass( OO.ui.NumberInputWidget, OO.ui.Widget );
14247
14248 /* Events */
14249
14250 /**
14251 * A `change` event is emitted when the value of the input changes.
14252 *
14253 * @event change
14254 */
14255
14256 /**
14257 * An `enter` event is emitted when the user presses 'enter' inside the text box.
14258 *
14259 * @event enter
14260 */
14261
14262 /* Methods */
14263
14264 /**
14265 * Set whether only integers are allowed
14266 * @param {boolean} flag
14267 */
14268 OO.ui.NumberInputWidget.prototype.setIsInteger = function ( flag ) {
14269 this.isInteger = !!flag;
14270 this.input.setValidityFlag();
14271 };
14272
14273 /**
14274 * Get whether only integers are allowed
14275 * @return {boolean} Flag value
14276 */
14277 OO.ui.NumberInputWidget.prototype.getIsInteger = function () {
14278 return this.isInteger;
14279 };
14280
14281 /**
14282 * Set the range of allowed values
14283 * @param {number} min Minimum allowed value
14284 * @param {number} max Maximum allowed value
14285 */
14286 OO.ui.NumberInputWidget.prototype.setRange = function ( min, max ) {
14287 if ( min > max ) {
14288 throw new Error( 'Minimum (' + min + ') must not be greater than maximum (' + max + ')' );
14289 }
14290 this.min = min;
14291 this.max = max;
14292 this.input.setValidityFlag();
14293 };
14294
14295 /**
14296 * Get the current range
14297 * @return {number[]} Minimum and maximum values
14298 */
14299 OO.ui.NumberInputWidget.prototype.getRange = function () {
14300 return [ this.min, this.max ];
14301 };
14302
14303 /**
14304 * Set the stepping deltas
14305 * @param {number} step Normal step
14306 * @param {number|null} pageStep Page step. If null, 10 * step will be used.
14307 */
14308 OO.ui.NumberInputWidget.prototype.setStep = function ( step, pageStep ) {
14309 if ( step <= 0 ) {
14310 throw new Error( 'Step value must be positive' );
14311 }
14312 if ( pageStep === null ) {
14313 pageStep = step * 10;
14314 } else if ( pageStep <= 0 ) {
14315 throw new Error( 'Page step value must be positive' );
14316 }
14317 this.step = step;
14318 this.pageStep = pageStep;
14319 };
14320
14321 /**
14322 * Get the current stepping values
14323 * @return {number[]} Step and page step
14324 */
14325 OO.ui.NumberInputWidget.prototype.getStep = function () {
14326 return [ this.step, this.pageStep ];
14327 };
14328
14329 /**
14330 * Get the current value of the widget
14331 * @return {string}
14332 */
14333 OO.ui.NumberInputWidget.prototype.getValue = function () {
14334 return this.input.getValue();
14335 };
14336
14337 /**
14338 * Get the current value of the widget as a number
14339 * @return {number} May be NaN, or an invalid number
14340 */
14341 OO.ui.NumberInputWidget.prototype.getNumericValue = function () {
14342 return +this.input.getValue();
14343 };
14344
14345 /**
14346 * Set the value of the widget
14347 * @param {string} value Invalid values are allowed
14348 */
14349 OO.ui.NumberInputWidget.prototype.setValue = function ( value ) {
14350 this.input.setValue( value );
14351 };
14352
14353 /**
14354 * Adjust the value of the widget
14355 * @param {number} delta Adjustment amount
14356 */
14357 OO.ui.NumberInputWidget.prototype.adjustValue = function ( delta ) {
14358 var n, v = this.getNumericValue();
14359
14360 delta = +delta;
14361 if ( isNaN( delta ) || !isFinite( delta ) ) {
14362 throw new Error( 'Delta must be a finite number' );
14363 }
14364
14365 if ( isNaN( v ) ) {
14366 n = 0;
14367 } else {
14368 n = v + delta;
14369 n = Math.max( Math.min( n, this.max ), this.min );
14370 if ( this.isInteger ) {
14371 n = Math.round( n );
14372 }
14373 }
14374
14375 if ( n !== v ) {
14376 this.setValue( n );
14377 }
14378 };
14379
14380 /**
14381 * Validate input
14382 * @private
14383 * @param {string} value Field value
14384 * @return {boolean}
14385 */
14386 OO.ui.NumberInputWidget.prototype.validateNumber = function ( value ) {
14387 var n = +value;
14388 if ( isNaN( n ) || !isFinite( n ) ) {
14389 return false;
14390 }
14391
14392 /*jshint bitwise: false */
14393 if ( this.isInteger && ( n | 0 ) !== n ) {
14394 return false;
14395 }
14396 /*jshint bitwise: true */
14397
14398 if ( n < this.min || n > this.max ) {
14399 return false;
14400 }
14401
14402 return true;
14403 };
14404
14405 /**
14406 * Handle mouse click events.
14407 *
14408 * @private
14409 * @param {number} dir +1 or -1
14410 */
14411 OO.ui.NumberInputWidget.prototype.onButtonClick = function ( dir ) {
14412 this.adjustValue( dir * this.step );
14413 };
14414
14415 /**
14416 * Handle mouse wheel events.
14417 *
14418 * @private
14419 * @param {jQuery.Event} event
14420 */
14421 OO.ui.NumberInputWidget.prototype.onWheel = function ( event ) {
14422 var delta = 0;
14423
14424 // Standard 'wheel' event
14425 if ( event.originalEvent.deltaMode !== undefined ) {
14426 this.sawWheelEvent = true;
14427 }
14428 if ( event.originalEvent.deltaY ) {
14429 delta = -event.originalEvent.deltaY;
14430 } else if ( event.originalEvent.deltaX ) {
14431 delta = event.originalEvent.deltaX;
14432 }
14433
14434 // Non-standard events
14435 if ( !this.sawWheelEvent ) {
14436 if ( event.originalEvent.wheelDeltaX ) {
14437 delta = -event.originalEvent.wheelDeltaX;
14438 } else if ( event.originalEvent.wheelDeltaY ) {
14439 delta = event.originalEvent.wheelDeltaY;
14440 } else if ( event.originalEvent.wheelDelta ) {
14441 delta = event.originalEvent.wheelDelta;
14442 } else if ( event.originalEvent.detail ) {
14443 delta = -event.originalEvent.detail;
14444 }
14445 }
14446
14447 if ( delta ) {
14448 delta = delta < 0 ? -1 : 1;
14449 this.adjustValue( delta * this.step );
14450 }
14451
14452 return false;
14453 };
14454
14455 /**
14456 * Handle key down events.
14457 *
14458 * @private
14459 * @param {jQuery.Event} e Key down event
14460 */
14461 OO.ui.NumberInputWidget.prototype.onKeyDown = function ( e ) {
14462 if ( !this.isDisabled() ) {
14463 switch ( e.which ) {
14464 case OO.ui.Keys.UP:
14465 this.adjustValue( this.step );
14466 return false;
14467 case OO.ui.Keys.DOWN:
14468 this.adjustValue( -this.step );
14469 return false;
14470 case OO.ui.Keys.PAGEUP:
14471 this.adjustValue( this.pageStep );
14472 return false;
14473 case OO.ui.Keys.PAGEDOWN:
14474 this.adjustValue( -this.pageStep );
14475 return false;
14476 }
14477 }
14478 };
14479
14480 /**
14481 * @inheritdoc
14482 */
14483 OO.ui.NumberInputWidget.prototype.setDisabled = function ( disabled ) {
14484 // Parent method
14485 OO.ui.NumberInputWidget.parent.prototype.setDisabled.call( this, disabled );
14486
14487 if ( this.input ) {
14488 this.input.setDisabled( this.isDisabled() );
14489 }
14490 if ( this.minusButton ) {
14491 this.minusButton.setDisabled( this.isDisabled() );
14492 }
14493 if ( this.plusButton ) {
14494 this.plusButton.setDisabled( this.isDisabled() );
14495 }
14496
14497 return this;
14498 };
14499
14500 }( OO ) );
14501
14502 /*!
14503 * OOjs UI v0.15.2
14504 * https://www.mediawiki.org/wiki/OOjs_UI
14505 *
14506 * Copyright 2011–2016 OOjs UI Team and other contributors.
14507 * Released under the MIT license
14508 * http://oojs.mit-license.org
14509 *
14510 * Date: 2016-02-02T22:07:00Z
14511 */
14512 ( function ( OO ) {
14513
14514 'use strict';
14515
14516 /**
14517 * Toolbars are complex interface components that permit users to easily access a variety
14518 * of {@link OO.ui.Tool tools} (e.g., formatting commands) and actions, which are additional commands that are
14519 * part of the toolbar, but not configured as tools.
14520 *
14521 * Individual tools are customized and then registered with a {@link OO.ui.ToolFactory tool factory}, which creates
14522 * the tools on demand. Each tool has a symbolic name (used when registering the tool), a title (e.g., ‘Insert
14523 * image’), and an icon.
14524 *
14525 * Individual tools are organized in {@link OO.ui.ToolGroup toolgroups}, which can be {@link OO.ui.MenuToolGroup menus}
14526 * of tools, {@link OO.ui.ListToolGroup lists} of tools, or a single {@link OO.ui.BarToolGroup bar} of tools.
14527 * The arrangement and order of the toolgroups is customized when the toolbar is set up. Tools can be presented in
14528 * any order, but each can only appear once in the toolbar.
14529 *
14530 * The toolbar can be synchronized with the state of the external "application", like a text
14531 * editor's editing area, marking tools as active/inactive (e.g. a 'bold' tool would be shown as
14532 * active when the text cursor was inside bolded text) or enabled/disabled (e.g. a table caption
14533 * tool would be disabled while the user is not editing a table). A state change is signalled by
14534 * emitting the {@link #event-updateState 'updateState' event}, which calls Tools'
14535 * {@link OO.ui.Tool#onUpdateState onUpdateState method}.
14536 *
14537 * The following is an example of a basic toolbar.
14538 *
14539 * @example
14540 * // Example of a toolbar
14541 * // Create the toolbar
14542 * var toolFactory = new OO.ui.ToolFactory();
14543 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
14544 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
14545 *
14546 * // We will be placing status text in this element when tools are used
14547 * var $area = $( '<p>' ).text( 'Toolbar example' );
14548 *
14549 * // Define the tools that we're going to place in our toolbar
14550 *
14551 * // Create a class inheriting from OO.ui.Tool
14552 * function SearchTool() {
14553 * SearchTool.parent.apply( this, arguments );
14554 * }
14555 * OO.inheritClass( SearchTool, OO.ui.Tool );
14556 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
14557 * // of 'icon' and 'title' (displayed icon and text).
14558 * SearchTool.static.name = 'search';
14559 * SearchTool.static.icon = 'search';
14560 * SearchTool.static.title = 'Search...';
14561 * // Defines the action that will happen when this tool is selected (clicked).
14562 * SearchTool.prototype.onSelect = function () {
14563 * $area.text( 'Search tool clicked!' );
14564 * // Never display this tool as "active" (selected).
14565 * this.setActive( false );
14566 * };
14567 * SearchTool.prototype.onUpdateState = function () {};
14568 * // Make this tool available in our toolFactory and thus our toolbar
14569 * toolFactory.register( SearchTool );
14570 *
14571 * // Register two more tools, nothing interesting here
14572 * function SettingsTool() {
14573 * SettingsTool.parent.apply( this, arguments );
14574 * }
14575 * OO.inheritClass( SettingsTool, OO.ui.Tool );
14576 * SettingsTool.static.name = 'settings';
14577 * SettingsTool.static.icon = 'settings';
14578 * SettingsTool.static.title = 'Change settings';
14579 * SettingsTool.prototype.onSelect = function () {
14580 * $area.text( 'Settings tool clicked!' );
14581 * this.setActive( false );
14582 * };
14583 * SettingsTool.prototype.onUpdateState = function () {};
14584 * toolFactory.register( SettingsTool );
14585 *
14586 * // Register two more tools, nothing interesting here
14587 * function StuffTool() {
14588 * StuffTool.parent.apply( this, arguments );
14589 * }
14590 * OO.inheritClass( StuffTool, OO.ui.Tool );
14591 * StuffTool.static.name = 'stuff';
14592 * StuffTool.static.icon = 'ellipsis';
14593 * StuffTool.static.title = 'More stuff';
14594 * StuffTool.prototype.onSelect = function () {
14595 * $area.text( 'More stuff tool clicked!' );
14596 * this.setActive( false );
14597 * };
14598 * StuffTool.prototype.onUpdateState = function () {};
14599 * toolFactory.register( StuffTool );
14600 *
14601 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
14602 * // little popup window (a PopupWidget).
14603 * function HelpTool( toolGroup, config ) {
14604 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
14605 * padded: true,
14606 * label: 'Help',
14607 * head: true
14608 * } }, config ) );
14609 * this.popup.$body.append( '<p>I am helpful!</p>' );
14610 * }
14611 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
14612 * HelpTool.static.name = 'help';
14613 * HelpTool.static.icon = 'help';
14614 * HelpTool.static.title = 'Help';
14615 * toolFactory.register( HelpTool );
14616 *
14617 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
14618 * // used once (but not all defined tools must be used).
14619 * toolbar.setup( [
14620 * {
14621 * // 'bar' tool groups display tools' icons only, side-by-side.
14622 * type: 'bar',
14623 * include: [ 'search', 'help' ]
14624 * },
14625 * {
14626 * // 'list' tool groups display both the titles and icons, in a dropdown list.
14627 * type: 'list',
14628 * indicator: 'down',
14629 * label: 'More',
14630 * include: [ 'settings', 'stuff' ]
14631 * }
14632 * // Note how the tools themselves are toolgroup-agnostic - the same tool can be displayed
14633 * // either in a 'list' or a 'bar'. There is a 'menu' tool group too, not showcased here,
14634 * // since it's more complicated to use. (See the next example snippet on this page.)
14635 * ] );
14636 *
14637 * // Create some UI around the toolbar and place it in the document
14638 * var frame = new OO.ui.PanelLayout( {
14639 * expanded: false,
14640 * framed: true
14641 * } );
14642 * var contentFrame = new OO.ui.PanelLayout( {
14643 * expanded: false,
14644 * padded: true
14645 * } );
14646 * frame.$element.append(
14647 * toolbar.$element,
14648 * contentFrame.$element.append( $area )
14649 * );
14650 * $( 'body' ).append( frame.$element );
14651 *
14652 * // Here is where the toolbar is actually built. This must be done after inserting it into the
14653 * // document.
14654 * toolbar.initialize();
14655 * toolbar.emit( 'updateState' );
14656 *
14657 * The following example extends the previous one to illustrate 'menu' toolgroups and the usage of
14658 * {@link #event-updateState 'updateState' event}.
14659 *
14660 * @example
14661 * // Create the toolbar
14662 * var toolFactory = new OO.ui.ToolFactory();
14663 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
14664 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
14665 *
14666 * // We will be placing status text in this element when tools are used
14667 * var $area = $( '<p>' ).text( 'Toolbar example' );
14668 *
14669 * // Define the tools that we're going to place in our toolbar
14670 *
14671 * // Create a class inheriting from OO.ui.Tool
14672 * function SearchTool() {
14673 * SearchTool.parent.apply( this, arguments );
14674 * }
14675 * OO.inheritClass( SearchTool, OO.ui.Tool );
14676 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
14677 * // of 'icon' and 'title' (displayed icon and text).
14678 * SearchTool.static.name = 'search';
14679 * SearchTool.static.icon = 'search';
14680 * SearchTool.static.title = 'Search...';
14681 * // Defines the action that will happen when this tool is selected (clicked).
14682 * SearchTool.prototype.onSelect = function () {
14683 * $area.text( 'Search tool clicked!' );
14684 * // Never display this tool as "active" (selected).
14685 * this.setActive( false );
14686 * };
14687 * SearchTool.prototype.onUpdateState = function () {};
14688 * // Make this tool available in our toolFactory and thus our toolbar
14689 * toolFactory.register( SearchTool );
14690 *
14691 * // Register two more tools, nothing interesting here
14692 * function SettingsTool() {
14693 * SettingsTool.parent.apply( this, arguments );
14694 * this.reallyActive = false;
14695 * }
14696 * OO.inheritClass( SettingsTool, OO.ui.Tool );
14697 * SettingsTool.static.name = 'settings';
14698 * SettingsTool.static.icon = 'settings';
14699 * SettingsTool.static.title = 'Change settings';
14700 * SettingsTool.prototype.onSelect = function () {
14701 * $area.text( 'Settings tool clicked!' );
14702 * // Toggle the active state on each click
14703 * this.reallyActive = !this.reallyActive;
14704 * this.setActive( this.reallyActive );
14705 * // To update the menu label
14706 * this.toolbar.emit( 'updateState' );
14707 * };
14708 * SettingsTool.prototype.onUpdateState = function () {};
14709 * toolFactory.register( SettingsTool );
14710 *
14711 * // Register two more tools, nothing interesting here
14712 * function StuffTool() {
14713 * StuffTool.parent.apply( this, arguments );
14714 * this.reallyActive = false;
14715 * }
14716 * OO.inheritClass( StuffTool, OO.ui.Tool );
14717 * StuffTool.static.name = 'stuff';
14718 * StuffTool.static.icon = 'ellipsis';
14719 * StuffTool.static.title = 'More stuff';
14720 * StuffTool.prototype.onSelect = function () {
14721 * $area.text( 'More stuff tool clicked!' );
14722 * // Toggle the active state on each click
14723 * this.reallyActive = !this.reallyActive;
14724 * this.setActive( this.reallyActive );
14725 * // To update the menu label
14726 * this.toolbar.emit( 'updateState' );
14727 * };
14728 * StuffTool.prototype.onUpdateState = function () {};
14729 * toolFactory.register( StuffTool );
14730 *
14731 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
14732 * // little popup window (a PopupWidget). 'onUpdateState' is also already implemented.
14733 * function HelpTool( toolGroup, config ) {
14734 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
14735 * padded: true,
14736 * label: 'Help',
14737 * head: true
14738 * } }, config ) );
14739 * this.popup.$body.append( '<p>I am helpful!</p>' );
14740 * }
14741 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
14742 * HelpTool.static.name = 'help';
14743 * HelpTool.static.icon = 'help';
14744 * HelpTool.static.title = 'Help';
14745 * toolFactory.register( HelpTool );
14746 *
14747 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
14748 * // used once (but not all defined tools must be used).
14749 * toolbar.setup( [
14750 * {
14751 * // 'bar' tool groups display tools' icons only, side-by-side.
14752 * type: 'bar',
14753 * include: [ 'search', 'help' ]
14754 * },
14755 * {
14756 * // 'menu' tool groups display both the titles and icons, in a dropdown menu.
14757 * // Menu label indicates which items are selected.
14758 * type: 'menu',
14759 * indicator: 'down',
14760 * include: [ 'settings', 'stuff' ]
14761 * }
14762 * ] );
14763 *
14764 * // Create some UI around the toolbar and place it in the document
14765 * var frame = new OO.ui.PanelLayout( {
14766 * expanded: false,
14767 * framed: true
14768 * } );
14769 * var contentFrame = new OO.ui.PanelLayout( {
14770 * expanded: false,
14771 * padded: true
14772 * } );
14773 * frame.$element.append(
14774 * toolbar.$element,
14775 * contentFrame.$element.append( $area )
14776 * );
14777 * $( 'body' ).append( frame.$element );
14778 *
14779 * // Here is where the toolbar is actually built. This must be done after inserting it into the
14780 * // document.
14781 * toolbar.initialize();
14782 * toolbar.emit( 'updateState' );
14783 *
14784 * @class
14785 * @extends OO.ui.Element
14786 * @mixins OO.EventEmitter
14787 * @mixins OO.ui.mixin.GroupElement
14788 *
14789 * @constructor
14790 * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
14791 * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating toolgroups
14792 * @param {Object} [config] Configuration options
14793 * @cfg {boolean} [actions] Add an actions section to the toolbar. Actions are commands that are included
14794 * in the toolbar, but are not configured as tools. By default, actions are displayed on the right side of
14795 * the toolbar.
14796 * @cfg {boolean} [shadow] Add a shadow below the toolbar.
14797 */
14798 OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
14799 // Allow passing positional parameters inside the config object
14800 if ( OO.isPlainObject( toolFactory ) && config === undefined ) {
14801 config = toolFactory;
14802 toolFactory = config.toolFactory;
14803 toolGroupFactory = config.toolGroupFactory;
14804 }
14805
14806 // Configuration initialization
14807 config = config || {};
14808
14809 // Parent constructor
14810 OO.ui.Toolbar.parent.call( this, config );
14811
14812 // Mixin constructors
14813 OO.EventEmitter.call( this );
14814 OO.ui.mixin.GroupElement.call( this, config );
14815
14816 // Properties
14817 this.toolFactory = toolFactory;
14818 this.toolGroupFactory = toolGroupFactory;
14819 this.groups = [];
14820 this.tools = {};
14821 this.$bar = $( '<div>' );
14822 this.$actions = $( '<div>' );
14823 this.initialized = false;
14824 this.onWindowResizeHandler = this.onWindowResize.bind( this );
14825
14826 // Events
14827 this.$element
14828 .add( this.$bar ).add( this.$group ).add( this.$actions )
14829 .on( 'mousedown keydown', this.onPointerDown.bind( this ) );
14830
14831 // Initialization
14832 this.$group.addClass( 'oo-ui-toolbar-tools' );
14833 if ( config.actions ) {
14834 this.$bar.append( this.$actions.addClass( 'oo-ui-toolbar-actions' ) );
14835 }
14836 this.$bar
14837 .addClass( 'oo-ui-toolbar-bar' )
14838 .append( this.$group, '<div style="clear:both"></div>' );
14839 if ( config.shadow ) {
14840 this.$bar.append( '<div class="oo-ui-toolbar-shadow"></div>' );
14841 }
14842 this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar );
14843 };
14844
14845 /* Setup */
14846
14847 OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
14848 OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
14849 OO.mixinClass( OO.ui.Toolbar, OO.ui.mixin.GroupElement );
14850
14851 /* Events */
14852
14853 /**
14854 * @event updateState
14855 *
14856 * An 'updateState' event must be emitted on the Toolbar (by calling `toolbar.emit( 'updateState' )`)
14857 * every time the state of the application using the toolbar changes, and an update to the state of
14858 * tools is required.
14859 *
14860 * @param {Mixed...} data Application-defined parameters
14861 */
14862
14863 /* Methods */
14864
14865 /**
14866 * Get the tool factory.
14867 *
14868 * @return {OO.ui.ToolFactory} Tool factory
14869 */
14870 OO.ui.Toolbar.prototype.getToolFactory = function () {
14871 return this.toolFactory;
14872 };
14873
14874 /**
14875 * Get the toolgroup factory.
14876 *
14877 * @return {OO.Factory} Toolgroup factory
14878 */
14879 OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
14880 return this.toolGroupFactory;
14881 };
14882
14883 /**
14884 * Handles mouse down events.
14885 *
14886 * @private
14887 * @param {jQuery.Event} e Mouse down event
14888 */
14889 OO.ui.Toolbar.prototype.onPointerDown = function ( e ) {
14890 var $closestWidgetToEvent = $( e.target ).closest( '.oo-ui-widget' ),
14891 $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
14892 if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[ 0 ] === $closestWidgetToToolbar[ 0 ] ) {
14893 return false;
14894 }
14895 };
14896
14897 /**
14898 * Handle window resize event.
14899 *
14900 * @private
14901 * @param {jQuery.Event} e Window resize event
14902 */
14903 OO.ui.Toolbar.prototype.onWindowResize = function () {
14904 this.$element.toggleClass(
14905 'oo-ui-toolbar-narrow',
14906 this.$bar.width() <= this.narrowThreshold
14907 );
14908 };
14909
14910 /**
14911 * Sets up handles and preloads required information for the toolbar to work.
14912 * This must be called after it is attached to a visible document and before doing anything else.
14913 */
14914 OO.ui.Toolbar.prototype.initialize = function () {
14915 if ( !this.initialized ) {
14916 this.initialized = true;
14917 this.narrowThreshold = this.$group.width() + this.$actions.width();
14918 $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
14919 this.onWindowResize();
14920 }
14921 };
14922
14923 /**
14924 * Set up the toolbar.
14925 *
14926 * The toolbar is set up with a list of toolgroup configurations that specify the type of
14927 * toolgroup ({@link OO.ui.BarToolGroup bar}, {@link OO.ui.MenuToolGroup menu}, or {@link OO.ui.ListToolGroup list})
14928 * to add and which tools to include, exclude, promote, or demote within that toolgroup. Please
14929 * see {@link OO.ui.ToolGroup toolgroups} for more information about including tools in toolgroups.
14930 *
14931 * @param {Object.<string,Array>} groups List of toolgroup configurations
14932 * @param {Array|string} [groups.include] Tools to include in the toolgroup
14933 * @param {Array|string} [groups.exclude] Tools to exclude from the toolgroup
14934 * @param {Array|string} [groups.promote] Tools to promote to the beginning of the toolgroup
14935 * @param {Array|string} [groups.demote] Tools to demote to the end of the toolgroup
14936 */
14937 OO.ui.Toolbar.prototype.setup = function ( groups ) {
14938 var i, len, type, group,
14939 items = [],
14940 defaultType = 'bar';
14941
14942 // Cleanup previous groups
14943 this.reset();
14944
14945 // Build out new groups
14946 for ( i = 0, len = groups.length; i < len; i++ ) {
14947 group = groups[ i ];
14948 if ( group.include === '*' ) {
14949 // Apply defaults to catch-all groups
14950 if ( group.type === undefined ) {
14951 group.type = 'list';
14952 }
14953 if ( group.label === undefined ) {
14954 group.label = OO.ui.msg( 'ooui-toolbar-more' );
14955 }
14956 }
14957 // Check type has been registered
14958 type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType;
14959 items.push(
14960 this.getToolGroupFactory().create( type, this, group )
14961 );
14962 }
14963 this.addItems( items );
14964 };
14965
14966 /**
14967 * Remove all tools and toolgroups from the toolbar.
14968 */
14969 OO.ui.Toolbar.prototype.reset = function () {
14970 var i, len;
14971
14972 this.groups = [];
14973 this.tools = {};
14974 for ( i = 0, len = this.items.length; i < len; i++ ) {
14975 this.items[ i ].destroy();
14976 }
14977 this.clearItems();
14978 };
14979
14980 /**
14981 * Destroy the toolbar.
14982 *
14983 * Destroying the toolbar removes all event handlers and DOM elements that constitute the toolbar. Call
14984 * this method whenever you are done using a toolbar.
14985 */
14986 OO.ui.Toolbar.prototype.destroy = function () {
14987 $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
14988 this.reset();
14989 this.$element.remove();
14990 };
14991
14992 /**
14993 * Check if the tool is available.
14994 *
14995 * Available tools are ones that have not yet been added to the toolbar.
14996 *
14997 * @param {string} name Symbolic name of tool
14998 * @return {boolean} Tool is available
14999 */
15000 OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
15001 return !this.tools[ name ];
15002 };
15003
15004 /**
15005 * Prevent tool from being used again.
15006 *
15007 * @param {OO.ui.Tool} tool Tool to reserve
15008 */
15009 OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
15010 this.tools[ tool.getName() ] = tool;
15011 };
15012
15013 /**
15014 * Allow tool to be used again.
15015 *
15016 * @param {OO.ui.Tool} tool Tool to release
15017 */
15018 OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
15019 delete this.tools[ tool.getName() ];
15020 };
15021
15022 /**
15023 * Get accelerator label for tool.
15024 *
15025 * The OOjs UI library does not contain an accelerator system, but this is the hook for one. To
15026 * use an accelerator system, subclass the toolbar and override this method, which is meant to return a label
15027 * that describes the accelerator keys for the tool passed (by symbolic name) to the method.
15028 *
15029 * @param {string} name Symbolic name of tool
15030 * @return {string|undefined} Tool accelerator label if available
15031 */
15032 OO.ui.Toolbar.prototype.getToolAccelerator = function () {
15033 return undefined;
15034 };
15035
15036 /**
15037 * Tools, together with {@link OO.ui.ToolGroup toolgroups}, constitute {@link OO.ui.Toolbar toolbars}.
15038 * Each tool is configured with a static name, title, and icon and is customized with the command to carry
15039 * out when the tool is selected. Tools must also be registered with a {@link OO.ui.ToolFactory tool factory},
15040 * which creates the tools on demand.
15041 *
15042 * Every Tool subclass must implement two methods:
15043 *
15044 * - {@link #onUpdateState}
15045 * - {@link #onSelect}
15046 *
15047 * Tools are added to toolgroups ({@link OO.ui.ListToolGroup ListToolGroup},
15048 * {@link OO.ui.BarToolGroup BarToolGroup}, or {@link OO.ui.MenuToolGroup MenuToolGroup}), which determine how
15049 * the tool is displayed in the toolbar. See {@link OO.ui.Toolbar toolbars} for an example.
15050 *
15051 * For more information, please see the [OOjs UI documentation on MediaWiki][1].
15052 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
15053 *
15054 * @abstract
15055 * @class
15056 * @extends OO.ui.Widget
15057 * @mixins OO.ui.mixin.IconElement
15058 * @mixins OO.ui.mixin.FlaggedElement
15059 * @mixins OO.ui.mixin.TabIndexedElement
15060 *
15061 * @constructor
15062 * @param {OO.ui.ToolGroup} toolGroup
15063 * @param {Object} [config] Configuration options
15064 * @cfg {string|Function} [title] Title text or a function that returns text. If this config is omitted, the value of
15065 * the {@link #static-title static title} property is used.
15066 *
15067 * The title is used in different ways depending on the type of toolgroup that contains the tool. The
15068 * 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
15069 * part of a {@link OO.ui.ListToolGroup list} or {@link OO.ui.MenuToolGroup menu} toolgroup.
15070 *
15071 * For bar toolgroups, a description of the accelerator key is appended to the title if an accelerator key
15072 * is associated with an action by the same name as the tool and accelerator functionality has been added to the application.
15073 * To add accelerator key functionality, you must subclass OO.ui.Toolbar and override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method.
15074 */
15075 OO.ui.Tool = function OoUiTool( toolGroup, config ) {
15076 // Allow passing positional parameters inside the config object
15077 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
15078 config = toolGroup;
15079 toolGroup = config.toolGroup;
15080 }
15081
15082 // Configuration initialization
15083 config = config || {};
15084
15085 // Parent constructor
15086 OO.ui.Tool.parent.call( this, config );
15087
15088 // Properties
15089 this.toolGroup = toolGroup;
15090 this.toolbar = this.toolGroup.getToolbar();
15091 this.active = false;
15092 this.$title = $( '<span>' );
15093 this.$accel = $( '<span>' );
15094 this.$link = $( '<a>' );
15095 this.title = null;
15096
15097 // Mixin constructors
15098 OO.ui.mixin.IconElement.call( this, config );
15099 OO.ui.mixin.FlaggedElement.call( this, config );
15100 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$link } ) );
15101
15102 // Events
15103 this.toolbar.connect( this, { updateState: 'onUpdateState' } );
15104
15105 // Initialization
15106 this.$title.addClass( 'oo-ui-tool-title' );
15107 this.$accel
15108 .addClass( 'oo-ui-tool-accel' )
15109 .prop( {
15110 // This may need to be changed if the key names are ever localized,
15111 // but for now they are essentially written in English
15112 dir: 'ltr',
15113 lang: 'en'
15114 } );
15115 this.$link
15116 .addClass( 'oo-ui-tool-link' )
15117 .append( this.$icon, this.$title, this.$accel )
15118 .attr( 'role', 'button' );
15119 this.$element
15120 .data( 'oo-ui-tool', this )
15121 .addClass(
15122 'oo-ui-tool ' + 'oo-ui-tool-name-' +
15123 this.constructor.static.name.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' )
15124 )
15125 .toggleClass( 'oo-ui-tool-with-label', this.constructor.static.displayBothIconAndLabel )
15126 .append( this.$link );
15127 this.setTitle( config.title || this.constructor.static.title );
15128 };
15129
15130 /* Setup */
15131
15132 OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
15133 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.IconElement );
15134 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.FlaggedElement );
15135 OO.mixinClass( OO.ui.Tool, OO.ui.mixin.TabIndexedElement );
15136
15137 /* Static Properties */
15138
15139 /**
15140 * @static
15141 * @inheritdoc
15142 */
15143 OO.ui.Tool.static.tagName = 'span';
15144
15145 /**
15146 * Symbolic name of tool.
15147 *
15148 * The symbolic name is used internally to register the tool with a {@link OO.ui.ToolFactory ToolFactory}. It can
15149 * also be used when adding tools to toolgroups.
15150 *
15151 * @abstract
15152 * @static
15153 * @inheritable
15154 * @property {string}
15155 */
15156 OO.ui.Tool.static.name = '';
15157
15158 /**
15159 * Symbolic name of the group.
15160 *
15161 * The group name is used to associate tools with each other so that they can be selected later by
15162 * a {@link OO.ui.ToolGroup toolgroup}.
15163 *
15164 * @abstract
15165 * @static
15166 * @inheritable
15167 * @property {string}
15168 */
15169 OO.ui.Tool.static.group = '';
15170
15171 /**
15172 * 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.
15173 *
15174 * @abstract
15175 * @static
15176 * @inheritable
15177 * @property {string|Function}
15178 */
15179 OO.ui.Tool.static.title = '';
15180
15181 /**
15182 * Display both icon and label when the tool is used in a {@link OO.ui.BarToolGroup bar} toolgroup.
15183 * Normally only the icon is displayed, or only the label if no icon is given.
15184 *
15185 * @static
15186 * @inheritable
15187 * @property {boolean}
15188 */
15189 OO.ui.Tool.static.displayBothIconAndLabel = false;
15190
15191 /**
15192 * Add tool to catch-all groups automatically.
15193 *
15194 * A catch-all group, which contains all tools that do not currently belong to a toolgroup,
15195 * can be included in a toolgroup using the wildcard selector, an asterisk (*).
15196 *
15197 * @static
15198 * @inheritable
15199 * @property {boolean}
15200 */
15201 OO.ui.Tool.static.autoAddToCatchall = true;
15202
15203 /**
15204 * Add tool to named groups automatically.
15205 *
15206 * By default, tools that are configured with a static ‘group’ property are added
15207 * to that group and will be selected when the symbolic name of the group is specified (e.g., when
15208 * toolgroups include tools by group name).
15209 *
15210 * @static
15211 * @property {boolean}
15212 * @inheritable
15213 */
15214 OO.ui.Tool.static.autoAddToGroup = true;
15215
15216 /**
15217 * Check if this tool is compatible with given data.
15218 *
15219 * This is a stub that can be overridden to provide support for filtering tools based on an
15220 * arbitrary piece of information (e.g., where the cursor is in a document). The implementation
15221 * must also call this method so that the compatibility check can be performed.
15222 *
15223 * @static
15224 * @inheritable
15225 * @param {Mixed} data Data to check
15226 * @return {boolean} Tool can be used with data
15227 */
15228 OO.ui.Tool.static.isCompatibleWith = function () {
15229 return false;
15230 };
15231
15232 /* Methods */
15233
15234 /**
15235 * Handle the toolbar state being updated. This method is called when the
15236 * {@link OO.ui.Toolbar#event-updateState 'updateState' event} is emitted on the
15237 * {@link OO.ui.Toolbar Toolbar} that uses this tool, and should set the state of this tool
15238 * depending on application state (usually by calling #setDisabled to enable or disable the tool,
15239 * or #setActive to mark is as currently in-use or not).
15240 *
15241 * This is an abstract method that must be overridden in a concrete subclass.
15242 *
15243 * @method
15244 * @protected
15245 * @abstract
15246 */
15247 OO.ui.Tool.prototype.onUpdateState = null;
15248
15249 /**
15250 * Handle the tool being selected. This method is called when the user triggers this tool,
15251 * usually by clicking on its label/icon.
15252 *
15253 * This is an abstract method that must be overridden in a concrete subclass.
15254 *
15255 * @method
15256 * @protected
15257 * @abstract
15258 */
15259 OO.ui.Tool.prototype.onSelect = null;
15260
15261 /**
15262 * Check if the tool is active.
15263 *
15264 * Tools become active when their #onSelect or #onUpdateState handlers change them to appear pressed
15265 * with the #setActive method. Additional CSS is applied to the tool to reflect the active state.
15266 *
15267 * @return {boolean} Tool is active
15268 */
15269 OO.ui.Tool.prototype.isActive = function () {
15270 return this.active;
15271 };
15272
15273 /**
15274 * Make the tool appear active or inactive.
15275 *
15276 * This method should be called within #onSelect or #onUpdateState event handlers to make the tool
15277 * appear pressed or not.
15278 *
15279 * @param {boolean} state Make tool appear active
15280 */
15281 OO.ui.Tool.prototype.setActive = function ( state ) {
15282 this.active = !!state;
15283 if ( this.active ) {
15284 this.$element.addClass( 'oo-ui-tool-active' );
15285 } else {
15286 this.$element.removeClass( 'oo-ui-tool-active' );
15287 }
15288 };
15289
15290 /**
15291 * Set the tool #title.
15292 *
15293 * @param {string|Function} title Title text or a function that returns text
15294 * @chainable
15295 */
15296 OO.ui.Tool.prototype.setTitle = function ( title ) {
15297 this.title = OO.ui.resolveMsg( title );
15298 this.updateTitle();
15299 return this;
15300 };
15301
15302 /**
15303 * Get the tool #title.
15304 *
15305 * @return {string} Title text
15306 */
15307 OO.ui.Tool.prototype.getTitle = function () {
15308 return this.title;
15309 };
15310
15311 /**
15312 * Get the tool's symbolic name.
15313 *
15314 * @return {string} Symbolic name of tool
15315 */
15316 OO.ui.Tool.prototype.getName = function () {
15317 return this.constructor.static.name;
15318 };
15319
15320 /**
15321 * Update the title.
15322 */
15323 OO.ui.Tool.prototype.updateTitle = function () {
15324 var titleTooltips = this.toolGroup.constructor.static.titleTooltips,
15325 accelTooltips = this.toolGroup.constructor.static.accelTooltips,
15326 accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
15327 tooltipParts = [];
15328
15329 this.$title.text( this.title );
15330 this.$accel.text( accel );
15331
15332 if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
15333 tooltipParts.push( this.title );
15334 }
15335 if ( accelTooltips && typeof accel === 'string' && accel.length ) {
15336 tooltipParts.push( accel );
15337 }
15338 if ( tooltipParts.length ) {
15339 this.$link.attr( 'title', tooltipParts.join( ' ' ) );
15340 } else {
15341 this.$link.removeAttr( 'title' );
15342 }
15343 };
15344
15345 /**
15346 * Destroy tool.
15347 *
15348 * Destroying the tool removes all event handlers and the tool’s DOM elements.
15349 * Call this method whenever you are done using a tool.
15350 */
15351 OO.ui.Tool.prototype.destroy = function () {
15352 this.toolbar.disconnect( this );
15353 this.$element.remove();
15354 };
15355
15356 /**
15357 * ToolGroups are collections of {@link OO.ui.Tool tools} that are used in a {@link OO.ui.Toolbar toolbar}.
15358 * The type of toolgroup ({@link OO.ui.ListToolGroup list}, {@link OO.ui.BarToolGroup bar}, or {@link OO.ui.MenuToolGroup menu})
15359 * to which a tool belongs determines how the tool is arranged and displayed in the toolbar. Toolgroups
15360 * themselves are created on demand with a {@link OO.ui.ToolGroupFactory toolgroup factory}.
15361 *
15362 * Toolgroups can contain individual tools, groups of tools, or all available tools, as specified
15363 * using the `include` config option. See OO.ui.ToolFactory#extract on documentation of the format.
15364 * The options `exclude`, `promote`, and `demote` support the same formats.
15365 *
15366 * See {@link OO.ui.Toolbar toolbars} for a full example. For more information about toolbars in general,
15367 * please see the [OOjs UI documentation on MediaWiki][1].
15368 *
15369 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
15370 *
15371 * @abstract
15372 * @class
15373 * @extends OO.ui.Widget
15374 * @mixins OO.ui.mixin.GroupElement
15375 *
15376 * @constructor
15377 * @param {OO.ui.Toolbar} toolbar
15378 * @param {Object} [config] Configuration options
15379 * @cfg {Array|string} [include] List of tools to include in the toolgroup, see above.
15380 * @cfg {Array|string} [exclude] List of tools to exclude from the toolgroup, see above.
15381 * @cfg {Array|string} [promote] List of tools to promote to the beginning of the toolgroup, see above.
15382 * @cfg {Array|string} [demote] List of tools to demote to the end of the toolgroup, see above.
15383 * This setting is particularly useful when tools have been added to the toolgroup
15384 * en masse (e.g., via the catch-all selector).
15385 */
15386 OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
15387 // Allow passing positional parameters inside the config object
15388 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
15389 config = toolbar;
15390 toolbar = config.toolbar;
15391 }
15392
15393 // Configuration initialization
15394 config = config || {};
15395
15396 // Parent constructor
15397 OO.ui.ToolGroup.parent.call( this, config );
15398
15399 // Mixin constructors
15400 OO.ui.mixin.GroupElement.call( this, config );
15401
15402 // Properties
15403 this.toolbar = toolbar;
15404 this.tools = {};
15405 this.pressed = null;
15406 this.autoDisabled = false;
15407 this.include = config.include || [];
15408 this.exclude = config.exclude || [];
15409 this.promote = config.promote || [];
15410 this.demote = config.demote || [];
15411 this.onCapturedMouseKeyUpHandler = this.onCapturedMouseKeyUp.bind( this );
15412
15413 // Events
15414 this.$element.on( {
15415 mousedown: this.onMouseKeyDown.bind( this ),
15416 mouseup: this.onMouseKeyUp.bind( this ),
15417 keydown: this.onMouseKeyDown.bind( this ),
15418 keyup: this.onMouseKeyUp.bind( this ),
15419 focus: this.onMouseOverFocus.bind( this ),
15420 blur: this.onMouseOutBlur.bind( this ),
15421 mouseover: this.onMouseOverFocus.bind( this ),
15422 mouseout: this.onMouseOutBlur.bind( this )
15423 } );
15424 this.toolbar.getToolFactory().connect( this, { register: 'onToolFactoryRegister' } );
15425 this.aggregate( { disable: 'itemDisable' } );
15426 this.connect( this, { itemDisable: 'updateDisabled' } );
15427
15428 // Initialization
15429 this.$group.addClass( 'oo-ui-toolGroup-tools' );
15430 this.$element
15431 .addClass( 'oo-ui-toolGroup' )
15432 .append( this.$group );
15433 this.populate();
15434 };
15435
15436 /* Setup */
15437
15438 OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget );
15439 OO.mixinClass( OO.ui.ToolGroup, OO.ui.mixin.GroupElement );
15440
15441 /* Events */
15442
15443 /**
15444 * @event update
15445 */
15446
15447 /* Static Properties */
15448
15449 /**
15450 * Show labels in tooltips.
15451 *
15452 * @static
15453 * @inheritable
15454 * @property {boolean}
15455 */
15456 OO.ui.ToolGroup.static.titleTooltips = false;
15457
15458 /**
15459 * Show acceleration labels in tooltips.
15460 *
15461 * Note: The OOjs UI library does not include an accelerator system, but does contain
15462 * a hook for one. To use an accelerator system, subclass the {@link OO.ui.Toolbar toolbar} and
15463 * override the {@link OO.ui.Toolbar#getToolAccelerator getToolAccelerator} method, which is
15464 * meant to return a label that describes the accelerator keys for a given tool (e.g., 'Ctrl + M').
15465 *
15466 * @static
15467 * @inheritable
15468 * @property {boolean}
15469 */
15470 OO.ui.ToolGroup.static.accelTooltips = false;
15471
15472 /**
15473 * Automatically disable the toolgroup when all tools are disabled
15474 *
15475 * @static
15476 * @inheritable
15477 * @property {boolean}
15478 */
15479 OO.ui.ToolGroup.static.autoDisable = true;
15480
15481 /* Methods */
15482
15483 /**
15484 * @inheritdoc
15485 */
15486 OO.ui.ToolGroup.prototype.isDisabled = function () {
15487 return this.autoDisabled || OO.ui.ToolGroup.parent.prototype.isDisabled.apply( this, arguments );
15488 };
15489
15490 /**
15491 * @inheritdoc
15492 */
15493 OO.ui.ToolGroup.prototype.updateDisabled = function () {
15494 var i, item, allDisabled = true;
15495
15496 if ( this.constructor.static.autoDisable ) {
15497 for ( i = this.items.length - 1; i >= 0; i-- ) {
15498 item = this.items[ i ];
15499 if ( !item.isDisabled() ) {
15500 allDisabled = false;
15501 break;
15502 }
15503 }
15504 this.autoDisabled = allDisabled;
15505 }
15506 OO.ui.ToolGroup.parent.prototype.updateDisabled.apply( this, arguments );
15507 };
15508
15509 /**
15510 * Handle mouse down and key down events.
15511 *
15512 * @protected
15513 * @param {jQuery.Event} e Mouse down or key down event
15514 */
15515 OO.ui.ToolGroup.prototype.onMouseKeyDown = function ( e ) {
15516 if (
15517 !this.isDisabled() &&
15518 ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
15519 ) {
15520 this.pressed = this.getTargetTool( e );
15521 if ( this.pressed ) {
15522 this.pressed.setActive( true );
15523 this.getElementDocument().addEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler, true );
15524 this.getElementDocument().addEventListener( 'keyup', this.onCapturedMouseKeyUpHandler, true );
15525 }
15526 return false;
15527 }
15528 };
15529
15530 /**
15531 * Handle captured mouse up and key up events.
15532 *
15533 * @protected
15534 * @param {Event} e Mouse up or key up event
15535 */
15536 OO.ui.ToolGroup.prototype.onCapturedMouseKeyUp = function ( e ) {
15537 this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseKeyUpHandler, true );
15538 this.getElementDocument().removeEventListener( 'keyup', this.onCapturedMouseKeyUpHandler, true );
15539 // onMouseKeyUp may be called a second time, depending on where the mouse is when the button is
15540 // released, but since `this.pressed` will no longer be true, the second call will be ignored.
15541 this.onMouseKeyUp( e );
15542 };
15543
15544 /**
15545 * Handle mouse up and key up events.
15546 *
15547 * @protected
15548 * @param {jQuery.Event} e Mouse up or key up event
15549 */
15550 OO.ui.ToolGroup.prototype.onMouseKeyUp = function ( e ) {
15551 var tool = this.getTargetTool( e );
15552
15553 if (
15554 !this.isDisabled() && this.pressed && this.pressed === tool &&
15555 ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
15556 ) {
15557 this.pressed.onSelect();
15558 this.pressed = null;
15559 return false;
15560 }
15561
15562 this.pressed = null;
15563 };
15564
15565 /**
15566 * Handle mouse over and focus events.
15567 *
15568 * @protected
15569 * @param {jQuery.Event} e Mouse over or focus event
15570 */
15571 OO.ui.ToolGroup.prototype.onMouseOverFocus = function ( e ) {
15572 var tool = this.getTargetTool( e );
15573
15574 if ( this.pressed && this.pressed === tool ) {
15575 this.pressed.setActive( true );
15576 }
15577 };
15578
15579 /**
15580 * Handle mouse out and blur events.
15581 *
15582 * @protected
15583 * @param {jQuery.Event} e Mouse out or blur event
15584 */
15585 OO.ui.ToolGroup.prototype.onMouseOutBlur = function ( e ) {
15586 var tool = this.getTargetTool( e );
15587
15588 if ( this.pressed && this.pressed === tool ) {
15589 this.pressed.setActive( false );
15590 }
15591 };
15592
15593 /**
15594 * Get the closest tool to a jQuery.Event.
15595 *
15596 * Only tool links are considered, which prevents other elements in the tool such as popups from
15597 * triggering tool group interactions.
15598 *
15599 * @private
15600 * @param {jQuery.Event} e
15601 * @return {OO.ui.Tool|null} Tool, `null` if none was found
15602 */
15603 OO.ui.ToolGroup.prototype.getTargetTool = function ( e ) {
15604 var tool,
15605 $item = $( e.target ).closest( '.oo-ui-tool-link' );
15606
15607 if ( $item.length ) {
15608 tool = $item.parent().data( 'oo-ui-tool' );
15609 }
15610
15611 return tool && !tool.isDisabled() ? tool : null;
15612 };
15613
15614 /**
15615 * Handle tool registry register events.
15616 *
15617 * If a tool is registered after the group is created, we must repopulate the list to account for:
15618 *
15619 * - a tool being added that may be included
15620 * - a tool already included being overridden
15621 *
15622 * @protected
15623 * @param {string} name Symbolic name of tool
15624 */
15625 OO.ui.ToolGroup.prototype.onToolFactoryRegister = function () {
15626 this.populate();
15627 };
15628
15629 /**
15630 * Get the toolbar that contains the toolgroup.
15631 *
15632 * @return {OO.ui.Toolbar} Toolbar that contains the toolgroup
15633 */
15634 OO.ui.ToolGroup.prototype.getToolbar = function () {
15635 return this.toolbar;
15636 };
15637
15638 /**
15639 * Add and remove tools based on configuration.
15640 */
15641 OO.ui.ToolGroup.prototype.populate = function () {
15642 var i, len, name, tool,
15643 toolFactory = this.toolbar.getToolFactory(),
15644 names = {},
15645 add = [],
15646 remove = [],
15647 list = this.toolbar.getToolFactory().getTools(
15648 this.include, this.exclude, this.promote, this.demote
15649 );
15650
15651 // Build a list of needed tools
15652 for ( i = 0, len = list.length; i < len; i++ ) {
15653 name = list[ i ];
15654 if (
15655 // Tool exists
15656 toolFactory.lookup( name ) &&
15657 // Tool is available or is already in this group
15658 ( this.toolbar.isToolAvailable( name ) || this.tools[ name ] )
15659 ) {
15660 // Hack to prevent infinite recursion via ToolGroupTool. We need to reserve the tool before
15661 // creating it, but we can't call reserveTool() yet because we haven't created the tool.
15662 this.toolbar.tools[ name ] = true;
15663 tool = this.tools[ name ];
15664 if ( !tool ) {
15665 // Auto-initialize tools on first use
15666 this.tools[ name ] = tool = toolFactory.create( name, this );
15667 tool.updateTitle();
15668 }
15669 this.toolbar.reserveTool( tool );
15670 add.push( tool );
15671 names[ name ] = true;
15672 }
15673 }
15674 // Remove tools that are no longer needed
15675 for ( name in this.tools ) {
15676 if ( !names[ name ] ) {
15677 this.tools[ name ].destroy();
15678 this.toolbar.releaseTool( this.tools[ name ] );
15679 remove.push( this.tools[ name ] );
15680 delete this.tools[ name ];
15681 }
15682 }
15683 if ( remove.length ) {
15684 this.removeItems( remove );
15685 }
15686 // Update emptiness state
15687 if ( add.length ) {
15688 this.$element.removeClass( 'oo-ui-toolGroup-empty' );
15689 } else {
15690 this.$element.addClass( 'oo-ui-toolGroup-empty' );
15691 }
15692 // Re-add tools (moving existing ones to new locations)
15693 this.addItems( add );
15694 // Disabled state may depend on items
15695 this.updateDisabled();
15696 };
15697
15698 /**
15699 * Destroy toolgroup.
15700 */
15701 OO.ui.ToolGroup.prototype.destroy = function () {
15702 var name;
15703
15704 this.clearItems();
15705 this.toolbar.getToolFactory().disconnect( this );
15706 for ( name in this.tools ) {
15707 this.toolbar.releaseTool( this.tools[ name ] );
15708 this.tools[ name ].disconnect( this ).destroy();
15709 delete this.tools[ name ];
15710 }
15711 this.$element.remove();
15712 };
15713
15714 /**
15715 * A ToolFactory creates tools on demand. All tools ({@link OO.ui.Tool Tools}, {@link OO.ui.PopupTool PopupTools},
15716 * and {@link OO.ui.ToolGroupTool ToolGroupTools}) must be registered with a tool factory. Tools are
15717 * registered by their symbolic name. See {@link OO.ui.Toolbar toolbars} for an example.
15718 *
15719 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
15720 *
15721 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
15722 *
15723 * @class
15724 * @extends OO.Factory
15725 * @constructor
15726 */
15727 OO.ui.ToolFactory = function OoUiToolFactory() {
15728 // Parent constructor
15729 OO.ui.ToolFactory.parent.call( this );
15730 };
15731
15732 /* Setup */
15733
15734 OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
15735
15736 /* Methods */
15737
15738 /**
15739 * Get tools from the factory
15740 *
15741 * @param {Array|string} [include] Included tools, see #extract for format
15742 * @param {Array|string} [exclude] Excluded tools, see #extract for format
15743 * @param {Array|string} [promote] Promoted tools, see #extract for format
15744 * @param {Array|string} [demote] Demoted tools, see #extract for format
15745 * @return {string[]} List of tools
15746 */
15747 OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
15748 var i, len, included, promoted, demoted,
15749 auto = [],
15750 used = {};
15751
15752 // Collect included and not excluded tools
15753 included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
15754
15755 // Promotion
15756 promoted = this.extract( promote, used );
15757 demoted = this.extract( demote, used );
15758
15759 // Auto
15760 for ( i = 0, len = included.length; i < len; i++ ) {
15761 if ( !used[ included[ i ] ] ) {
15762 auto.push( included[ i ] );
15763 }
15764 }
15765
15766 return promoted.concat( auto ).concat( demoted );
15767 };
15768
15769 /**
15770 * Get a flat list of names from a list of names or groups.
15771 *
15772 * Normally, `collection` is an array of tool specifications. Tools can be specified in the
15773 * following ways:
15774 *
15775 * - To include an individual tool, use the symbolic name: `{ name: 'tool-name' }` or `'tool-name'`.
15776 * - To include all tools in a group, use the group name: `{ group: 'group-name' }`. (To assign the
15777 * tool to a group, use OO.ui.Tool.static.group.)
15778 *
15779 * Alternatively, to include all tools that are not yet assigned to any other toolgroup, use the
15780 * catch-all selector `'*'`.
15781 *
15782 * If `used` is passed, tool names that appear as properties in this object will be considered
15783 * already assigned, and will not be returned even if specified otherwise. The tool names extracted
15784 * by this function call will be added as new properties in the object.
15785 *
15786 * @private
15787 * @param {Array|string} collection List of tools, see above
15788 * @param {Object} [used] Object containing information about used tools, see above
15789 * @return {string[]} List of extracted tool names
15790 */
15791 OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
15792 var i, len, item, name, tool,
15793 names = [];
15794
15795 if ( collection === '*' ) {
15796 for ( name in this.registry ) {
15797 tool = this.registry[ name ];
15798 if (
15799 // Only add tools by group name when auto-add is enabled
15800 tool.static.autoAddToCatchall &&
15801 // Exclude already used tools
15802 ( !used || !used[ name ] )
15803 ) {
15804 names.push( name );
15805 if ( used ) {
15806 used[ name ] = true;
15807 }
15808 }
15809 }
15810 } else if ( Array.isArray( collection ) ) {
15811 for ( i = 0, len = collection.length; i < len; i++ ) {
15812 item = collection[ i ];
15813 // Allow plain strings as shorthand for named tools
15814 if ( typeof item === 'string' ) {
15815 item = { name: item };
15816 }
15817 if ( OO.isPlainObject( item ) ) {
15818 if ( item.group ) {
15819 for ( name in this.registry ) {
15820 tool = this.registry[ name ];
15821 if (
15822 // Include tools with matching group
15823 tool.static.group === item.group &&
15824 // Only add tools by group name when auto-add is enabled
15825 tool.static.autoAddToGroup &&
15826 // Exclude already used tools
15827 ( !used || !used[ name ] )
15828 ) {
15829 names.push( name );
15830 if ( used ) {
15831 used[ name ] = true;
15832 }
15833 }
15834 }
15835 // Include tools with matching name and exclude already used tools
15836 } else if ( item.name && ( !used || !used[ item.name ] ) ) {
15837 names.push( item.name );
15838 if ( used ) {
15839 used[ item.name ] = true;
15840 }
15841 }
15842 }
15843 }
15844 }
15845 return names;
15846 };
15847
15848 /**
15849 * ToolGroupFactories create {@link OO.ui.ToolGroup toolgroups} on demand. The toolgroup classes must
15850 * specify a symbolic name and be registered with the factory. The following classes are registered by
15851 * default:
15852 *
15853 * - {@link OO.ui.BarToolGroup BarToolGroups} (‘bar’)
15854 * - {@link OO.ui.MenuToolGroup MenuToolGroups} (‘menu’)
15855 * - {@link OO.ui.ListToolGroup ListToolGroups} (‘list’)
15856 *
15857 * See {@link OO.ui.Toolbar toolbars} for an example.
15858 *
15859 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
15860 *
15861 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
15862 * @class
15863 * @extends OO.Factory
15864 * @constructor
15865 */
15866 OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() {
15867 var i, l, defaultClasses;
15868 // Parent constructor
15869 OO.Factory.call( this );
15870
15871 defaultClasses = this.constructor.static.getDefaultClasses();
15872
15873 // Register default toolgroups
15874 for ( i = 0, l = defaultClasses.length; i < l; i++ ) {
15875 this.register( defaultClasses[ i ] );
15876 }
15877 };
15878
15879 /* Setup */
15880
15881 OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory );
15882
15883 /* Static Methods */
15884
15885 /**
15886 * Get a default set of classes to be registered on construction.
15887 *
15888 * @return {Function[]} Default classes
15889 */
15890 OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
15891 return [
15892 OO.ui.BarToolGroup,
15893 OO.ui.ListToolGroup,
15894 OO.ui.MenuToolGroup
15895 ];
15896 };
15897
15898 /**
15899 * Popup tools open a popup window when they are selected from the {@link OO.ui.Toolbar toolbar}. Each popup tool is configured
15900 * 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
15901 * an #onSelect or #onUpdateState method, as these methods have been implemented already.
15902 *
15903 * // Example of a popup tool. When selected, a popup tool displays
15904 * // a popup window.
15905 * function HelpTool( toolGroup, config ) {
15906 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
15907 * padded: true,
15908 * label: 'Help',
15909 * head: true
15910 * } }, config ) );
15911 * this.popup.$body.append( '<p>I am helpful!</p>' );
15912 * };
15913 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
15914 * HelpTool.static.name = 'help';
15915 * HelpTool.static.icon = 'help';
15916 * HelpTool.static.title = 'Help';
15917 * toolFactory.register( HelpTool );
15918 *
15919 * For an example of a toolbar that contains a popup tool, see {@link OO.ui.Toolbar toolbars}. For more information about
15920 * toolbars in genreral, please see the [OOjs UI documentation on MediaWiki][1].
15921 *
15922 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
15923 *
15924 * @abstract
15925 * @class
15926 * @extends OO.ui.Tool
15927 * @mixins OO.ui.mixin.PopupElement
15928 *
15929 * @constructor
15930 * @param {OO.ui.ToolGroup} toolGroup
15931 * @param {Object} [config] Configuration options
15932 */
15933 OO.ui.PopupTool = function OoUiPopupTool( toolGroup, config ) {
15934 // Allow passing positional parameters inside the config object
15935 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
15936 config = toolGroup;
15937 toolGroup = config.toolGroup;
15938 }
15939
15940 // Parent constructor
15941 OO.ui.PopupTool.parent.call( this, toolGroup, config );
15942
15943 // Mixin constructors
15944 OO.ui.mixin.PopupElement.call( this, config );
15945
15946 // Initialization
15947 this.$element
15948 .addClass( 'oo-ui-popupTool' )
15949 .append( this.popup.$element );
15950 };
15951
15952 /* Setup */
15953
15954 OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool );
15955 OO.mixinClass( OO.ui.PopupTool, OO.ui.mixin.PopupElement );
15956
15957 /* Methods */
15958
15959 /**
15960 * Handle the tool being selected.
15961 *
15962 * @inheritdoc
15963 */
15964 OO.ui.PopupTool.prototype.onSelect = function () {
15965 if ( !this.isDisabled() ) {
15966 this.popup.toggle();
15967 }
15968 this.setActive( false );
15969 return false;
15970 };
15971
15972 /**
15973 * Handle the toolbar state being updated.
15974 *
15975 * @inheritdoc
15976 */
15977 OO.ui.PopupTool.prototype.onUpdateState = function () {
15978 this.setActive( false );
15979 };
15980
15981 /**
15982 * A ToolGroupTool is a special sort of tool that can contain other {@link OO.ui.Tool tools}
15983 * and {@link OO.ui.ToolGroup toolgroups}. The ToolGroupTool was specifically designed to be used
15984 * inside a {@link OO.ui.BarToolGroup bar} toolgroup to provide access to additional tools from
15985 * the bar item. Included tools will be displayed in a dropdown {@link OO.ui.ListToolGroup list}
15986 * when the ToolGroupTool is selected.
15987 *
15988 * // Example: ToolGroupTool with two nested tools, 'setting1' and 'setting2', defined elsewhere.
15989 *
15990 * function SettingsTool() {
15991 * SettingsTool.parent.apply( this, arguments );
15992 * };
15993 * OO.inheritClass( SettingsTool, OO.ui.ToolGroupTool );
15994 * SettingsTool.static.name = 'settings';
15995 * SettingsTool.static.title = 'Change settings';
15996 * SettingsTool.static.groupConfig = {
15997 * icon: 'settings',
15998 * label: 'ToolGroupTool',
15999 * include: [ 'setting1', 'setting2' ]
16000 * };
16001 * toolFactory.register( SettingsTool );
16002 *
16003 * For more information, please see the [OOjs UI documentation on MediaWiki][1].
16004 *
16005 * Please note that this implementation is subject to change per [T74159] [2].
16006 *
16007 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars#ToolGroupTool
16008 * [2]: https://phabricator.wikimedia.org/T74159
16009 *
16010 * @abstract
16011 * @class
16012 * @extends OO.ui.Tool
16013 *
16014 * @constructor
16015 * @param {OO.ui.ToolGroup} toolGroup
16016 * @param {Object} [config] Configuration options
16017 */
16018 OO.ui.ToolGroupTool = function OoUiToolGroupTool( toolGroup, config ) {
16019 // Allow passing positional parameters inside the config object
16020 if ( OO.isPlainObject( toolGroup ) && config === undefined ) {
16021 config = toolGroup;
16022 toolGroup = config.toolGroup;
16023 }
16024
16025 // Parent constructor
16026 OO.ui.ToolGroupTool.parent.call( this, toolGroup, config );
16027
16028 // Properties
16029 this.innerToolGroup = this.createGroup( this.constructor.static.groupConfig );
16030
16031 // Events
16032 this.innerToolGroup.connect( this, { disable: 'onToolGroupDisable' } );
16033
16034 // Initialization
16035 this.$link.remove();
16036 this.$element
16037 .addClass( 'oo-ui-toolGroupTool' )
16038 .append( this.innerToolGroup.$element );
16039 };
16040
16041 /* Setup */
16042
16043 OO.inheritClass( OO.ui.ToolGroupTool, OO.ui.Tool );
16044
16045 /* Static Properties */
16046
16047 /**
16048 * Toolgroup configuration.
16049 *
16050 * The toolgroup configuration consists of the tools to include, as well as an icon and label
16051 * to use for the bar item. Tools can be included by symbolic name, group, or with the
16052 * wildcard selector. Please see {@link OO.ui.ToolGroup toolgroup} for more information.
16053 *
16054 * @property {Object.<string,Array>}
16055 */
16056 OO.ui.ToolGroupTool.static.groupConfig = {};
16057
16058 /* Methods */
16059
16060 /**
16061 * Handle the tool being selected.
16062 *
16063 * @inheritdoc
16064 */
16065 OO.ui.ToolGroupTool.prototype.onSelect = function () {
16066 this.innerToolGroup.setActive( !this.innerToolGroup.active );
16067 return false;
16068 };
16069
16070 /**
16071 * Synchronize disabledness state of the tool with the inner toolgroup.
16072 *
16073 * @private
16074 * @param {boolean} disabled Element is disabled
16075 */
16076 OO.ui.ToolGroupTool.prototype.onToolGroupDisable = function ( disabled ) {
16077 this.setDisabled( disabled );
16078 };
16079
16080 /**
16081 * Handle the toolbar state being updated.
16082 *
16083 * @inheritdoc
16084 */
16085 OO.ui.ToolGroupTool.prototype.onUpdateState = function () {
16086 this.setActive( false );
16087 };
16088
16089 /**
16090 * Build a {@link OO.ui.ToolGroup toolgroup} from the specified configuration.
16091 *
16092 * @param {Object.<string,Array>} group Toolgroup configuration. Please see {@link OO.ui.ToolGroup toolgroup} for
16093 * more information.
16094 * @return {OO.ui.ListToolGroup}
16095 */
16096 OO.ui.ToolGroupTool.prototype.createGroup = function ( group ) {
16097 if ( group.include === '*' ) {
16098 // Apply defaults to catch-all groups
16099 if ( group.label === undefined ) {
16100 group.label = OO.ui.msg( 'ooui-toolbar-more' );
16101 }
16102 }
16103
16104 return this.toolbar.getToolGroupFactory().create( 'list', this.toolbar, group );
16105 };
16106
16107 /**
16108 * BarToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
16109 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.MenuToolGroup MenuToolGroup}
16110 * and {@link OO.ui.ListToolGroup ListToolGroup}). The {@link OO.ui.Tool tools} in a BarToolGroup are
16111 * displayed by icon in a single row. The title of the tool is displayed when users move the mouse over
16112 * the tool.
16113 *
16114 * BarToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar is
16115 * set up.
16116 *
16117 * @example
16118 * // Example of a BarToolGroup with two tools
16119 * var toolFactory = new OO.ui.ToolFactory();
16120 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
16121 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
16122 *
16123 * // We will be placing status text in this element when tools are used
16124 * var $area = $( '<p>' ).text( 'Example of a BarToolGroup with two tools.' );
16125 *
16126 * // Define the tools that we're going to place in our toolbar
16127 *
16128 * // Create a class inheriting from OO.ui.Tool
16129 * function SearchTool() {
16130 * SearchTool.parent.apply( this, arguments );
16131 * }
16132 * OO.inheritClass( SearchTool, OO.ui.Tool );
16133 * // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
16134 * // of 'icon' and 'title' (displayed icon and text).
16135 * SearchTool.static.name = 'search';
16136 * SearchTool.static.icon = 'search';
16137 * SearchTool.static.title = 'Search...';
16138 * // Defines the action that will happen when this tool is selected (clicked).
16139 * SearchTool.prototype.onSelect = function () {
16140 * $area.text( 'Search tool clicked!' );
16141 * // Never display this tool as "active" (selected).
16142 * this.setActive( false );
16143 * };
16144 * SearchTool.prototype.onUpdateState = function () {};
16145 * // Make this tool available in our toolFactory and thus our toolbar
16146 * toolFactory.register( SearchTool );
16147 *
16148 * // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
16149 * // little popup window (a PopupWidget).
16150 * function HelpTool( toolGroup, config ) {
16151 * OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
16152 * padded: true,
16153 * label: 'Help',
16154 * head: true
16155 * } }, config ) );
16156 * this.popup.$body.append( '<p>I am helpful!</p>' );
16157 * }
16158 * OO.inheritClass( HelpTool, OO.ui.PopupTool );
16159 * HelpTool.static.name = 'help';
16160 * HelpTool.static.icon = 'help';
16161 * HelpTool.static.title = 'Help';
16162 * toolFactory.register( HelpTool );
16163 *
16164 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
16165 * // used once (but not all defined tools must be used).
16166 * toolbar.setup( [
16167 * {
16168 * // 'bar' tool groups display tools by icon only
16169 * type: 'bar',
16170 * include: [ 'search', 'help' ]
16171 * }
16172 * ] );
16173 *
16174 * // Create some UI around the toolbar and place it in the document
16175 * var frame = new OO.ui.PanelLayout( {
16176 * expanded: false,
16177 * framed: true
16178 * } );
16179 * var contentFrame = new OO.ui.PanelLayout( {
16180 * expanded: false,
16181 * padded: true
16182 * } );
16183 * frame.$element.append(
16184 * toolbar.$element,
16185 * contentFrame.$element.append( $area )
16186 * );
16187 * $( 'body' ).append( frame.$element );
16188 *
16189 * // Here is where the toolbar is actually built. This must be done after inserting it into the
16190 * // document.
16191 * toolbar.initialize();
16192 *
16193 * For more information about how to add tools to a bar tool group, please see {@link OO.ui.ToolGroup toolgroup}.
16194 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
16195 *
16196 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
16197 *
16198 * @class
16199 * @extends OO.ui.ToolGroup
16200 *
16201 * @constructor
16202 * @param {OO.ui.Toolbar} toolbar
16203 * @param {Object} [config] Configuration options
16204 */
16205 OO.ui.BarToolGroup = function OoUiBarToolGroup( toolbar, config ) {
16206 // Allow passing positional parameters inside the config object
16207 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
16208 config = toolbar;
16209 toolbar = config.toolbar;
16210 }
16211
16212 // Parent constructor
16213 OO.ui.BarToolGroup.parent.call( this, toolbar, config );
16214
16215 // Initialization
16216 this.$element.addClass( 'oo-ui-barToolGroup' );
16217 };
16218
16219 /* Setup */
16220
16221 OO.inheritClass( OO.ui.BarToolGroup, OO.ui.ToolGroup );
16222
16223 /* Static Properties */
16224
16225 OO.ui.BarToolGroup.static.titleTooltips = true;
16226
16227 OO.ui.BarToolGroup.static.accelTooltips = true;
16228
16229 OO.ui.BarToolGroup.static.name = 'bar';
16230
16231 /**
16232 * PopupToolGroup is an abstract base class used by both {@link OO.ui.MenuToolGroup MenuToolGroup}
16233 * and {@link OO.ui.ListToolGroup ListToolGroup} to provide a popup--an overlaid menu or list of tools with an
16234 * optional icon and label. This class can be used for other base classes that also use this functionality.
16235 *
16236 * @abstract
16237 * @class
16238 * @extends OO.ui.ToolGroup
16239 * @mixins OO.ui.mixin.IconElement
16240 * @mixins OO.ui.mixin.IndicatorElement
16241 * @mixins OO.ui.mixin.LabelElement
16242 * @mixins OO.ui.mixin.TitledElement
16243 * @mixins OO.ui.mixin.ClippableElement
16244 * @mixins OO.ui.mixin.TabIndexedElement
16245 *
16246 * @constructor
16247 * @param {OO.ui.Toolbar} toolbar
16248 * @param {Object} [config] Configuration options
16249 * @cfg {string} [header] Text to display at the top of the popup
16250 */
16251 OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
16252 // Allow passing positional parameters inside the config object
16253 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
16254 config = toolbar;
16255 toolbar = config.toolbar;
16256 }
16257
16258 // Configuration initialization
16259 config = config || {};
16260
16261 // Parent constructor
16262 OO.ui.PopupToolGroup.parent.call( this, toolbar, config );
16263
16264 // Properties
16265 this.active = false;
16266 this.dragging = false;
16267 this.onBlurHandler = this.onBlur.bind( this );
16268 this.$handle = $( '<span>' );
16269
16270 // Mixin constructors
16271 OO.ui.mixin.IconElement.call( this, config );
16272 OO.ui.mixin.IndicatorElement.call( this, config );
16273 OO.ui.mixin.LabelElement.call( this, config );
16274 OO.ui.mixin.TitledElement.call( this, config );
16275 OO.ui.mixin.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
16276 OO.ui.mixin.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
16277
16278 // Events
16279 this.$handle.on( {
16280 keydown: this.onHandleMouseKeyDown.bind( this ),
16281 keyup: this.onHandleMouseKeyUp.bind( this ),
16282 mousedown: this.onHandleMouseKeyDown.bind( this ),
16283 mouseup: this.onHandleMouseKeyUp.bind( this )
16284 } );
16285
16286 // Initialization
16287 this.$handle
16288 .addClass( 'oo-ui-popupToolGroup-handle' )
16289 .append( this.$icon, this.$label, this.$indicator );
16290 // If the pop-up should have a header, add it to the top of the toolGroup.
16291 // Note: If this feature is useful for other widgets, we could abstract it into an
16292 // OO.ui.HeaderedElement mixin constructor.
16293 if ( config.header !== undefined ) {
16294 this.$group
16295 .prepend( $( '<span>' )
16296 .addClass( 'oo-ui-popupToolGroup-header' )
16297 .text( config.header )
16298 );
16299 }
16300 this.$element
16301 .addClass( 'oo-ui-popupToolGroup' )
16302 .prepend( this.$handle );
16303 };
16304
16305 /* Setup */
16306
16307 OO.inheritClass( OO.ui.PopupToolGroup, OO.ui.ToolGroup );
16308 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.IconElement );
16309 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.IndicatorElement );
16310 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.LabelElement );
16311 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.TitledElement );
16312 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.ClippableElement );
16313 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.mixin.TabIndexedElement );
16314
16315 /* Methods */
16316
16317 /**
16318 * @inheritdoc
16319 */
16320 OO.ui.PopupToolGroup.prototype.setDisabled = function () {
16321 // Parent method
16322 OO.ui.PopupToolGroup.parent.prototype.setDisabled.apply( this, arguments );
16323
16324 if ( this.isDisabled() && this.isElementAttached() ) {
16325 this.setActive( false );
16326 }
16327 };
16328
16329 /**
16330 * Handle focus being lost.
16331 *
16332 * The event is actually generated from a mouseup/keyup, so it is not a normal blur event object.
16333 *
16334 * @protected
16335 * @param {jQuery.Event} e Mouse up or key up event
16336 */
16337 OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) {
16338 // Only deactivate when clicking outside the dropdown element
16339 if ( $( e.target ).closest( '.oo-ui-popupToolGroup' )[ 0 ] !== this.$element[ 0 ] ) {
16340 this.setActive( false );
16341 }
16342 };
16343
16344 /**
16345 * @inheritdoc
16346 */
16347 OO.ui.PopupToolGroup.prototype.onMouseKeyUp = function ( e ) {
16348 // Only close toolgroup when a tool was actually selected
16349 if (
16350 !this.isDisabled() && this.pressed && this.pressed === this.getTargetTool( e ) &&
16351 ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
16352 ) {
16353 this.setActive( false );
16354 }
16355 return OO.ui.PopupToolGroup.parent.prototype.onMouseKeyUp.call( this, e );
16356 };
16357
16358 /**
16359 * Handle mouse up and key up events.
16360 *
16361 * @protected
16362 * @param {jQuery.Event} e Mouse up or key up event
16363 */
16364 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyUp = function ( e ) {
16365 if (
16366 !this.isDisabled() &&
16367 ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
16368 ) {
16369 return false;
16370 }
16371 };
16372
16373 /**
16374 * Handle mouse down and key down events.
16375 *
16376 * @protected
16377 * @param {jQuery.Event} e Mouse down or key down event
16378 */
16379 OO.ui.PopupToolGroup.prototype.onHandleMouseKeyDown = function ( e ) {
16380 if (
16381 !this.isDisabled() &&
16382 ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
16383 ) {
16384 this.setActive( !this.active );
16385 return false;
16386 }
16387 };
16388
16389 /**
16390 * Switch into 'active' mode.
16391 *
16392 * When active, the popup is visible. A mouseup event anywhere in the document will trigger
16393 * deactivation.
16394 */
16395 OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
16396 var containerWidth, containerLeft;
16397 value = !!value;
16398 if ( this.active !== value ) {
16399 this.active = value;
16400 if ( value ) {
16401 this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler, true );
16402 this.getElementDocument().addEventListener( 'keyup', this.onBlurHandler, true );
16403
16404 this.$clippable.css( 'left', '' );
16405 // Try anchoring the popup to the left first
16406 this.$element.addClass( 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left' );
16407 this.toggleClipping( true );
16408 if ( this.isClippedHorizontally() ) {
16409 // Anchoring to the left caused the popup to clip, so anchor it to the right instead
16410 this.toggleClipping( false );
16411 this.$element
16412 .removeClass( 'oo-ui-popupToolGroup-left' )
16413 .addClass( 'oo-ui-popupToolGroup-right' );
16414 this.toggleClipping( true );
16415 }
16416 if ( this.isClippedHorizontally() ) {
16417 // Anchoring to the right also caused the popup to clip, so just make it fill the container
16418 containerWidth = this.$clippableScrollableContainer.width();
16419 containerLeft = this.$clippableScrollableContainer.offset().left;
16420
16421 this.toggleClipping( false );
16422 this.$element.removeClass( 'oo-ui-popupToolGroup-right' );
16423
16424 this.$clippable.css( {
16425 left: -( this.$element.offset().left - containerLeft ),
16426 width: containerWidth
16427 } );
16428 }
16429 } else {
16430 this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler, true );
16431 this.getElementDocument().removeEventListener( 'keyup', this.onBlurHandler, true );
16432 this.$element.removeClass(
16433 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left oo-ui-popupToolGroup-right'
16434 );
16435 this.toggleClipping( false );
16436 }
16437 }
16438 };
16439
16440 /**
16441 * ListToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
16442 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.MenuToolGroup MenuToolGroup}
16443 * and {@link OO.ui.BarToolGroup BarToolGroup}). The {@link OO.ui.Tool tools} in a ListToolGroup are displayed
16444 * by label in a dropdown menu. The title of the tool is used as the label text. The menu itself can be configured
16445 * with a label, icon, indicator, header, and title.
16446 *
16447 * ListToolGroups can be configured to be expanded and collapsed. Collapsed lists will have a ‘More’ option that
16448 * users can select to see the full list of tools. If a collapsed toolgroup is expanded, a ‘Fewer’ option permits
16449 * users to collapse the list again.
16450 *
16451 * ListToolGroups are created by a {@link OO.ui.ToolGroupFactory toolgroup factory} when the toolbar is set up. The factory
16452 * requires the ListToolGroup's symbolic name, 'list', which is specified along with the other configurations. For more
16453 * information about how to add tools to a ListToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
16454 *
16455 * @example
16456 * // Example of a ListToolGroup
16457 * var toolFactory = new OO.ui.ToolFactory();
16458 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
16459 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
16460 *
16461 * // Configure and register two tools
16462 * function SettingsTool() {
16463 * SettingsTool.parent.apply( this, arguments );
16464 * }
16465 * OO.inheritClass( SettingsTool, OO.ui.Tool );
16466 * SettingsTool.static.name = 'settings';
16467 * SettingsTool.static.icon = 'settings';
16468 * SettingsTool.static.title = 'Change settings';
16469 * SettingsTool.prototype.onSelect = function () {
16470 * this.setActive( false );
16471 * };
16472 * SettingsTool.prototype.onUpdateState = function () {};
16473 * toolFactory.register( SettingsTool );
16474 * // Register two more tools, nothing interesting here
16475 * function StuffTool() {
16476 * StuffTool.parent.apply( this, arguments );
16477 * }
16478 * OO.inheritClass( StuffTool, OO.ui.Tool );
16479 * StuffTool.static.name = 'stuff';
16480 * StuffTool.static.icon = 'search';
16481 * StuffTool.static.title = 'Change the world';
16482 * StuffTool.prototype.onSelect = function () {
16483 * this.setActive( false );
16484 * };
16485 * StuffTool.prototype.onUpdateState = function () {};
16486 * toolFactory.register( StuffTool );
16487 * toolbar.setup( [
16488 * {
16489 * // Configurations for list toolgroup.
16490 * type: 'list',
16491 * label: 'ListToolGroup',
16492 * indicator: 'down',
16493 * icon: 'ellipsis',
16494 * title: 'This is the title, displayed when user moves the mouse over the list toolgroup',
16495 * header: 'This is the header',
16496 * include: [ 'settings', 'stuff' ],
16497 * allowCollapse: ['stuff']
16498 * }
16499 * ] );
16500 *
16501 * // Create some UI around the toolbar and place it in the document
16502 * var frame = new OO.ui.PanelLayout( {
16503 * expanded: false,
16504 * framed: true
16505 * } );
16506 * frame.$element.append(
16507 * toolbar.$element
16508 * );
16509 * $( 'body' ).append( frame.$element );
16510 * // Build the toolbar. This must be done after the toolbar has been appended to the document.
16511 * toolbar.initialize();
16512 *
16513 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki][1].
16514 *
16515 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
16516 *
16517 * @class
16518 * @extends OO.ui.PopupToolGroup
16519 *
16520 * @constructor
16521 * @param {OO.ui.Toolbar} toolbar
16522 * @param {Object} [config] Configuration options
16523 * @cfg {Array} [allowCollapse] Allow the specified tools to be collapsed. By default, collapsible tools
16524 * will only be displayed if users click the ‘More’ option displayed at the bottom of the list. If
16525 * the list is expanded, a ‘Fewer’ option permits users to collapse the list again. Any tools that
16526 * are included in the toolgroup, but are not designated as collapsible, will always be displayed.
16527 * To open a collapsible list in its expanded state, set #expanded to 'true'.
16528 * @cfg {Array} [forceExpand] Expand the specified tools. All other tools will be designated as collapsible.
16529 * Unless #expanded is set to true, the collapsible tools will be collapsed when the list is first opened.
16530 * @cfg {boolean} [expanded=false] Expand collapsible tools. This config is only relevant if tools have
16531 * been designated as collapsible. When expanded is set to true, all tools in the group will be displayed
16532 * when the list is first opened. Users can collapse the list with a ‘Fewer’ option at the bottom.
16533 */
16534 OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
16535 // Allow passing positional parameters inside the config object
16536 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
16537 config = toolbar;
16538 toolbar = config.toolbar;
16539 }
16540
16541 // Configuration initialization
16542 config = config || {};
16543
16544 // Properties (must be set before parent constructor, which calls #populate)
16545 this.allowCollapse = config.allowCollapse;
16546 this.forceExpand = config.forceExpand;
16547 this.expanded = config.expanded !== undefined ? config.expanded : false;
16548 this.collapsibleTools = [];
16549
16550 // Parent constructor
16551 OO.ui.ListToolGroup.parent.call( this, toolbar, config );
16552
16553 // Initialization
16554 this.$element.addClass( 'oo-ui-listToolGroup' );
16555 };
16556
16557 /* Setup */
16558
16559 OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup );
16560
16561 /* Static Properties */
16562
16563 OO.ui.ListToolGroup.static.name = 'list';
16564
16565 /* Methods */
16566
16567 /**
16568 * @inheritdoc
16569 */
16570 OO.ui.ListToolGroup.prototype.populate = function () {
16571 var i, len, allowCollapse = [];
16572
16573 OO.ui.ListToolGroup.parent.prototype.populate.call( this );
16574
16575 // Update the list of collapsible tools
16576 if ( this.allowCollapse !== undefined ) {
16577 allowCollapse = this.allowCollapse;
16578 } else if ( this.forceExpand !== undefined ) {
16579 allowCollapse = OO.simpleArrayDifference( Object.keys( this.tools ), this.forceExpand );
16580 }
16581
16582 this.collapsibleTools = [];
16583 for ( i = 0, len = allowCollapse.length; i < len; i++ ) {
16584 if ( this.tools[ allowCollapse[ i ] ] !== undefined ) {
16585 this.collapsibleTools.push( this.tools[ allowCollapse[ i ] ] );
16586 }
16587 }
16588
16589 // Keep at the end, even when tools are added
16590 this.$group.append( this.getExpandCollapseTool().$element );
16591
16592 this.getExpandCollapseTool().toggle( this.collapsibleTools.length !== 0 );
16593 this.updateCollapsibleState();
16594 };
16595
16596 OO.ui.ListToolGroup.prototype.getExpandCollapseTool = function () {
16597 var ExpandCollapseTool;
16598 if ( this.expandCollapseTool === undefined ) {
16599 ExpandCollapseTool = function () {
16600 ExpandCollapseTool.parent.apply( this, arguments );
16601 };
16602
16603 OO.inheritClass( ExpandCollapseTool, OO.ui.Tool );
16604
16605 ExpandCollapseTool.prototype.onSelect = function () {
16606 this.toolGroup.expanded = !this.toolGroup.expanded;
16607 this.toolGroup.updateCollapsibleState();
16608 this.setActive( false );
16609 };
16610 ExpandCollapseTool.prototype.onUpdateState = function () {
16611 // Do nothing. Tool interface requires an implementation of this function.
16612 };
16613
16614 ExpandCollapseTool.static.name = 'more-fewer';
16615
16616 this.expandCollapseTool = new ExpandCollapseTool( this );
16617 }
16618 return this.expandCollapseTool;
16619 };
16620
16621 /**
16622 * @inheritdoc
16623 */
16624 OO.ui.ListToolGroup.prototype.onMouseKeyUp = function ( e ) {
16625 // Do not close the popup when the user wants to show more/fewer tools
16626 if (
16627 $( e.target ).closest( '.oo-ui-tool-name-more-fewer' ).length &&
16628 ( e.which === OO.ui.MouseButtons.LEFT || e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER )
16629 ) {
16630 // HACK: Prevent the popup list from being hidden. Skip the PopupToolGroup implementation (which
16631 // hides the popup list when a tool is selected) and call ToolGroup's implementation directly.
16632 return OO.ui.ListToolGroup.parent.parent.prototype.onMouseKeyUp.call( this, e );
16633 } else {
16634 return OO.ui.ListToolGroup.parent.prototype.onMouseKeyUp.call( this, e );
16635 }
16636 };
16637
16638 OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () {
16639 var i, len;
16640
16641 this.getExpandCollapseTool()
16642 .setIcon( this.expanded ? 'collapse' : 'expand' )
16643 .setTitle( OO.ui.msg( this.expanded ? 'ooui-toolgroup-collapse' : 'ooui-toolgroup-expand' ) );
16644
16645 for ( i = 0, len = this.collapsibleTools.length; i < len; i++ ) {
16646 this.collapsibleTools[ i ].toggle( this.expanded );
16647 }
16648 };
16649
16650 /**
16651 * MenuToolGroups are one of three types of {@link OO.ui.ToolGroup toolgroups} that are used to
16652 * create {@link OO.ui.Toolbar toolbars} (the other types of groups are {@link OO.ui.BarToolGroup BarToolGroup}
16653 * and {@link OO.ui.ListToolGroup ListToolGroup}). MenuToolGroups contain selectable {@link OO.ui.Tool tools},
16654 * which are displayed by label in a dropdown menu. The tool's title is used as the label text, and the
16655 * menu label is updated to reflect which tool or tools are currently selected. If no tools are selected,
16656 * the menu label is empty. The menu can be configured with an indicator, icon, title, and/or header.
16657 *
16658 * MenuToolGroups are created by a {@link OO.ui.ToolGroupFactory tool group factory} when the toolbar
16659 * is set up.
16660 *
16661 * @example
16662 * // Example of a MenuToolGroup
16663 * var toolFactory = new OO.ui.ToolFactory();
16664 * var toolGroupFactory = new OO.ui.ToolGroupFactory();
16665 * var toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
16666 *
16667 * // We will be placing status text in this element when tools are used
16668 * var $area = $( '<p>' ).text( 'An example of a MenuToolGroup. Select a tool from the dropdown menu.' );
16669 *
16670 * // Define the tools that we're going to place in our toolbar
16671 *
16672 * function SettingsTool() {
16673 * SettingsTool.parent.apply( this, arguments );
16674 * this.reallyActive = false;
16675 * }
16676 * OO.inheritClass( SettingsTool, OO.ui.Tool );
16677 * SettingsTool.static.name = 'settings';
16678 * SettingsTool.static.icon = 'settings';
16679 * SettingsTool.static.title = 'Change settings';
16680 * SettingsTool.prototype.onSelect = function () {
16681 * $area.text( 'Settings tool clicked!' );
16682 * // Toggle the active state on each click
16683 * this.reallyActive = !this.reallyActive;
16684 * this.setActive( this.reallyActive );
16685 * // To update the menu label
16686 * this.toolbar.emit( 'updateState' );
16687 * };
16688 * SettingsTool.prototype.onUpdateState = function () {};
16689 * toolFactory.register( SettingsTool );
16690 *
16691 * function StuffTool() {
16692 * StuffTool.parent.apply( this, arguments );
16693 * this.reallyActive = false;
16694 * }
16695 * OO.inheritClass( StuffTool, OO.ui.Tool );
16696 * StuffTool.static.name = 'stuff';
16697 * StuffTool.static.icon = 'ellipsis';
16698 * StuffTool.static.title = 'More stuff';
16699 * StuffTool.prototype.onSelect = function () {
16700 * $area.text( 'More stuff tool clicked!' );
16701 * // Toggle the active state on each click
16702 * this.reallyActive = !this.reallyActive;
16703 * this.setActive( this.reallyActive );
16704 * // To update the menu label
16705 * this.toolbar.emit( 'updateState' );
16706 * };
16707 * StuffTool.prototype.onUpdateState = function () {};
16708 * toolFactory.register( StuffTool );
16709 *
16710 * // Finally define which tools and in what order appear in the toolbar. Each tool may only be
16711 * // used once (but not all defined tools must be used).
16712 * toolbar.setup( [
16713 * {
16714 * type: 'menu',
16715 * header: 'This is the (optional) header',
16716 * title: 'This is the (optional) title',
16717 * indicator: 'down',
16718 * include: [ 'settings', 'stuff' ]
16719 * }
16720 * ] );
16721 *
16722 * // Create some UI around the toolbar and place it in the document
16723 * var frame = new OO.ui.PanelLayout( {
16724 * expanded: false,
16725 * framed: true
16726 * } );
16727 * var contentFrame = new OO.ui.PanelLayout( {
16728 * expanded: false,
16729 * padded: true
16730 * } );
16731 * frame.$element.append(
16732 * toolbar.$element,
16733 * contentFrame.$element.append( $area )
16734 * );
16735 * $( 'body' ).append( frame.$element );
16736 *
16737 * // Here is where the toolbar is actually built. This must be done after inserting it into the
16738 * // document.
16739 * toolbar.initialize();
16740 * toolbar.emit( 'updateState' );
16741 *
16742 * For more information about how to add tools to a MenuToolGroup, please see {@link OO.ui.ToolGroup toolgroup}.
16743 * For more information about toolbars in general, please see the [OOjs UI documentation on MediaWiki] [1].
16744 *
16745 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Toolbars
16746 *
16747 * @class
16748 * @extends OO.ui.PopupToolGroup
16749 *
16750 * @constructor
16751 * @param {OO.ui.Toolbar} toolbar
16752 * @param {Object} [config] Configuration options
16753 */
16754 OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
16755 // Allow passing positional parameters inside the config object
16756 if ( OO.isPlainObject( toolbar ) && config === undefined ) {
16757 config = toolbar;
16758 toolbar = config.toolbar;
16759 }
16760
16761 // Configuration initialization
16762 config = config || {};
16763
16764 // Parent constructor
16765 OO.ui.MenuToolGroup.parent.call( this, toolbar, config );
16766
16767 // Events
16768 this.toolbar.connect( this, { updateState: 'onUpdateState' } );
16769
16770 // Initialization
16771 this.$element.addClass( 'oo-ui-menuToolGroup' );
16772 };
16773
16774 /* Setup */
16775
16776 OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup );
16777
16778 /* Static Properties */
16779
16780 OO.ui.MenuToolGroup.static.name = 'menu';
16781
16782 /* Methods */
16783
16784 /**
16785 * Handle the toolbar state being updated.
16786 *
16787 * When the state changes, the title of each active item in the menu will be joined together and
16788 * used as a label for the group. The label will be empty if none of the items are active.
16789 *
16790 * @private
16791 */
16792 OO.ui.MenuToolGroup.prototype.onUpdateState = function () {
16793 var name,
16794 labelTexts = [];
16795
16796 for ( name in this.tools ) {
16797 if ( this.tools[ name ].isActive() ) {
16798 labelTexts.push( this.tools[ name ].getTitle() );
16799 }
16800 }
16801
16802 this.setLabel( labelTexts.join( ', ' ) || ' ' );
16803 };
16804
16805 }( OO ) );
16806
16807 /*!
16808 * OOjs UI v0.15.2
16809 * https://www.mediawiki.org/wiki/OOjs_UI
16810 *
16811 * Copyright 2011–2016 OOjs UI Team and other contributors.
16812 * Released under the MIT license
16813 * http://oojs.mit-license.org
16814 *
16815 * Date: 2016-02-02T22:07:00Z
16816 */
16817 ( function ( OO ) {
16818
16819 'use strict';
16820
16821 /**
16822 * An ActionWidget is a {@link OO.ui.ButtonWidget button widget} that executes an action.
16823 * Action widgets are used with OO.ui.ActionSet, which manages the behavior and availability
16824 * of the actions.
16825 *
16826 * Both actions and action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
16827 * Please see the [OOjs UI documentation on MediaWiki] [1] for more information
16828 * and examples.
16829 *
16830 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
16831 *
16832 * @class
16833 * @extends OO.ui.ButtonWidget
16834 * @mixins OO.ui.mixin.PendingElement
16835 *
16836 * @constructor
16837 * @param {Object} [config] Configuration options
16838 * @cfg {string} [action] Symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
16839 * @cfg {string[]} [modes] Symbolic names of the modes (e.g., ‘edit’ or ‘read’) in which the action
16840 * should be made available. See the action set's {@link OO.ui.ActionSet#setMode setMode} method
16841 * for more information about setting modes.
16842 * @cfg {boolean} [framed=false] Render the action button with a frame
16843 */
16844 OO.ui.ActionWidget = function OoUiActionWidget( config ) {
16845 // Configuration initialization
16846 config = $.extend( { framed: false }, config );
16847
16848 // Parent constructor
16849 OO.ui.ActionWidget.parent.call( this, config );
16850
16851 // Mixin constructors
16852 OO.ui.mixin.PendingElement.call( this, config );
16853
16854 // Properties
16855 this.action = config.action || '';
16856 this.modes = config.modes || [];
16857 this.width = 0;
16858 this.height = 0;
16859
16860 // Initialization
16861 this.$element.addClass( 'oo-ui-actionWidget' );
16862 };
16863
16864 /* Setup */
16865
16866 OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget );
16867 OO.mixinClass( OO.ui.ActionWidget, OO.ui.mixin.PendingElement );
16868
16869 /* Events */
16870
16871 /**
16872 * A resize event is emitted when the size of the widget changes.
16873 *
16874 * @event resize
16875 */
16876
16877 /* Methods */
16878
16879 /**
16880 * Check if the action is configured to be available in the specified `mode`.
16881 *
16882 * @param {string} mode Name of mode
16883 * @return {boolean} The action is configured with the mode
16884 */
16885 OO.ui.ActionWidget.prototype.hasMode = function ( mode ) {
16886 return this.modes.indexOf( mode ) !== -1;
16887 };
16888
16889 /**
16890 * Get the symbolic name of the action (e.g., ‘continue’ or ‘cancel’).
16891 *
16892 * @return {string}
16893 */
16894 OO.ui.ActionWidget.prototype.getAction = function () {
16895 return this.action;
16896 };
16897
16898 /**
16899 * Get the symbolic name of the mode or modes for which the action is configured to be available.
16900 *
16901 * The current mode is set with the action set's {@link OO.ui.ActionSet#setMode setMode} method.
16902 * Only actions that are configured to be avaiable in the current mode will be visible. All other actions
16903 * are hidden.
16904 *
16905 * @return {string[]}
16906 */
16907 OO.ui.ActionWidget.prototype.getModes = function () {
16908 return this.modes.slice();
16909 };
16910
16911 /**
16912 * Emit a resize event if the size has changed.
16913 *
16914 * @private
16915 * @chainable
16916 */
16917 OO.ui.ActionWidget.prototype.propagateResize = function () {
16918 var width, height;
16919
16920 if ( this.isElementAttached() ) {
16921 width = this.$element.width();
16922 height = this.$element.height();
16923
16924 if ( width !== this.width || height !== this.height ) {
16925 this.width = width;
16926 this.height = height;
16927 this.emit( 'resize' );
16928 }
16929 }
16930
16931 return this;
16932 };
16933
16934 /**
16935 * @inheritdoc
16936 */
16937 OO.ui.ActionWidget.prototype.setIcon = function () {
16938 // Mixin method
16939 OO.ui.mixin.IconElement.prototype.setIcon.apply( this, arguments );
16940 this.propagateResize();
16941
16942 return this;
16943 };
16944
16945 /**
16946 * @inheritdoc
16947 */
16948 OO.ui.ActionWidget.prototype.setLabel = function () {
16949 // Mixin method
16950 OO.ui.mixin.LabelElement.prototype.setLabel.apply( this, arguments );
16951 this.propagateResize();
16952
16953 return this;
16954 };
16955
16956 /**
16957 * @inheritdoc
16958 */
16959 OO.ui.ActionWidget.prototype.setFlags = function () {
16960 // Mixin method
16961 OO.ui.mixin.FlaggedElement.prototype.setFlags.apply( this, arguments );
16962 this.propagateResize();
16963
16964 return this;
16965 };
16966
16967 /**
16968 * @inheritdoc
16969 */
16970 OO.ui.ActionWidget.prototype.clearFlags = function () {
16971 // Mixin method
16972 OO.ui.mixin.FlaggedElement.prototype.clearFlags.apply( this, arguments );
16973 this.propagateResize();
16974
16975 return this;
16976 };
16977
16978 /**
16979 * Toggle the visibility of the action button.
16980 *
16981 * @param {boolean} [show] Show button, omit to toggle visibility
16982 * @chainable
16983 */
16984 OO.ui.ActionWidget.prototype.toggle = function () {
16985 // Parent method
16986 OO.ui.ActionWidget.parent.prototype.toggle.apply( this, arguments );
16987 this.propagateResize();
16988
16989 return this;
16990 };
16991
16992 /**
16993 * ActionSets manage the behavior of the {@link OO.ui.ActionWidget action widgets} that comprise them.
16994 * Actions can be made available for specific contexts (modes) and circumstances
16995 * (abilities). Action sets are primarily used with {@link OO.ui.Dialog Dialogs}.
16996 *
16997 * ActionSets contain two types of actions:
16998 *
16999 * - 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.
17000 * - Other: Other actions include all non-special visible actions.
17001 *
17002 * Please see the [OOjs UI documentation on MediaWiki][1] for more information.
17003 *
17004 * @example
17005 * // Example: An action set used in a process dialog
17006 * function MyProcessDialog( config ) {
17007 * MyProcessDialog.parent.call( this, config );
17008 * }
17009 * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
17010 * MyProcessDialog.static.title = 'An action set in a process dialog';
17011 * // An action set that uses modes ('edit' and 'help' mode, in this example).
17012 * MyProcessDialog.static.actions = [
17013 * { action: 'continue', modes: 'edit', label: 'Continue', flags: [ 'primary', 'constructive' ] },
17014 * { action: 'help', modes: 'edit', label: 'Help' },
17015 * { modes: 'edit', label: 'Cancel', flags: 'safe' },
17016 * { action: 'back', modes: 'help', label: 'Back', flags: 'safe' }
17017 * ];
17018 *
17019 * MyProcessDialog.prototype.initialize = function () {
17020 * MyProcessDialog.parent.prototype.initialize.apply( this, arguments );
17021 * this.panel1 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
17022 * 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>' );
17023 * this.panel2 = new OO.ui.PanelLayout( { padded: true, expanded: false } );
17024 * 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>' );
17025 * this.stackLayout = new OO.ui.StackLayout( {
17026 * items: [ this.panel1, this.panel2 ]
17027 * } );
17028 * this.$body.append( this.stackLayout.$element );
17029 * };
17030 * MyProcessDialog.prototype.getSetupProcess = function ( data ) {
17031 * return MyProcessDialog.parent.prototype.getSetupProcess.call( this, data )
17032 * .next( function () {
17033 * this.actions.setMode( 'edit' );
17034 * }, this );
17035 * };
17036 * MyProcessDialog.prototype.getActionProcess = function ( action ) {
17037 * if ( action === 'help' ) {
17038 * this.actions.setMode( 'help' );
17039 * this.stackLayout.setItem( this.panel2 );
17040 * } else if ( action === 'back' ) {
17041 * this.actions.setMode( 'edit' );
17042 * this.stackLayout.setItem( this.panel1 );
17043 * } else if ( action === 'continue' ) {
17044 * var dialog = this;
17045 * return new OO.ui.Process( function () {
17046 * dialog.close();
17047 * } );
17048 * }
17049 * return MyProcessDialog.parent.prototype.getActionProcess.call( this, action );
17050 * };
17051 * MyProcessDialog.prototype.getBodyHeight = function () {
17052 * return this.panel1.$element.outerHeight( true );
17053 * };
17054 * var windowManager = new OO.ui.WindowManager();
17055 * $( 'body' ).append( windowManager.$element );
17056 * var dialog = new MyProcessDialog( {
17057 * size: 'medium'
17058 * } );
17059 * windowManager.addWindows( [ dialog ] );
17060 * windowManager.openWindow( dialog );
17061 *
17062 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
17063 *
17064 * @abstract
17065 * @class
17066 * @mixins OO.EventEmitter
17067 *
17068 * @constructor
17069 * @param {Object} [config] Configuration options
17070 */
17071 OO.ui.ActionSet = function OoUiActionSet( config ) {
17072 // Configuration initialization
17073 config = config || {};
17074
17075 // Mixin constructors
17076 OO.EventEmitter.call( this );
17077
17078 // Properties
17079 this.list = [];
17080 this.categories = {
17081 actions: 'getAction',
17082 flags: 'getFlags',
17083 modes: 'getModes'
17084 };
17085 this.categorized = {};
17086 this.special = {};
17087 this.others = [];
17088 this.organized = false;
17089 this.changing = false;
17090 this.changed = false;
17091 };
17092
17093 /* Setup */
17094
17095 OO.mixinClass( OO.ui.ActionSet, OO.EventEmitter );
17096
17097 /* Static Properties */
17098
17099 /**
17100 * Symbolic name of the flags used to identify special actions. Special actions are displayed in the
17101 * header of a {@link OO.ui.ProcessDialog process dialog}.
17102 * See the [OOjs UI documentation on MediaWiki][2] for more information and examples.
17103 *
17104 * [2]:https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
17105 *
17106 * @abstract
17107 * @static
17108 * @inheritable
17109 * @property {string}
17110 */
17111 OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ];
17112
17113 /* Events */
17114
17115 /**
17116 * @event click
17117 *
17118 * A 'click' event is emitted when an action is clicked.
17119 *
17120 * @param {OO.ui.ActionWidget} action Action that was clicked
17121 */
17122
17123 /**
17124 * @event resize
17125 *
17126 * A 'resize' event is emitted when an action widget is resized.
17127 *
17128 * @param {OO.ui.ActionWidget} action Action that was resized
17129 */
17130
17131 /**
17132 * @event add
17133 *
17134 * An 'add' event is emitted when actions are {@link #method-add added} to the action set.
17135 *
17136 * @param {OO.ui.ActionWidget[]} added Actions added
17137 */
17138
17139 /**
17140 * @event remove
17141 *
17142 * A 'remove' event is emitted when actions are {@link #method-remove removed}
17143 * or {@link #clear cleared}.
17144 *
17145 * @param {OO.ui.ActionWidget[]} added Actions removed
17146 */
17147
17148 /**
17149 * @event change
17150 *
17151 * A 'change' event is emitted when actions are {@link #method-add added}, {@link #clear cleared},
17152 * or {@link #method-remove removed} from the action set or when the {@link #setMode mode} is changed.
17153 *
17154 */
17155
17156 /* Methods */
17157
17158 /**
17159 * Handle action change events.
17160 *
17161 * @private
17162 * @fires change
17163 */
17164 OO.ui.ActionSet.prototype.onActionChange = function () {
17165 this.organized = false;
17166 if ( this.changing ) {
17167 this.changed = true;
17168 } else {
17169 this.emit( 'change' );
17170 }
17171 };
17172
17173 /**
17174 * Check if an action is one of the special actions.
17175 *
17176 * @param {OO.ui.ActionWidget} action Action to check
17177 * @return {boolean} Action is special
17178 */
17179 OO.ui.ActionSet.prototype.isSpecial = function ( action ) {
17180 var flag;
17181
17182 for ( flag in this.special ) {
17183 if ( action === this.special[ flag ] ) {
17184 return true;
17185 }
17186 }
17187
17188 return false;
17189 };
17190
17191 /**
17192 * Get action widgets based on the specified filter: ‘actions’, ‘flags’, ‘modes’, ‘visible’,
17193 * or ‘disabled’.
17194 *
17195 * @param {Object} [filters] Filters to use, omit to get all actions
17196 * @param {string|string[]} [filters.actions] Actions that action widgets must have
17197 * @param {string|string[]} [filters.flags] Flags that action widgets must have (e.g., 'safe')
17198 * @param {string|string[]} [filters.modes] Modes that action widgets must have
17199 * @param {boolean} [filters.visible] Action widgets must be visible
17200 * @param {boolean} [filters.disabled] Action widgets must be disabled
17201 * @return {OO.ui.ActionWidget[]} Action widgets matching all criteria
17202 */
17203 OO.ui.ActionSet.prototype.get = function ( filters ) {
17204 var i, len, list, category, actions, index, match, matches;
17205
17206 if ( filters ) {
17207 this.organize();
17208
17209 // Collect category candidates
17210 matches = [];
17211 for ( category in this.categorized ) {
17212 list = filters[ category ];
17213 if ( list ) {
17214 if ( !Array.isArray( list ) ) {
17215 list = [ list ];
17216 }
17217 for ( i = 0, len = list.length; i < len; i++ ) {
17218 actions = this.categorized[ category ][ list[ i ] ];
17219 if ( Array.isArray( actions ) ) {
17220 matches.push.apply( matches, actions );
17221 }
17222 }
17223 }
17224 }
17225 // Remove by boolean filters
17226 for ( i = 0, len = matches.length; i < len; i++ ) {
17227 match = matches[ i ];
17228 if (
17229 ( filters.visible !== undefined && match.isVisible() !== filters.visible ) ||
17230 ( filters.disabled !== undefined && match.isDisabled() !== filters.disabled )
17231 ) {
17232 matches.splice( i, 1 );
17233 len--;
17234 i--;
17235 }
17236 }
17237 // Remove duplicates
17238 for ( i = 0, len = matches.length; i < len; i++ ) {
17239 match = matches[ i ];
17240 index = matches.lastIndexOf( match );
17241 while ( index !== i ) {
17242 matches.splice( index, 1 );
17243 len--;
17244 index = matches.lastIndexOf( match );
17245 }
17246 }
17247 return matches;
17248 }
17249 return this.list.slice();
17250 };
17251
17252 /**
17253 * Get 'special' actions.
17254 *
17255 * Special actions are the first visible action widgets with special flags, such as 'safe' and 'primary'.
17256 * Special flags can be configured in subclasses by changing the static #specialFlags property.
17257 *
17258 * @return {OO.ui.ActionWidget[]|null} 'Special' action widgets.
17259 */
17260 OO.ui.ActionSet.prototype.getSpecial = function () {
17261 this.organize();
17262 return $.extend( {}, this.special );
17263 };
17264
17265 /**
17266 * Get 'other' actions.
17267 *
17268 * Other actions include all non-special visible action widgets.
17269 *
17270 * @return {OO.ui.ActionWidget[]} 'Other' action widgets
17271 */
17272 OO.ui.ActionSet.prototype.getOthers = function () {
17273 this.organize();
17274 return this.others.slice();
17275 };
17276
17277 /**
17278 * Set the mode (e.g., ‘edit’ or ‘view’). Only {@link OO.ui.ActionWidget#modes actions} configured
17279 * to be available in the specified mode will be made visible. All other actions will be hidden.
17280 *
17281 * @param {string} mode The mode. Only actions configured to be available in the specified
17282 * mode will be made visible.
17283 * @chainable
17284 * @fires toggle
17285 * @fires change
17286 */
17287 OO.ui.ActionSet.prototype.setMode = function ( mode ) {
17288 var i, len, action;
17289
17290 this.changing = true;
17291 for ( i = 0, len = this.list.length; i < len; i++ ) {
17292 action = this.list[ i ];
17293 action.toggle( action.hasMode( mode ) );
17294 }
17295
17296 this.organized = false;
17297 this.changing = false;
17298 this.emit( 'change' );
17299
17300 return this;
17301 };
17302
17303 /**
17304 * Set the abilities of the specified actions.
17305 *
17306 * Action widgets that are configured with the specified actions will be enabled
17307 * or disabled based on the boolean values specified in the `actions`
17308 * parameter.
17309 *
17310 * @param {Object.<string,boolean>} actions A list keyed by action name with boolean
17311 * values that indicate whether or not the action should be enabled.
17312 * @chainable
17313 */
17314 OO.ui.ActionSet.prototype.setAbilities = function ( actions ) {
17315 var i, len, action, item;
17316
17317 for ( i = 0, len = this.list.length; i < len; i++ ) {
17318 item = this.list[ i ];
17319 action = item.getAction();
17320 if ( actions[ action ] !== undefined ) {
17321 item.setDisabled( !actions[ action ] );
17322 }
17323 }
17324
17325 return this;
17326 };
17327
17328 /**
17329 * Executes a function once per action.
17330 *
17331 * When making changes to multiple actions, use this method instead of iterating over the actions
17332 * manually to defer emitting a #change event until after all actions have been changed.
17333 *
17334 * @param {Object|null} actions Filters to use to determine which actions to iterate over; see #get
17335 * @param {Function} callback Callback to run for each action; callback is invoked with three
17336 * arguments: the action, the action's index, the list of actions being iterated over
17337 * @chainable
17338 */
17339 OO.ui.ActionSet.prototype.forEach = function ( filter, callback ) {
17340 this.changed = false;
17341 this.changing = true;
17342 this.get( filter ).forEach( callback );
17343 this.changing = false;
17344 if ( this.changed ) {
17345 this.emit( 'change' );
17346 }
17347
17348 return this;
17349 };
17350
17351 /**
17352 * Add action widgets to the action set.
17353 *
17354 * @param {OO.ui.ActionWidget[]} actions Action widgets to add
17355 * @chainable
17356 * @fires add
17357 * @fires change
17358 */
17359 OO.ui.ActionSet.prototype.add = function ( actions ) {
17360 var i, len, action;
17361
17362 this.changing = true;
17363 for ( i = 0, len = actions.length; i < len; i++ ) {
17364 action = actions[ i ];
17365 action.connect( this, {
17366 click: [ 'emit', 'click', action ],
17367 resize: [ 'emit', 'resize', action ],
17368 toggle: [ 'onActionChange' ]
17369 } );
17370 this.list.push( action );
17371 }
17372 this.organized = false;
17373 this.emit( 'add', actions );
17374 this.changing = false;
17375 this.emit( 'change' );
17376
17377 return this;
17378 };
17379
17380 /**
17381 * Remove action widgets from the set.
17382 *
17383 * To remove all actions, you may wish to use the #clear method instead.
17384 *
17385 * @param {OO.ui.ActionWidget[]} actions Action widgets to remove
17386 * @chainable
17387 * @fires remove
17388 * @fires change
17389 */
17390 OO.ui.ActionSet.prototype.remove = function ( actions ) {
17391 var i, len, index, action;
17392
17393 this.changing = true;
17394 for ( i = 0, len = actions.length; i < len; i++ ) {
17395 action = actions[ i ];
17396 index = this.list.indexOf( action );
17397 if ( index !== -1 ) {
17398 action.disconnect( this );
17399 this.list.splice( index, 1 );
17400 }
17401 }
17402 this.organized = false;
17403 this.emit( 'remove', actions );
17404 this.changing = false;
17405 this.emit( 'change' );
17406
17407 return this;
17408 };
17409
17410 /**
17411 * Remove all action widets from the set.
17412 *
17413 * To remove only specified actions, use the {@link #method-remove remove} method instead.
17414 *
17415 * @chainable
17416 * @fires remove
17417 * @fires change
17418 */
17419 OO.ui.ActionSet.prototype.clear = function () {
17420 var i, len, action,
17421 removed = this.list.slice();
17422
17423 this.changing = true;
17424 for ( i = 0, len = this.list.length; i < len; i++ ) {
17425 action = this.list[ i ];
17426 action.disconnect( this );
17427 }
17428
17429 this.list = [];
17430
17431 this.organized = false;
17432 this.emit( 'remove', removed );
17433 this.changing = false;
17434 this.emit( 'change' );
17435
17436 return this;
17437 };
17438
17439 /**
17440 * Organize actions.
17441 *
17442 * This is called whenever organized information is requested. It will only reorganize the actions
17443 * if something has changed since the last time it ran.
17444 *
17445 * @private
17446 * @chainable
17447 */
17448 OO.ui.ActionSet.prototype.organize = function () {
17449 var i, iLen, j, jLen, flag, action, category, list, item, special,
17450 specialFlags = this.constructor.static.specialFlags;
17451
17452 if ( !this.organized ) {
17453 this.categorized = {};
17454 this.special = {};
17455 this.others = [];
17456 for ( i = 0, iLen = this.list.length; i < iLen; i++ ) {
17457 action = this.list[ i ];
17458 if ( action.isVisible() ) {
17459 // Populate categories
17460 for ( category in this.categories ) {
17461 if ( !this.categorized[ category ] ) {
17462 this.categorized[ category ] = {};
17463 }
17464 list = action[ this.categories[ category ] ]();
17465 if ( !Array.isArray( list ) ) {
17466 list = [ list ];
17467 }
17468 for ( j = 0, jLen = list.length; j < jLen; j++ ) {
17469 item = list[ j ];
17470 if ( !this.categorized[ category ][ item ] ) {
17471 this.categorized[ category ][ item ] = [];
17472 }
17473 this.categorized[ category ][ item ].push( action );
17474 }
17475 }
17476 // Populate special/others
17477 special = false;
17478 for ( j = 0, jLen = specialFlags.length; j < jLen; j++ ) {
17479 flag = specialFlags[ j ];
17480 if ( !this.special[ flag ] && action.hasFlag( flag ) ) {
17481 this.special[ flag ] = action;
17482 special = true;
17483 break;
17484 }
17485 }
17486 if ( !special ) {
17487 this.others.push( action );
17488 }
17489 }
17490 }
17491 this.organized = true;
17492 }
17493
17494 return this;
17495 };
17496
17497 /**
17498 * Errors contain a required message (either a string or jQuery selection) that is used to describe what went wrong
17499 * in a {@link OO.ui.Process process}. The error's #recoverable and #warning configurations are used to customize the
17500 * appearance and functionality of the error interface.
17501 *
17502 * The basic error interface contains a formatted error message as well as two buttons: 'Dismiss' and 'Try again' (i.e., the error
17503 * is 'recoverable' by default). If the error is not recoverable, the 'Try again' button will not be rendered and the widget
17504 * that initiated the failed process will be disabled.
17505 *
17506 * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button, which will try the
17507 * process again.
17508 *
17509 * For an example of error interfaces, please see the [OOjs UI documentation on MediaWiki][1].
17510 *
17511 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Processes_and_errors
17512 *
17513 * @class
17514 *
17515 * @constructor
17516 * @param {string|jQuery} message Description of error
17517 * @param {Object} [config] Configuration options
17518 * @cfg {boolean} [recoverable=true] Error is recoverable.
17519 * By default, errors are recoverable, and users can try the process again.
17520 * @cfg {boolean} [warning=false] Error is a warning.
17521 * If the error is a warning, the error interface will include a
17522 * 'Dismiss' and a 'Continue' button. It is the responsibility of the developer to ensure that the warning
17523 * is not triggered a second time if the user chooses to continue.
17524 */
17525 OO.ui.Error = function OoUiError( message, config ) {
17526 // Allow passing positional parameters inside the config object
17527 if ( OO.isPlainObject( message ) && config === undefined ) {
17528 config = message;
17529 message = config.message;
17530 }
17531
17532 // Configuration initialization
17533 config = config || {};
17534
17535 // Properties
17536 this.message = message instanceof jQuery ? message : String( message );
17537 this.recoverable = config.recoverable === undefined || !!config.recoverable;
17538 this.warning = !!config.warning;
17539 };
17540
17541 /* Setup */
17542
17543 OO.initClass( OO.ui.Error );
17544
17545 /* Methods */
17546
17547 /**
17548 * Check if the error is recoverable.
17549 *
17550 * If the error is recoverable, users are able to try the process again.
17551 *
17552 * @return {boolean} Error is recoverable
17553 */
17554 OO.ui.Error.prototype.isRecoverable = function () {
17555 return this.recoverable;
17556 };
17557
17558 /**
17559 * Check if the error is a warning.
17560 *
17561 * If the error is a warning, the error interface will include a 'Dismiss' and a 'Continue' button.
17562 *
17563 * @return {boolean} Error is warning
17564 */
17565 OO.ui.Error.prototype.isWarning = function () {
17566 return this.warning;
17567 };
17568
17569 /**
17570 * Get error message as DOM nodes.
17571 *
17572 * @return {jQuery} Error message in DOM nodes
17573 */
17574 OO.ui.Error.prototype.getMessage = function () {
17575 return this.message instanceof jQuery ?
17576 this.message.clone() :
17577 $( '<div>' ).text( this.message ).contents();
17578 };
17579
17580 /**
17581 * Get the error message text.
17582 *
17583 * @return {string} Error message
17584 */
17585 OO.ui.Error.prototype.getMessageText = function () {
17586 return this.message instanceof jQuery ? this.message.text() : this.message;
17587 };
17588
17589 /**
17590 * A Process is a list of steps that are called in sequence. The step can be a number, a jQuery promise,
17591 * or a function:
17592 *
17593 * - **number**: the process will wait for the specified number of milliseconds before proceeding.
17594 * - **promise**: the process will continue to the next step when the promise is successfully resolved
17595 * or stop if the promise is rejected.
17596 * - **function**: the process will execute the function. The process will stop if the function returns
17597 * either a boolean `false` or a promise that is rejected; if the function returns a number, the process
17598 * will wait for that number of milliseconds before proceeding.
17599 *
17600 * If the process fails, an {@link OO.ui.Error error} is generated. Depending on how the error is
17601 * configured, users can dismiss the error and try the process again, or not. If a process is stopped,
17602 * its remaining steps will not be performed.
17603 *
17604 * @class
17605 *
17606 * @constructor
17607 * @param {number|jQuery.Promise|Function} step Number of miliseconds to wait before proceeding, promise
17608 * that must be resolved before proceeding, or a function to execute. See #createStep for more information. see #createStep for more information
17609 * @param {Object} [context=null] Execution context of the function. The context is ignored if the step is
17610 * a number or promise.
17611 * @return {Object} Step object, with `callback` and `context` properties
17612 */
17613 OO.ui.Process = function ( step, context ) {
17614 // Properties
17615 this.steps = [];
17616
17617 // Initialization
17618 if ( step !== undefined ) {
17619 this.next( step, context );
17620 }
17621 };
17622
17623 /* Setup */
17624
17625 OO.initClass( OO.ui.Process );
17626
17627 /* Methods */
17628
17629 /**
17630 * Start the process.
17631 *
17632 * @return {jQuery.Promise} Promise that is resolved when all steps have successfully completed.
17633 * If any of the steps return a promise that is rejected or a boolean false, this promise is rejected
17634 * and any remaining steps are not performed.
17635 */
17636 OO.ui.Process.prototype.execute = function () {
17637 var i, len, promise;
17638
17639 /**
17640 * Continue execution.
17641 *
17642 * @ignore
17643 * @param {Array} step A function and the context it should be called in
17644 * @return {Function} Function that continues the process
17645 */
17646 function proceed( step ) {
17647 return function () {
17648 // Execute step in the correct context
17649 var deferred,
17650 result = step.callback.call( step.context );
17651
17652 if ( result === false ) {
17653 // Use rejected promise for boolean false results
17654 return $.Deferred().reject( [] ).promise();
17655 }
17656 if ( typeof result === 'number' ) {
17657 if ( result < 0 ) {
17658 throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
17659 }
17660 // Use a delayed promise for numbers, expecting them to be in milliseconds
17661 deferred = $.Deferred();
17662 setTimeout( deferred.resolve, result );
17663 return deferred.promise();
17664 }
17665 if ( result instanceof OO.ui.Error ) {
17666 // Use rejected promise for error
17667 return $.Deferred().reject( [ result ] ).promise();
17668 }
17669 if ( Array.isArray( result ) && result.length && result[ 0 ] instanceof OO.ui.Error ) {
17670 // Use rejected promise for list of errors
17671 return $.Deferred().reject( result ).promise();
17672 }
17673 // Duck-type the object to see if it can produce a promise
17674 if ( result && $.isFunction( result.promise ) ) {
17675 // Use a promise generated from the result
17676 return result.promise();
17677 }
17678 // Use resolved promise for other results
17679 return $.Deferred().resolve().promise();
17680 };
17681 }
17682
17683 if ( this.steps.length ) {
17684 // Generate a chain reaction of promises
17685 promise = proceed( this.steps[ 0 ] )();
17686 for ( i = 1, len = this.steps.length; i < len; i++ ) {
17687 promise = promise.then( proceed( this.steps[ i ] ) );
17688 }
17689 } else {
17690 promise = $.Deferred().resolve().promise();
17691 }
17692
17693 return promise;
17694 };
17695
17696 /**
17697 * Create a process step.
17698 *
17699 * @private
17700 * @param {number|jQuery.Promise|Function} step
17701 *
17702 * - Number of milliseconds to wait before proceeding
17703 * - Promise that must be resolved before proceeding
17704 * - Function to execute
17705 * - If the function returns a boolean false the process will stop
17706 * - If the function returns a promise, the process will continue to the next
17707 * step when the promise is resolved or stop if the promise is rejected
17708 * - If the function returns a number, the process will wait for that number of
17709 * milliseconds before proceeding
17710 * @param {Object} [context=null] Execution context of the function. The context is
17711 * ignored if the step is a number or promise.
17712 * @return {Object} Step object, with `callback` and `context` properties
17713 */
17714 OO.ui.Process.prototype.createStep = function ( step, context ) {
17715 if ( typeof step === 'number' || $.isFunction( step.promise ) ) {
17716 return {
17717 callback: function () {
17718 return step;
17719 },
17720 context: null
17721 };
17722 }
17723 if ( $.isFunction( step ) ) {
17724 return {
17725 callback: step,
17726 context: context
17727 };
17728 }
17729 throw new Error( 'Cannot create process step: number, promise or function expected' );
17730 };
17731
17732 /**
17733 * Add step to the beginning of the process.
17734 *
17735 * @inheritdoc #createStep
17736 * @return {OO.ui.Process} this
17737 * @chainable
17738 */
17739 OO.ui.Process.prototype.first = function ( step, context ) {
17740 this.steps.unshift( this.createStep( step, context ) );
17741 return this;
17742 };
17743
17744 /**
17745 * Add step to the end of the process.
17746 *
17747 * @inheritdoc #createStep
17748 * @return {OO.ui.Process} this
17749 * @chainable
17750 */
17751 OO.ui.Process.prototype.next = function ( step, context ) {
17752 this.steps.push( this.createStep( step, context ) );
17753 return this;
17754 };
17755
17756 /**
17757 * Window managers are used to open and close {@link OO.ui.Window windows} and control their presentation.
17758 * Managed windows are mutually exclusive. If a new window is opened while a current window is opening
17759 * or is opened, the current window will be closed and any ongoing {@link OO.ui.Process process} will be cancelled. Windows
17760 * themselves are persistent and—rather than being torn down when closed—can be repopulated with the
17761 * pertinent data and reused.
17762 *
17763 * Over the lifecycle of a window, the window manager makes available three promises: `opening`,
17764 * `opened`, and `closing`, which represent the primary stages of the cycle:
17765 *
17766 * **Opening**: the opening stage begins when the window manager’s #openWindow or a window’s
17767 * {@link OO.ui.Window#open open} method is used, and the window manager begins to open the window.
17768 *
17769 * - an `opening` event is emitted with an `opening` promise
17770 * - the #getSetupDelay method is called and the returned value is used to time a pause in execution before
17771 * the window’s {@link OO.ui.Window#getSetupProcess getSetupProcess} method is called on the
17772 * window and its result executed
17773 * - a `setup` progress notification is emitted from the `opening` promise
17774 * - the #getReadyDelay method is called the returned value is used to time a pause in execution before
17775 * the window’s {@link OO.ui.Window#getReadyProcess getReadyProcess} method is called on the
17776 * window and its result executed
17777 * - a `ready` progress notification is emitted from the `opening` promise
17778 * - the `opening` promise is resolved with an `opened` promise
17779 *
17780 * **Opened**: the window is now open.
17781 *
17782 * **Closing**: the closing stage begins when the window manager's #closeWindow or the
17783 * window's {@link OO.ui.Window#close close} methods is used, and the window manager begins
17784 * to close the window.
17785 *
17786 * - the `opened` promise is resolved with `closing` promise and a `closing` event is emitted
17787 * - the #getHoldDelay method is called and the returned value is used to time a pause in execution before
17788 * the window's {@link OO.ui.Window#getHoldProcess getHoldProces} method is called on the
17789 * window and its result executed
17790 * - a `hold` progress notification is emitted from the `closing` promise
17791 * - the #getTeardownDelay() method is called and the returned value is used to time a pause in execution before
17792 * the window's {@link OO.ui.Window#getTeardownProcess getTeardownProcess} method is called on the
17793 * window and its result executed
17794 * - a `teardown` progress notification is emitted from the `closing` promise
17795 * - the `closing` promise is resolved. The window is now closed
17796 *
17797 * See the [OOjs UI documentation on MediaWiki][1] for more information.
17798 *
17799 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
17800 *
17801 * @class
17802 * @extends OO.ui.Element
17803 * @mixins OO.EventEmitter
17804 *
17805 * @constructor
17806 * @param {Object} [config] Configuration options
17807 * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
17808 * Note that window classes that are instantiated with a factory must have
17809 * a {@link OO.ui.Dialog#static-name static name} property that specifies a symbolic name.
17810 * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
17811 */
17812 OO.ui.WindowManager = function OoUiWindowManager( config ) {
17813 // Configuration initialization
17814 config = config || {};
17815
17816 // Parent constructor
17817 OO.ui.WindowManager.parent.call( this, config );
17818
17819 // Mixin constructors
17820 OO.EventEmitter.call( this );
17821
17822 // Properties
17823 this.factory = config.factory;
17824 this.modal = config.modal === undefined || !!config.modal;
17825 this.windows = {};
17826 this.opening = null;
17827 this.opened = null;
17828 this.closing = null;
17829 this.preparingToOpen = null;
17830 this.preparingToClose = null;
17831 this.currentWindow = null;
17832 this.globalEvents = false;
17833 this.$ariaHidden = null;
17834 this.onWindowResizeTimeout = null;
17835 this.onWindowResizeHandler = this.onWindowResize.bind( this );
17836 this.afterWindowResizeHandler = this.afterWindowResize.bind( this );
17837
17838 // Initialization
17839 this.$element
17840 .addClass( 'oo-ui-windowManager' )
17841 .toggleClass( 'oo-ui-windowManager-modal', this.modal );
17842 };
17843
17844 /* Setup */
17845
17846 OO.inheritClass( OO.ui.WindowManager, OO.ui.Element );
17847 OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter );
17848
17849 /* Events */
17850
17851 /**
17852 * An 'opening' event is emitted when the window begins to be opened.
17853 *
17854 * @event opening
17855 * @param {OO.ui.Window} win Window that's being opened
17856 * @param {jQuery.Promise} opening An `opening` promise resolved with a value when the window is opened successfully.
17857 * When the `opening` promise is resolved, the first argument of the value is an 'opened' promise, the second argument
17858 * is the opening data. The `opening` promise emits `setup` and `ready` notifications when those processes are complete.
17859 * @param {Object} data Window opening data
17860 */
17861
17862 /**
17863 * A 'closing' event is emitted when the window begins to be closed.
17864 *
17865 * @event closing
17866 * @param {OO.ui.Window} win Window that's being closed
17867 * @param {jQuery.Promise} closing A `closing` promise is resolved with a value when the window
17868 * is closed successfully. The promise emits `hold` and `teardown` notifications when those
17869 * processes are complete. When the `closing` promise is resolved, the first argument of its value
17870 * is the closing data.
17871 * @param {Object} data Window closing data
17872 */
17873
17874 /**
17875 * A 'resize' event is emitted when a window is resized.
17876 *
17877 * @event resize
17878 * @param {OO.ui.Window} win Window that was resized
17879 */
17880
17881 /* Static Properties */
17882
17883 /**
17884 * Map of the symbolic name of each window size and its CSS properties.
17885 *
17886 * @static
17887 * @inheritable
17888 * @property {Object}
17889 */
17890 OO.ui.WindowManager.static.sizes = {
17891 small: {
17892 width: 300
17893 },
17894 medium: {
17895 width: 500
17896 },
17897 large: {
17898 width: 700
17899 },
17900 larger: {
17901 width: 900
17902 },
17903 full: {
17904 // These can be non-numeric because they are never used in calculations
17905 width: '100%',
17906 height: '100%'
17907 }
17908 };
17909
17910 /**
17911 * Symbolic name of the default window size.
17912 *
17913 * The default size is used if the window's requested size is not recognized.
17914 *
17915 * @static
17916 * @inheritable
17917 * @property {string}
17918 */
17919 OO.ui.WindowManager.static.defaultSize = 'medium';
17920
17921 /* Methods */
17922
17923 /**
17924 * Handle window resize events.
17925 *
17926 * @private
17927 * @param {jQuery.Event} e Window resize event
17928 */
17929 OO.ui.WindowManager.prototype.onWindowResize = function () {
17930 clearTimeout( this.onWindowResizeTimeout );
17931 this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 );
17932 };
17933
17934 /**
17935 * Handle window resize events.
17936 *
17937 * @private
17938 * @param {jQuery.Event} e Window resize event
17939 */
17940 OO.ui.WindowManager.prototype.afterWindowResize = function () {
17941 if ( this.currentWindow ) {
17942 this.updateWindowSize( this.currentWindow );
17943 }
17944 };
17945
17946 /**
17947 * Check if window is opening.
17948 *
17949 * @return {boolean} Window is opening
17950 */
17951 OO.ui.WindowManager.prototype.isOpening = function ( win ) {
17952 return win === this.currentWindow && !!this.opening && this.opening.state() === 'pending';
17953 };
17954
17955 /**
17956 * Check if window is closing.
17957 *
17958 * @return {boolean} Window is closing
17959 */
17960 OO.ui.WindowManager.prototype.isClosing = function ( win ) {
17961 return win === this.currentWindow && !!this.closing && this.closing.state() === 'pending';
17962 };
17963
17964 /**
17965 * Check if window is opened.
17966 *
17967 * @return {boolean} Window is opened
17968 */
17969 OO.ui.WindowManager.prototype.isOpened = function ( win ) {
17970 return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending';
17971 };
17972
17973 /**
17974 * Check if a window is being managed.
17975 *
17976 * @param {OO.ui.Window} win Window to check
17977 * @return {boolean} Window is being managed
17978 */
17979 OO.ui.WindowManager.prototype.hasWindow = function ( win ) {
17980 var name;
17981
17982 for ( name in this.windows ) {
17983 if ( this.windows[ name ] === win ) {
17984 return true;
17985 }
17986 }
17987
17988 return false;
17989 };
17990
17991 /**
17992 * Get the number of milliseconds to wait after opening begins before executing the ‘setup’ process.
17993 *
17994 * @param {OO.ui.Window} win Window being opened
17995 * @param {Object} [data] Window opening data
17996 * @return {number} Milliseconds to wait
17997 */
17998 OO.ui.WindowManager.prototype.getSetupDelay = function () {
17999 return 0;
18000 };
18001
18002 /**
18003 * Get the number of milliseconds to wait after setup has finished before executing the ‘ready’ process.
18004 *
18005 * @param {OO.ui.Window} win Window being opened
18006 * @param {Object} [data] Window opening data
18007 * @return {number} Milliseconds to wait
18008 */
18009 OO.ui.WindowManager.prototype.getReadyDelay = function () {
18010 return 0;
18011 };
18012
18013 /**
18014 * Get the number of milliseconds to wait after closing has begun before executing the 'hold' process.
18015 *
18016 * @param {OO.ui.Window} win Window being closed
18017 * @param {Object} [data] Window closing data
18018 * @return {number} Milliseconds to wait
18019 */
18020 OO.ui.WindowManager.prototype.getHoldDelay = function () {
18021 return 0;
18022 };
18023
18024 /**
18025 * Get the number of milliseconds to wait after the ‘hold’ process has finished before
18026 * executing the ‘teardown’ process.
18027 *
18028 * @param {OO.ui.Window} win Window being closed
18029 * @param {Object} [data] Window closing data
18030 * @return {number} Milliseconds to wait
18031 */
18032 OO.ui.WindowManager.prototype.getTeardownDelay = function () {
18033 return this.modal ? 250 : 0;
18034 };
18035
18036 /**
18037 * Get a window by its symbolic name.
18038 *
18039 * If the window is not yet instantiated and its symbolic name is recognized by a factory, it will be
18040 * instantiated and added to the window manager automatically. Please see the [OOjs UI documentation on MediaWiki][3]
18041 * for more information about using factories.
18042 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
18043 *
18044 * @param {string} name Symbolic name of the window
18045 * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
18046 * @throws {Error} An error is thrown if the symbolic name is not recognized by the factory.
18047 * @throws {Error} An error is thrown if the named window is not recognized as a managed window.
18048 */
18049 OO.ui.WindowManager.prototype.getWindow = function ( name ) {
18050 var deferred = $.Deferred(),
18051 win = this.windows[ name ];
18052
18053 if ( !( win instanceof OO.ui.Window ) ) {
18054 if ( this.factory ) {
18055 if ( !this.factory.lookup( name ) ) {
18056 deferred.reject( new OO.ui.Error(
18057 'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
18058 ) );
18059 } else {
18060 win = this.factory.create( name );
18061 this.addWindows( [ win ] );
18062 deferred.resolve( win );
18063 }
18064 } else {
18065 deferred.reject( new OO.ui.Error(
18066 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
18067 ) );
18068 }
18069 } else {
18070 deferred.resolve( win );
18071 }
18072
18073 return deferred.promise();
18074 };
18075
18076 /**
18077 * Get current window.
18078 *
18079 * @return {OO.ui.Window|null} Currently opening/opened/closing window
18080 */
18081 OO.ui.WindowManager.prototype.getCurrentWindow = function () {
18082 return this.currentWindow;
18083 };
18084
18085 /**
18086 * Open a window.
18087 *
18088 * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
18089 * @param {Object} [data] Window opening data
18090 * @return {jQuery.Promise} An `opening` promise resolved when the window is done opening.
18091 * See {@link #event-opening 'opening' event} for more information about `opening` promises.
18092 * @fires opening
18093 */
18094 OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
18095 var manager = this,
18096 opening = $.Deferred();
18097
18098 // Argument handling
18099 if ( typeof win === 'string' ) {
18100 return this.getWindow( win ).then( function ( win ) {
18101 return manager.openWindow( win, data );
18102 } );
18103 }
18104
18105 // Error handling
18106 if ( !this.hasWindow( win ) ) {
18107 opening.reject( new OO.ui.Error(
18108 'Cannot open window: window is not attached to manager'
18109 ) );
18110 } else if ( this.preparingToOpen || this.opening || this.opened ) {
18111 opening.reject( new OO.ui.Error(
18112 'Cannot open window: another window is opening or open'
18113 ) );
18114 }
18115
18116 // Window opening
18117 if ( opening.state() !== 'rejected' ) {
18118 // If a window is currently closing, wait for it to complete
18119 this.preparingToOpen = $.when( this.closing );
18120 // Ensure handlers get called after preparingToOpen is set
18121 this.preparingToOpen.done( function () {
18122 if ( manager.modal ) {
18123 manager.toggleGlobalEvents( true );
18124 manager.toggleAriaIsolation( true );
18125 }
18126 manager.currentWindow = win;
18127 manager.opening = opening;
18128 manager.preparingToOpen = null;
18129 manager.emit( 'opening', win, opening, data );
18130 setTimeout( function () {
18131 win.setup( data ).then( function () {
18132 manager.updateWindowSize( win );
18133 manager.opening.notify( { state: 'setup' } );
18134 setTimeout( function () {
18135 win.ready( data ).then( function () {
18136 manager.opening.notify( { state: 'ready' } );
18137 manager.opening = null;
18138 manager.opened = $.Deferred();
18139 opening.resolve( manager.opened.promise(), data );
18140 }, function () {
18141 manager.opening = null;
18142 manager.opened = $.Deferred();
18143 opening.reject();
18144 manager.closeWindow( win );
18145 } );
18146 }, manager.getReadyDelay() );
18147 }, function () {
18148 manager.opening = null;
18149 manager.opened = $.Deferred();
18150 opening.reject();
18151 manager.closeWindow( win );
18152 } );
18153 }, manager.getSetupDelay() );
18154 } );
18155 }
18156
18157 return opening.promise();
18158 };
18159
18160 /**
18161 * Close a window.
18162 *
18163 * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
18164 * @param {Object} [data] Window closing data
18165 * @return {jQuery.Promise} A `closing` promise resolved when the window is done closing.
18166 * See {@link #event-closing 'closing' event} for more information about closing promises.
18167 * @throws {Error} An error is thrown if the window is not managed by the window manager.
18168 * @fires closing
18169 */
18170 OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
18171 var manager = this,
18172 closing = $.Deferred(),
18173 opened;
18174
18175 // Argument handling
18176 if ( typeof win === 'string' ) {
18177 win = this.windows[ win ];
18178 } else if ( !this.hasWindow( win ) ) {
18179 win = null;
18180 }
18181
18182 // Error handling
18183 if ( !win ) {
18184 closing.reject( new OO.ui.Error(
18185 'Cannot close window: window is not attached to manager'
18186 ) );
18187 } else if ( win !== this.currentWindow ) {
18188 closing.reject( new OO.ui.Error(
18189 'Cannot close window: window already closed with different data'
18190 ) );
18191 } else if ( this.preparingToClose || this.closing ) {
18192 closing.reject( new OO.ui.Error(
18193 'Cannot close window: window already closing with different data'
18194 ) );
18195 }
18196
18197 // Window closing
18198 if ( closing.state() !== 'rejected' ) {
18199 // If the window is currently opening, close it when it's done
18200 this.preparingToClose = $.when( this.opening );
18201 // Ensure handlers get called after preparingToClose is set
18202 this.preparingToClose.always( function () {
18203 manager.closing = closing;
18204 manager.preparingToClose = null;
18205 manager.emit( 'closing', win, closing, data );
18206 opened = manager.opened;
18207 manager.opened = null;
18208 opened.resolve( closing.promise(), data );
18209 setTimeout( function () {
18210 win.hold( data ).then( function () {
18211 closing.notify( { state: 'hold' } );
18212 setTimeout( function () {
18213 win.teardown( data ).then( function () {
18214 closing.notify( { state: 'teardown' } );
18215 if ( manager.modal ) {
18216 manager.toggleGlobalEvents( false );
18217 manager.toggleAriaIsolation( false );
18218 }
18219 manager.closing = null;
18220 manager.currentWindow = null;
18221 closing.resolve( data );
18222 } );
18223 }, manager.getTeardownDelay() );
18224 } );
18225 }, manager.getHoldDelay() );
18226 } );
18227 }
18228
18229 return closing.promise();
18230 };
18231
18232 /**
18233 * Add windows to the window manager.
18234 *
18235 * Windows can be added by reference, symbolic name, or explicitly defined symbolic names.
18236 * See the [OOjs ui documentation on MediaWiki] [2] for examples.
18237 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
18238 *
18239 * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows An array of window objects specified
18240 * by reference, symbolic name, or explicitly defined symbolic names.
18241 * @throws {Error} An error is thrown if a window is added by symbolic name, but has neither an
18242 * explicit nor a statically configured symbolic name.
18243 */
18244 OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
18245 var i, len, win, name, list;
18246
18247 if ( Array.isArray( windows ) ) {
18248 // Convert to map of windows by looking up symbolic names from static configuration
18249 list = {};
18250 for ( i = 0, len = windows.length; i < len; i++ ) {
18251 name = windows[ i ].constructor.static.name;
18252 if ( typeof name !== 'string' ) {
18253 throw new Error( 'Cannot add window' );
18254 }
18255 list[ name ] = windows[ i ];
18256 }
18257 } else if ( OO.isPlainObject( windows ) ) {
18258 list = windows;
18259 }
18260
18261 // Add windows
18262 for ( name in list ) {
18263 win = list[ name ];
18264 this.windows[ name ] = win.toggle( false );
18265 this.$element.append( win.$element );
18266 win.setManager( this );
18267 }
18268 };
18269
18270 /**
18271 * Remove the specified windows from the windows manager.
18272 *
18273 * Windows will be closed before they are removed. If you wish to remove all windows, you may wish to use
18274 * the #clearWindows method instead. If you no longer need the window manager and want to ensure that it no
18275 * longer listens to events, use the #destroy method.
18276 *
18277 * @param {string[]} names Symbolic names of windows to remove
18278 * @return {jQuery.Promise} Promise resolved when window is closed and removed
18279 * @throws {Error} An error is thrown if the named windows are not managed by the window manager.
18280 */
18281 OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
18282 var i, len, win, name, cleanupWindow,
18283 manager = this,
18284 promises = [],
18285 cleanup = function ( name, win ) {
18286 delete manager.windows[ name ];
18287 win.$element.detach();
18288 };
18289
18290 for ( i = 0, len = names.length; i < len; i++ ) {
18291 name = names[ i ];
18292 win = this.windows[ name ];
18293 if ( !win ) {
18294 throw new Error( 'Cannot remove window' );
18295 }
18296 cleanupWindow = cleanup.bind( null, name, win );
18297 promises.push( this.closeWindow( name ).then( cleanupWindow, cleanupWindow ) );
18298 }
18299
18300 return $.when.apply( $, promises );
18301 };
18302
18303 /**
18304 * Remove all windows from the window manager.
18305 *
18306 * Windows will be closed before they are removed. Note that the window manager, though not in use, will still
18307 * listen to events. If the window manager will not be used again, you may wish to use the #destroy method instead.
18308 * To remove just a subset of windows, use the #removeWindows method.
18309 *
18310 * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
18311 */
18312 OO.ui.WindowManager.prototype.clearWindows = function () {
18313 return this.removeWindows( Object.keys( this.windows ) );
18314 };
18315
18316 /**
18317 * Set dialog size. In general, this method should not be called directly.
18318 *
18319 * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
18320 *
18321 * @chainable
18322 */
18323 OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) {
18324 var isFullscreen;
18325
18326 // Bypass for non-current, and thus invisible, windows
18327 if ( win !== this.currentWindow ) {
18328 return;
18329 }
18330
18331 isFullscreen = win.getSize() === 'full';
18332
18333 this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', isFullscreen );
18334 this.$element.toggleClass( 'oo-ui-windowManager-floating', !isFullscreen );
18335 win.setDimensions( win.getSizeProperties() );
18336
18337 this.emit( 'resize', win );
18338
18339 return this;
18340 };
18341
18342 /**
18343 * Bind or unbind global events for scrolling.
18344 *
18345 * @private
18346 * @param {boolean} [on] Bind global events
18347 * @chainable
18348 */
18349 OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) {
18350 var scrollWidth, bodyMargin,
18351 $body = $( this.getElementDocument().body ),
18352 // We could have multiple window managers open so only modify
18353 // the body css at the bottom of the stack
18354 stackDepth = $body.data( 'windowManagerGlobalEvents' ) || 0 ;
18355
18356 on = on === undefined ? !!this.globalEvents : !!on;
18357
18358 if ( on ) {
18359 if ( !this.globalEvents ) {
18360 $( this.getElementWindow() ).on( {
18361 // Start listening for top-level window dimension changes
18362 'orientationchange resize': this.onWindowResizeHandler
18363 } );
18364 if ( stackDepth === 0 ) {
18365 scrollWidth = window.innerWidth - document.documentElement.clientWidth;
18366 bodyMargin = parseFloat( $body.css( 'margin-right' ) ) || 0;
18367 $body.css( {
18368 overflow: 'hidden',
18369 'margin-right': bodyMargin + scrollWidth
18370 } );
18371 }
18372 stackDepth++;
18373 this.globalEvents = true;
18374 }
18375 } else if ( this.globalEvents ) {
18376 $( this.getElementWindow() ).off( {
18377 // Stop listening for top-level window dimension changes
18378 'orientationchange resize': this.onWindowResizeHandler
18379 } );
18380 stackDepth--;
18381 if ( stackDepth === 0 ) {
18382 $body.css( {
18383 overflow: '',
18384 'margin-right': ''
18385 } );
18386 }
18387 this.globalEvents = false;
18388 }
18389 $body.data( 'windowManagerGlobalEvents', stackDepth );
18390
18391 return this;
18392 };
18393
18394 /**
18395 * Toggle screen reader visibility of content other than the window manager.
18396 *
18397 * @private
18398 * @param {boolean} [isolate] Make only the window manager visible to screen readers
18399 * @chainable
18400 */
18401 OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) {
18402 isolate = isolate === undefined ? !this.$ariaHidden : !!isolate;
18403
18404 if ( isolate ) {
18405 if ( !this.$ariaHidden ) {
18406 // Hide everything other than the window manager from screen readers
18407 this.$ariaHidden = $( 'body' )
18408 .children()
18409 .not( this.$element.parentsUntil( 'body' ).last() )
18410 .attr( 'aria-hidden', '' );
18411 }
18412 } else if ( this.$ariaHidden ) {
18413 // Restore screen reader visibility
18414 this.$ariaHidden.removeAttr( 'aria-hidden' );
18415 this.$ariaHidden = null;
18416 }
18417
18418 return this;
18419 };
18420
18421 /**
18422 * Destroy the window manager.
18423 *
18424 * Destroying the window manager ensures that it will no longer listen to events. If you would like to
18425 * continue using the window manager, but wish to remove all windows from it, use the #clearWindows method
18426 * instead.
18427 */
18428 OO.ui.WindowManager.prototype.destroy = function () {
18429 this.toggleGlobalEvents( false );
18430 this.toggleAriaIsolation( false );
18431 this.clearWindows();
18432 this.$element.remove();
18433 };
18434
18435 /**
18436 * A window is a container for elements that are in a child frame. They are used with
18437 * a window manager (OO.ui.WindowManager), which is used to open and close the window and control
18438 * its presentation. The size of a window is specified using a symbolic name (e.g., ‘small’, ‘medium’,
18439 * ‘large’), which is interpreted by the window manager. If the requested size is not recognized,
18440 * the window manager will choose a sensible fallback.
18441 *
18442 * The lifecycle of a window has three primary stages (opening, opened, and closing) in which
18443 * different processes are executed:
18444 *
18445 * **opening**: The opening stage begins when the window manager's {@link OO.ui.WindowManager#openWindow
18446 * openWindow} or the window's {@link #open open} methods are used, and the window manager begins to open
18447 * the window.
18448 *
18449 * - {@link #getSetupProcess} method is called and its result executed
18450 * - {@link #getReadyProcess} method is called and its result executed
18451 *
18452 * **opened**: The window is now open
18453 *
18454 * **closing**: The closing stage begins when the window manager's
18455 * {@link OO.ui.WindowManager#closeWindow closeWindow}
18456 * or the window's {@link #close} methods are used, and the window manager begins to close the window.
18457 *
18458 * - {@link #getHoldProcess} method is called and its result executed
18459 * - {@link #getTeardownProcess} method is called and its result executed. The window is now closed
18460 *
18461 * Each of the window's processes (setup, ready, hold, and teardown) can be extended in subclasses
18462 * by overriding the window's #getSetupProcess, #getReadyProcess, #getHoldProcess and #getTeardownProcess
18463 * methods. Note that each {@link OO.ui.Process process} is executed in series, so asynchronous
18464 * processing can complete. Always assume window processes are executed asynchronously.
18465 *
18466 * For more information, please see the [OOjs UI documentation on MediaWiki] [1].
18467 *
18468 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows
18469 *
18470 * @abstract
18471 * @class
18472 * @extends OO.ui.Element
18473 * @mixins OO.EventEmitter
18474 *
18475 * @constructor
18476 * @param {Object} [config] Configuration options
18477 * @cfg {string} [size] Symbolic name of the dialog size: `small`, `medium`, `large`, `larger` or
18478 * `full`. If omitted, the value of the {@link #static-size static size} property will be used.
18479 */
18480 OO.ui.Window = function OoUiWindow( config ) {
18481 // Configuration initialization
18482 config = config || {};
18483
18484 // Parent constructor
18485 OO.ui.Window.parent.call( this, config );
18486
18487 // Mixin constructors
18488 OO.EventEmitter.call( this );
18489
18490 // Properties
18491 this.manager = null;
18492 this.size = config.size || this.constructor.static.size;
18493 this.$frame = $( '<div>' );
18494 this.$overlay = $( '<div>' );
18495 this.$content = $( '<div>' );
18496
18497 this.$focusTrapBefore = $( '<div>' ).prop( 'tabIndex', 0 );
18498 this.$focusTrapAfter = $( '<div>' ).prop( 'tabIndex', 0 );
18499 this.$focusTraps = this.$focusTrapBefore.add( this.$focusTrapAfter );
18500
18501 // Initialization
18502 this.$overlay.addClass( 'oo-ui-window-overlay' );
18503 this.$content
18504 .addClass( 'oo-ui-window-content' )
18505 .attr( 'tabindex', 0 );
18506 this.$frame
18507 .addClass( 'oo-ui-window-frame' )
18508 .append( this.$focusTrapBefore, this.$content, this.$focusTrapAfter );
18509
18510 this.$element
18511 .addClass( 'oo-ui-window' )
18512 .append( this.$frame, this.$overlay );
18513
18514 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
18515 // that reference properties not initialized at that time of parent class construction
18516 // TODO: Find a better way to handle post-constructor setup
18517 this.visible = false;
18518 this.$element.addClass( 'oo-ui-element-hidden' );
18519 };
18520
18521 /* Setup */
18522
18523 OO.inheritClass( OO.ui.Window, OO.ui.Element );
18524 OO.mixinClass( OO.ui.Window, OO.EventEmitter );
18525
18526 /* Static Properties */
18527
18528 /**
18529 * Symbolic name of the window size: `small`, `medium`, `large`, `larger` or `full`.
18530 *
18531 * The static size is used if no #size is configured during construction.
18532 *
18533 * @static
18534 * @inheritable
18535 * @property {string}
18536 */
18537 OO.ui.Window.static.size = 'medium';
18538
18539 /* Methods */
18540
18541 /**
18542 * Handle mouse down events.
18543 *
18544 * @private
18545 * @param {jQuery.Event} e Mouse down event
18546 */
18547 OO.ui.Window.prototype.onMouseDown = function ( e ) {
18548 // Prevent clicking on the click-block from stealing focus
18549 if ( e.target === this.$element[ 0 ] ) {
18550 return false;
18551 }
18552 };
18553
18554 /**
18555 * Check if the window has been initialized.
18556 *
18557 * Initialization occurs when a window is added to a manager.
18558 *
18559 * @return {boolean} Window has been initialized
18560 */
18561 OO.ui.Window.prototype.isInitialized = function () {
18562 return !!this.manager;
18563 };
18564
18565 /**
18566 * Check if the window is visible.
18567 *
18568 * @return {boolean} Window is visible
18569 */
18570 OO.ui.Window.prototype.isVisible = function () {
18571 return this.visible;
18572 };
18573
18574 /**
18575 * Check if the window is opening.
18576 *
18577 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpening isOpening}
18578 * method.
18579 *
18580 * @return {boolean} Window is opening
18581 */
18582 OO.ui.Window.prototype.isOpening = function () {
18583 return this.manager.isOpening( this );
18584 };
18585
18586 /**
18587 * Check if the window is closing.
18588 *
18589 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isClosing isClosing} method.
18590 *
18591 * @return {boolean} Window is closing
18592 */
18593 OO.ui.Window.prototype.isClosing = function () {
18594 return this.manager.isClosing( this );
18595 };
18596
18597 /**
18598 * Check if the window is opened.
18599 *
18600 * This method is a wrapper around the window manager's {@link OO.ui.WindowManager#isOpened isOpened} method.
18601 *
18602 * @return {boolean} Window is opened
18603 */
18604 OO.ui.Window.prototype.isOpened = function () {
18605 return this.manager.isOpened( this );
18606 };
18607
18608 /**
18609 * Get the window manager.
18610 *
18611 * All windows must be attached to a window manager, which is used to open
18612 * and close the window and control its presentation.
18613 *
18614 * @return {OO.ui.WindowManager} Manager of window
18615 */
18616 OO.ui.Window.prototype.getManager = function () {
18617 return this.manager;
18618 };
18619
18620 /**
18621 * Get the symbolic name of the window size (e.g., `small` or `medium`).
18622 *
18623 * @return {string} Symbolic name of the size: `small`, `medium`, `large`, `larger`, `full`
18624 */
18625 OO.ui.Window.prototype.getSize = function () {
18626 var viewport = OO.ui.Element.static.getDimensions( this.getElementWindow() ),
18627 sizes = this.manager.constructor.static.sizes,
18628 size = this.size;
18629
18630 if ( !sizes[ size ] ) {
18631 size = this.manager.constructor.static.defaultSize;
18632 }
18633 if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[ size ].width ) {
18634 size = 'full';
18635 }
18636
18637 return size;
18638 };
18639
18640 /**
18641 * Get the size properties associated with the current window size
18642 *
18643 * @return {Object} Size properties
18644 */
18645 OO.ui.Window.prototype.getSizeProperties = function () {
18646 return this.manager.constructor.static.sizes[ this.getSize() ];
18647 };
18648
18649 /**
18650 * Disable transitions on window's frame for the duration of the callback function, then enable them
18651 * back.
18652 *
18653 * @private
18654 * @param {Function} callback Function to call while transitions are disabled
18655 */
18656 OO.ui.Window.prototype.withoutSizeTransitions = function ( callback ) {
18657 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
18658 // Disable transitions first, otherwise we'll get values from when the window was animating.
18659 var oldTransition,
18660 styleObj = this.$frame[ 0 ].style;
18661 oldTransition = styleObj.transition || styleObj.OTransition || styleObj.MsTransition ||
18662 styleObj.MozTransition || styleObj.WebkitTransition;
18663 styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
18664 styleObj.MozTransition = styleObj.WebkitTransition = 'none';
18665 callback();
18666 // Force reflow to make sure the style changes done inside callback really are not transitioned
18667 this.$frame.height();
18668 styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
18669 styleObj.MozTransition = styleObj.WebkitTransition = oldTransition;
18670 };
18671
18672 /**
18673 * Get the height of the full window contents (i.e., the window head, body and foot together).
18674 *
18675 * What consistitutes the head, body, and foot varies depending on the window type.
18676 * A {@link OO.ui.MessageDialog message dialog} displays a title and message in its body,
18677 * and any actions in the foot. A {@link OO.ui.ProcessDialog process dialog} displays a title
18678 * and special actions in the head, and dialog content in the body.
18679 *
18680 * To get just the height of the dialog body, use the #getBodyHeight method.
18681 *
18682 * @return {number} The height of the window contents (the dialog head, body and foot) in pixels
18683 */
18684 OO.ui.Window.prototype.getContentHeight = function () {
18685 var bodyHeight,
18686 win = this,
18687 bodyStyleObj = this.$body[ 0 ].style,
18688 frameStyleObj = this.$frame[ 0 ].style;
18689
18690 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
18691 // Disable transitions first, otherwise we'll get values from when the window was animating.
18692 this.withoutSizeTransitions( function () {
18693 var oldHeight = frameStyleObj.height,
18694 oldPosition = bodyStyleObj.position;
18695 frameStyleObj.height = '1px';
18696 // Force body to resize to new width
18697 bodyStyleObj.position = 'relative';
18698 bodyHeight = win.getBodyHeight();
18699 frameStyleObj.height = oldHeight;
18700 bodyStyleObj.position = oldPosition;
18701 } );
18702
18703 return (
18704 // Add buffer for border
18705 ( this.$frame.outerHeight() - this.$frame.innerHeight() ) +
18706 // Use combined heights of children
18707 ( this.$head.outerHeight( true ) + bodyHeight + this.$foot.outerHeight( true ) )
18708 );
18709 };
18710
18711 /**
18712 * Get the height of the window body.
18713 *
18714 * To get the height of the full window contents (the window body, head, and foot together),
18715 * use #getContentHeight.
18716 *
18717 * When this function is called, the window will temporarily have been resized
18718 * to height=1px, so .scrollHeight measurements can be taken accurately.
18719 *
18720 * @return {number} Height of the window body in pixels
18721 */
18722 OO.ui.Window.prototype.getBodyHeight = function () {
18723 return this.$body[ 0 ].scrollHeight;
18724 };
18725
18726 /**
18727 * Get the directionality of the frame (right-to-left or left-to-right).
18728 *
18729 * @return {string} Directionality: `'ltr'` or `'rtl'`
18730 */
18731 OO.ui.Window.prototype.getDir = function () {
18732 return OO.ui.Element.static.getDir( this.$content ) || 'ltr';
18733 };
18734
18735 /**
18736 * Get the 'setup' process.
18737 *
18738 * The setup process is used to set up a window for use in a particular context,
18739 * based on the `data` argument. This method is called during the opening phase of the window’s
18740 * lifecycle.
18741 *
18742 * Override this method to add additional steps to the ‘setup’ process the parent method provides
18743 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
18744 * of OO.ui.Process.
18745 *
18746 * To add window content that persists between openings, you may wish to use the #initialize method
18747 * instead.
18748 *
18749 * @param {Object} [data] Window opening data
18750 * @return {OO.ui.Process} Setup process
18751 */
18752 OO.ui.Window.prototype.getSetupProcess = function () {
18753 return new OO.ui.Process();
18754 };
18755
18756 /**
18757 * Get the ‘ready’ process.
18758 *
18759 * The ready process is used to ready a window for use in a particular
18760 * context, based on the `data` argument. This method is called during the opening phase of
18761 * the window’s lifecycle, after the window has been {@link #getSetupProcess setup}.
18762 *
18763 * Override this method to add additional steps to the ‘ready’ process the parent method
18764 * provides using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next}
18765 * methods of OO.ui.Process.
18766 *
18767 * @param {Object} [data] Window opening data
18768 * @return {OO.ui.Process} Ready process
18769 */
18770 OO.ui.Window.prototype.getReadyProcess = function () {
18771 return new OO.ui.Process();
18772 };
18773
18774 /**
18775 * Get the 'hold' process.
18776 *
18777 * The hold proccess is used to keep a window from being used in a particular context,
18778 * based on the `data` argument. This method is called during the closing phase of the window’s
18779 * lifecycle.
18780 *
18781 * Override this method to add additional steps to the 'hold' process the parent method provides
18782 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
18783 * of OO.ui.Process.
18784 *
18785 * @param {Object} [data] Window closing data
18786 * @return {OO.ui.Process} Hold process
18787 */
18788 OO.ui.Window.prototype.getHoldProcess = function () {
18789 return new OO.ui.Process();
18790 };
18791
18792 /**
18793 * Get the ‘teardown’ process.
18794 *
18795 * The teardown process is used to teardown a window after use. During teardown,
18796 * user interactions within the window are conveyed and the window is closed, based on the `data`
18797 * argument. This method is called during the closing phase of the window’s lifecycle.
18798 *
18799 * Override this method to add additional steps to the ‘teardown’ process the parent method provides
18800 * using the {@link OO.ui.Process#first first} and {@link OO.ui.Process#next next} methods
18801 * of OO.ui.Process.
18802 *
18803 * @param {Object} [data] Window closing data
18804 * @return {OO.ui.Process} Teardown process
18805 */
18806 OO.ui.Window.prototype.getTeardownProcess = function () {
18807 return new OO.ui.Process();
18808 };
18809
18810 /**
18811 * Set the window manager.
18812 *
18813 * This will cause the window to initialize. Calling it more than once will cause an error.
18814 *
18815 * @param {OO.ui.WindowManager} manager Manager for this window
18816 * @throws {Error} An error is thrown if the method is called more than once
18817 * @chainable
18818 */
18819 OO.ui.Window.prototype.setManager = function ( manager ) {
18820 if ( this.manager ) {
18821 throw new Error( 'Cannot set window manager, window already has a manager' );
18822 }
18823
18824 this.manager = manager;
18825 this.initialize();
18826
18827 return this;
18828 };
18829
18830 /**
18831 * Set the window size by symbolic name (e.g., 'small' or 'medium')
18832 *
18833 * @param {string} size Symbolic name of size: `small`, `medium`, `large`, `larger` or
18834 * `full`
18835 * @chainable
18836 */
18837 OO.ui.Window.prototype.setSize = function ( size ) {
18838 this.size = size;
18839 this.updateSize();
18840 return this;
18841 };
18842
18843 /**
18844 * Update the window size.
18845 *
18846 * @throws {Error} An error is thrown if the window is not attached to a window manager
18847 * @chainable
18848 */
18849 OO.ui.Window.prototype.updateSize = function () {
18850 if ( !this.manager ) {
18851 throw new Error( 'Cannot update window size, must be attached to a manager' );
18852 }
18853
18854 this.manager.updateWindowSize( this );
18855
18856 return this;
18857 };
18858
18859 /**
18860 * Set window dimensions. This method is called by the {@link OO.ui.WindowManager window manager}
18861 * when the window is opening. In general, setDimensions should not be called directly.
18862 *
18863 * To set the size of the window, use the #setSize method.
18864 *
18865 * @param {Object} dim CSS dimension properties
18866 * @param {string|number} [dim.width] Width
18867 * @param {string|number} [dim.minWidth] Minimum width
18868 * @param {string|number} [dim.maxWidth] Maximum width
18869 * @param {string|number} [dim.width] Height, omit to set based on height of contents
18870 * @param {string|number} [dim.minWidth] Minimum height
18871 * @param {string|number} [dim.maxWidth] Maximum height
18872 * @chainable
18873 */
18874 OO.ui.Window.prototype.setDimensions = function ( dim ) {
18875 var height,
18876 win = this,
18877 styleObj = this.$frame[ 0 ].style;
18878
18879 // Calculate the height we need to set using the correct width
18880 if ( dim.height === undefined ) {
18881 this.withoutSizeTransitions( function () {
18882 var oldWidth = styleObj.width;
18883 win.$frame.css( 'width', dim.width || '' );
18884 height = win.getContentHeight();
18885 styleObj.width = oldWidth;
18886 } );
18887 } else {
18888 height = dim.height;
18889 }
18890
18891 this.$frame.css( {
18892 width: dim.width || '',
18893 minWidth: dim.minWidth || '',
18894 maxWidth: dim.maxWidth || '',
18895 height: height || '',
18896 minHeight: dim.minHeight || '',
18897 maxHeight: dim.maxHeight || ''
18898 } );
18899
18900 return this;
18901 };
18902
18903 /**
18904 * Initialize window contents.
18905 *
18906 * Before the window is opened for the first time, #initialize is called so that content that
18907 * persists between openings can be added to the window.
18908 *
18909 * To set up a window with new content each time the window opens, use #getSetupProcess.
18910 *
18911 * @throws {Error} An error is thrown if the window is not attached to a window manager
18912 * @chainable
18913 */
18914 OO.ui.Window.prototype.initialize = function () {
18915 if ( !this.manager ) {
18916 throw new Error( 'Cannot initialize window, must be attached to a manager' );
18917 }
18918
18919 // Properties
18920 this.$head = $( '<div>' );
18921 this.$body = $( '<div>' );
18922 this.$foot = $( '<div>' );
18923 this.$document = $( this.getElementDocument() );
18924
18925 // Events
18926 this.$element.on( 'mousedown', this.onMouseDown.bind( this ) );
18927
18928 // Initialization
18929 this.$head.addClass( 'oo-ui-window-head' );
18930 this.$body.addClass( 'oo-ui-window-body' );
18931 this.$foot.addClass( 'oo-ui-window-foot' );
18932 this.$content.append( this.$head, this.$body, this.$foot );
18933
18934 return this;
18935 };
18936
18937 /**
18938 * Called when someone tries to focus the hidden element at the end of the dialog.
18939 * Sends focus back to the start of the dialog.
18940 *
18941 * @param {jQuery.Event} event Focus event
18942 */
18943 OO.ui.Window.prototype.onFocusTrapFocused = function ( event ) {
18944 if ( this.$focusTrapBefore.is( event.target ) ) {
18945 OO.ui.findFocusable( this.$content, true ).focus();
18946 } else {
18947 // this.$content is the part of the focus cycle, and is the first focusable element
18948 this.$content.focus();
18949 }
18950 };
18951
18952 /**
18953 * Open the window.
18954 *
18955 * This method is a wrapper around a call to the window manager’s {@link OO.ui.WindowManager#openWindow openWindow}
18956 * method, which returns a promise resolved when the window is done opening.
18957 *
18958 * To customize the window each time it opens, use #getSetupProcess or #getReadyProcess.
18959 *
18960 * @param {Object} [data] Window opening data
18961 * @return {jQuery.Promise} Promise resolved with a value when the window is opened, or rejected
18962 * if the window fails to open. When the promise is resolved successfully, the first argument of the
18963 * value is a new promise, which is resolved when the window begins closing.
18964 * @throws {Error} An error is thrown if the window is not attached to a window manager
18965 */
18966 OO.ui.Window.prototype.open = function ( data ) {
18967 if ( !this.manager ) {
18968 throw new Error( 'Cannot open window, must be attached to a manager' );
18969 }
18970
18971 return this.manager.openWindow( this, data );
18972 };
18973
18974 /**
18975 * Close the window.
18976 *
18977 * This method is a wrapper around a call to the window
18978 * manager’s {@link OO.ui.WindowManager#closeWindow closeWindow} method,
18979 * which returns a closing promise resolved when the window is done closing.
18980 *
18981 * The window's #getHoldProcess and #getTeardownProcess methods are called during the closing
18982 * phase of the window’s lifecycle and can be used to specify closing behavior each time
18983 * the window closes.
18984 *
18985 * @param {Object} [data] Window closing data
18986 * @return {jQuery.Promise} Promise resolved when window is closed
18987 * @throws {Error} An error is thrown if the window is not attached to a window manager
18988 */
18989 OO.ui.Window.prototype.close = function ( data ) {
18990 if ( !this.manager ) {
18991 throw new Error( 'Cannot close window, must be attached to a manager' );
18992 }
18993
18994 return this.manager.closeWindow( this, data );
18995 };
18996
18997 /**
18998 * Setup window.
18999 *
19000 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
19001 * by other systems.
19002 *
19003 * @param {Object} [data] Window opening data
19004 * @return {jQuery.Promise} Promise resolved when window is setup
19005 */
19006 OO.ui.Window.prototype.setup = function ( data ) {
19007 var win = this;
19008
19009 this.toggle( true );
19010
19011 this.focusTrapHandler = OO.ui.bind( this.onFocusTrapFocused, this );
19012 this.$focusTraps.on( 'focus', this.focusTrapHandler );
19013
19014 return this.getSetupProcess( data ).execute().then( function () {
19015 // Force redraw by asking the browser to measure the elements' widths
19016 win.$element.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
19017 win.$content.addClass( 'oo-ui-window-content-setup' ).width();
19018 } );
19019 };
19020
19021 /**
19022 * Ready window.
19023 *
19024 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
19025 * by other systems.
19026 *
19027 * @param {Object} [data] Window opening data
19028 * @return {jQuery.Promise} Promise resolved when window is ready
19029 */
19030 OO.ui.Window.prototype.ready = function ( data ) {
19031 var win = this;
19032
19033 this.$content.focus();
19034 return this.getReadyProcess( data ).execute().then( function () {
19035 // Force redraw by asking the browser to measure the elements' widths
19036 win.$element.addClass( 'oo-ui-window-ready' ).width();
19037 win.$content.addClass( 'oo-ui-window-content-ready' ).width();
19038 } );
19039 };
19040
19041 /**
19042 * Hold window.
19043 *
19044 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
19045 * by other systems.
19046 *
19047 * @param {Object} [data] Window closing data
19048 * @return {jQuery.Promise} Promise resolved when window is held
19049 */
19050 OO.ui.Window.prototype.hold = function ( data ) {
19051 var win = this;
19052
19053 return this.getHoldProcess( data ).execute().then( function () {
19054 // Get the focused element within the window's content
19055 var $focus = win.$content.find( OO.ui.Element.static.getDocument( win.$content ).activeElement );
19056
19057 // Blur the focused element
19058 if ( $focus.length ) {
19059 $focus[ 0 ].blur();
19060 }
19061
19062 // Force redraw by asking the browser to measure the elements' widths
19063 win.$element.removeClass( 'oo-ui-window-ready' ).width();
19064 win.$content.removeClass( 'oo-ui-window-content-ready' ).width();
19065 } );
19066 };
19067
19068 /**
19069 * Teardown window.
19070 *
19071 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
19072 * by other systems.
19073 *
19074 * @param {Object} [data] Window closing data
19075 * @return {jQuery.Promise} Promise resolved when window is torn down
19076 */
19077 OO.ui.Window.prototype.teardown = function ( data ) {
19078 var win = this;
19079
19080 return this.getTeardownProcess( data ).execute().then( function () {
19081 // Force redraw by asking the browser to measure the elements' widths
19082 win.$element.removeClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
19083 win.$content.removeClass( 'oo-ui-window-content-setup' ).width();
19084 win.$focusTraps.off( 'focus', win.focusTrapHandler );
19085 win.toggle( false );
19086 } );
19087 };
19088
19089 /**
19090 * The Dialog class serves as the base class for the other types of dialogs.
19091 * Unless extended to include controls, the rendered dialog box is a simple window
19092 * that users can close by hitting the ‘Esc’ key. Dialog windows are used with OO.ui.WindowManager,
19093 * which opens, closes, and controls the presentation of the window. See the
19094 * [OOjs UI documentation on MediaWiki] [1] for more information.
19095 *
19096 * @example
19097 * // A simple dialog window.
19098 * function MyDialog( config ) {
19099 * MyDialog.parent.call( this, config );
19100 * }
19101 * OO.inheritClass( MyDialog, OO.ui.Dialog );
19102 * MyDialog.prototype.initialize = function () {
19103 * MyDialog.parent.prototype.initialize.call( this );
19104 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
19105 * this.content.$element.append( '<p>A simple dialog window. Press \'Esc\' to close.</p>' );
19106 * this.$body.append( this.content.$element );
19107 * };
19108 * MyDialog.prototype.getBodyHeight = function () {
19109 * return this.content.$element.outerHeight( true );
19110 * };
19111 * var myDialog = new MyDialog( {
19112 * size: 'medium'
19113 * } );
19114 * // Create and append a window manager, which opens and closes the window.
19115 * var windowManager = new OO.ui.WindowManager();
19116 * $( 'body' ).append( windowManager.$element );
19117 * windowManager.addWindows( [ myDialog ] );
19118 * // Open the window!
19119 * windowManager.openWindow( myDialog );
19120 *
19121 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Dialogs
19122 *
19123 * @abstract
19124 * @class
19125 * @extends OO.ui.Window
19126 * @mixins OO.ui.mixin.PendingElement
19127 *
19128 * @constructor
19129 * @param {Object} [config] Configuration options
19130 */
19131 OO.ui.Dialog = function OoUiDialog( config ) {
19132 // Parent constructor
19133 OO.ui.Dialog.parent.call( this, config );
19134
19135 // Mixin constructors
19136 OO.ui.mixin.PendingElement.call( this );
19137
19138 // Properties
19139 this.actions = new OO.ui.ActionSet();
19140 this.attachedActions = [];
19141 this.currentAction = null;
19142 this.onDialogKeyDownHandler = this.onDialogKeyDown.bind( this );
19143
19144 // Events
19145 this.actions.connect( this, {
19146 click: 'onActionClick',
19147 resize: 'onActionResize',
19148 change: 'onActionsChange'
19149 } );
19150
19151 // Initialization
19152 this.$element
19153 .addClass( 'oo-ui-dialog' )
19154 .attr( 'role', 'dialog' );
19155 };
19156
19157 /* Setup */
19158
19159 OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
19160 OO.mixinClass( OO.ui.Dialog, OO.ui.mixin.PendingElement );
19161
19162 /* Static Properties */
19163
19164 /**
19165 * Symbolic name of dialog.
19166 *
19167 * The dialog class must have a symbolic name in order to be registered with OO.Factory.
19168 * Please see the [OOjs UI documentation on MediaWiki] [3] for more information.
19169 *
19170 * [3]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Window_managers
19171 *
19172 * @abstract
19173 * @static
19174 * @inheritable
19175 * @property {string}
19176 */
19177 OO.ui.Dialog.static.name = '';
19178
19179 /**
19180 * The dialog title.
19181 *
19182 * The title can be specified as a plaintext string, a {@link OO.ui.mixin.LabelElement Label} node, or a function
19183 * that will produce a Label node or string. The title can also be specified with data passed to the
19184 * constructor (see #getSetupProcess). In this case, the static value will be overridden.
19185 *
19186 * @abstract
19187 * @static
19188 * @inheritable
19189 * @property {jQuery|string|Function}
19190 */
19191 OO.ui.Dialog.static.title = '';
19192
19193 /**
19194 * An array of configured {@link OO.ui.ActionWidget action widgets}.
19195 *
19196 * Actions can also be specified with data passed to the constructor (see #getSetupProcess). In this case, the static
19197 * value will be overridden.
19198 *
19199 * [2]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs#Action_sets
19200 *
19201 * @static
19202 * @inheritable
19203 * @property {Object[]}
19204 */
19205 OO.ui.Dialog.static.actions = [];
19206
19207 /**
19208 * Close the dialog when the 'Esc' key is pressed.
19209 *
19210 * @static
19211 * @abstract
19212 * @inheritable
19213 * @property {boolean}
19214 */
19215 OO.ui.Dialog.static.escapable = true;
19216
19217 /* Methods */
19218
19219 /**
19220 * Handle frame document key down events.
19221 *
19222 * @private
19223 * @param {jQuery.Event} e Key down event
19224 */
19225 OO.ui.Dialog.prototype.onDialogKeyDown = function ( e ) {
19226 if ( e.which === OO.ui.Keys.ESCAPE ) {
19227 this.executeAction( '' );
19228 e.preventDefault();
19229 e.stopPropagation();
19230 }
19231 };
19232
19233 /**
19234 * Handle action resized events.
19235 *
19236 * @private
19237 * @param {OO.ui.ActionWidget} action Action that was resized
19238 */
19239 OO.ui.Dialog.prototype.onActionResize = function () {
19240 // Override in subclass
19241 };
19242
19243 /**
19244 * Handle action click events.
19245 *
19246 * @private
19247 * @param {OO.ui.ActionWidget} action Action that was clicked
19248 */
19249 OO.ui.Dialog.prototype.onActionClick = function ( action ) {
19250 if ( !this.isPending() ) {
19251 this.executeAction( action.getAction() );
19252 }
19253 };
19254
19255 /**
19256 * Handle actions change event.
19257 *
19258 * @private
19259 */
19260 OO.ui.Dialog.prototype.onActionsChange = function () {
19261 this.detachActions();
19262 if ( !this.isClosing() ) {
19263 this.attachActions();
19264 }
19265 };
19266
19267 /**
19268 * Get the set of actions used by the dialog.
19269 *
19270 * @return {OO.ui.ActionSet}
19271 */
19272 OO.ui.Dialog.prototype.getActions = function () {
19273 return this.actions;
19274 };
19275
19276 /**
19277 * Get a process for taking action.
19278 *
19279 * When you override this method, you can create a new OO.ui.Process and return it, or add additional
19280 * accept steps to the process the parent method provides using the {@link OO.ui.Process#first 'first'}
19281 * and {@link OO.ui.Process#next 'next'} methods of OO.ui.Process.
19282 *
19283 * @param {string} [action] Symbolic name of action
19284 * @return {OO.ui.Process} Action process
19285 */
19286 OO.ui.Dialog.prototype.getActionProcess = function ( action ) {
19287 return new OO.ui.Process()
19288 .next( function () {
19289 if ( !action ) {
19290 // An empty action always closes the dialog without data, which should always be
19291 // safe and make no changes
19292 this.close();
19293 }
19294 }, this );
19295 };
19296
19297 /**
19298 * @inheritdoc
19299 *
19300 * @param {Object} [data] Dialog opening data
19301 * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use
19302 * the {@link #static-title static title}
19303 * @param {Object[]} [data.actions] List of configuration options for each
19304 * {@link OO.ui.ActionWidget action widget}, omit to use {@link #static-actions static actions}.
19305 */
19306 OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
19307 data = data || {};
19308
19309 // Parent method
19310 return OO.ui.Dialog.parent.prototype.getSetupProcess.call( this, data )
19311 .next( function () {
19312 var config = this.constructor.static,
19313 actions = data.actions !== undefined ? data.actions : config.actions;
19314
19315 this.title.setLabel(
19316 data.title !== undefined ? data.title : this.constructor.static.title
19317 );
19318 this.actions.add( this.getActionWidgets( actions ) );
19319
19320 if ( this.constructor.static.escapable ) {
19321 this.$element.on( 'keydown', this.onDialogKeyDownHandler );
19322 }
19323 }, this );
19324 };
19325
19326 /**
19327 * @inheritdoc
19328 */
19329 OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
19330 // Parent method
19331 return OO.ui.Dialog.parent.prototype.getTeardownProcess.call( this, data )
19332 .first( function () {
19333 if ( this.constructor.static.escapable ) {
19334 this.$element.off( 'keydown', this.onDialogKeyDownHandler );
19335 }
19336
19337 this.actions.clear();
19338 this.currentAction = null;
19339 }, this );
19340 };
19341
19342 /**
19343 * @inheritdoc
19344 */
19345 OO.ui.Dialog.prototype.initialize = function () {
19346 var titleId;
19347
19348 // Parent method
19349 OO.ui.Dialog.parent.prototype.initialize.call( this );
19350
19351 titleId = OO.ui.generateElementId();
19352
19353 // Properties
19354 this.title = new OO.ui.LabelWidget( {
19355 id: titleId
19356 } );
19357
19358 // Initialization
19359 this.$content.addClass( 'oo-ui-dialog-content' );
19360 this.$element.attr( 'aria-labelledby', titleId );
19361 this.setPendingElement( this.$head );
19362 };
19363
19364 /**
19365 * Get action widgets from a list of configs
19366 *
19367 * @param {Object[]} actions Action widget configs
19368 * @return {OO.ui.ActionWidget[]} Action widgets
19369 */
19370 OO.ui.Dialog.prototype.getActionWidgets = function ( actions ) {
19371 var i, len, widgets = [];
19372 for ( i = 0, len = actions.length; i < len; i++ ) {
19373 widgets.push(
19374 new OO.ui.ActionWidget( actions[ i ] )
19375 );
19376 }
19377 return widgets;
19378 };
19379
19380 /**
19381 * Attach action actions.
19382 *
19383 * @protected
19384 */
19385 OO.ui.Dialog.prototype.attachActions = function () {
19386 // Remember the list of potentially attached actions
19387 this.attachedActions = this.actions.get();
19388 };
19389
19390 /**
19391 * Detach action actions.
19392 *
19393 * @protected
19394 * @chainable
19395 */
19396 OO.ui.Dialog.prototype.detachActions = function () {
19397 var i, len;
19398
19399 // Detach all actions that may have been previously attached
19400 for ( i = 0, len = this.attachedActions.length; i < len; i++ ) {
19401 this.attachedActions[ i ].$element.detach();
19402 }
19403 this.attachedActions = [];
19404 };
19405
19406 /**
19407 * Execute an action.
19408 *
19409 * @param {string} action Symbolic name of action to execute
19410 * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
19411 */
19412 OO.ui.Dialog.prototype.executeAction = function ( action ) {
19413 this.pushPending();
19414 this.currentAction = action;
19415 return this.getActionProcess( action ).execute()
19416 .always( this.popPending.bind( this ) );
19417 };
19418
19419 /**
19420 * MessageDialogs display a confirmation or alert message. By default, the rendered dialog box
19421 * consists of a header that contains the dialog title, a body with the message, and a footer that
19422 * contains any {@link OO.ui.ActionWidget action widgets}. The MessageDialog class is the only type
19423 * of {@link OO.ui.Dialog dialog} that is usually instantiated directly.
19424 *
19425 * There are two basic types of message dialogs, confirmation and alert:
19426 *
19427 * - **confirmation**: the dialog title describes what a progressive action will do and the message provides
19428 * more details about the consequences.
19429 * - **alert**: the dialog title describes which event occurred and the message provides more information
19430 * about why the event occurred.
19431 *
19432 * The MessageDialog class specifies two actions: ‘accept’, the primary
19433 * action (e.g., ‘ok’) and ‘reject,’ the safe action (e.g., ‘cancel’). Both will close the window,
19434 * passing along the selected action.
19435 *
19436 * For more information and examples, please see the [OOjs UI documentation on MediaWiki][1].
19437 *
19438 * @example
19439 * // Example: Creating and opening a message dialog window.
19440 * var messageDialog = new OO.ui.MessageDialog();
19441 *
19442 * // Create and append a window manager.
19443 * var windowManager = new OO.ui.WindowManager();
19444 * $( 'body' ).append( windowManager.$element );
19445 * windowManager.addWindows( [ messageDialog ] );
19446 * // Open the window.
19447 * windowManager.openWindow( messageDialog, {
19448 * title: 'Basic message dialog',
19449 * message: 'This is the message'
19450 * } );
19451 *
19452 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Message_Dialogs
19453 *
19454 * @class
19455 * @extends OO.ui.Dialog
19456 *
19457 * @constructor
19458 * @param {Object} [config] Configuration options
19459 */
19460 OO.ui.MessageDialog = function OoUiMessageDialog( config ) {
19461 // Parent constructor
19462 OO.ui.MessageDialog.parent.call( this, config );
19463
19464 // Properties
19465 this.verticalActionLayout = null;
19466
19467 // Initialization
19468 this.$element.addClass( 'oo-ui-messageDialog' );
19469 };
19470
19471 /* Setup */
19472
19473 OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog );
19474
19475 /* Static Properties */
19476
19477 OO.ui.MessageDialog.static.name = 'message';
19478
19479 OO.ui.MessageDialog.static.size = 'small';
19480
19481 OO.ui.MessageDialog.static.verbose = false;
19482
19483 /**
19484 * Dialog title.
19485 *
19486 * The title of a confirmation dialog describes what a progressive action will do. The
19487 * title of an alert dialog describes which event occurred.
19488 *
19489 * @static
19490 * @inheritable
19491 * @property {jQuery|string|Function|null}
19492 */
19493 OO.ui.MessageDialog.static.title = null;
19494
19495 /**
19496 * The message displayed in the dialog body.
19497 *
19498 * A confirmation message describes the consequences of a progressive action. An alert
19499 * message describes why an event occurred.
19500 *
19501 * @static
19502 * @inheritable
19503 * @property {jQuery|string|Function|null}
19504 */
19505 OO.ui.MessageDialog.static.message = null;
19506
19507 // Note that OO.ui.alert() and OO.ui.confirm() rely on these.
19508 OO.ui.MessageDialog.static.actions = [
19509 { action: 'accept', label: OO.ui.deferMsg( 'ooui-dialog-message-accept' ), flags: 'primary' },
19510 { action: 'reject', label: OO.ui.deferMsg( 'ooui-dialog-message-reject' ), flags: 'safe' }
19511 ];
19512
19513 /* Methods */
19514
19515 /**
19516 * @inheritdoc
19517 */
19518 OO.ui.MessageDialog.prototype.setManager = function ( manager ) {
19519 OO.ui.MessageDialog.parent.prototype.setManager.call( this, manager );
19520
19521 // Events
19522 this.manager.connect( this, {
19523 resize: 'onResize'
19524 } );
19525
19526 return this;
19527 };
19528
19529 /**
19530 * @inheritdoc
19531 */
19532 OO.ui.MessageDialog.prototype.onActionResize = function ( action ) {
19533 this.fitActions();
19534 return OO.ui.MessageDialog.parent.prototype.onActionResize.call( this, action );
19535 };
19536
19537 /**
19538 * Handle window resized events.
19539 *
19540 * @private
19541 */
19542 OO.ui.MessageDialog.prototype.onResize = function () {
19543 var dialog = this;
19544 dialog.fitActions();
19545 // Wait for CSS transition to finish and do it again :(
19546 setTimeout( function () {
19547 dialog.fitActions();
19548 }, 300 );
19549 };
19550
19551 /**
19552 * Toggle action layout between vertical and horizontal.
19553 *
19554 * @private
19555 * @param {boolean} [value] Layout actions vertically, omit to toggle
19556 * @chainable
19557 */
19558 OO.ui.MessageDialog.prototype.toggleVerticalActionLayout = function ( value ) {
19559 value = value === undefined ? !this.verticalActionLayout : !!value;
19560
19561 if ( value !== this.verticalActionLayout ) {
19562 this.verticalActionLayout = value;
19563 this.$actions
19564 .toggleClass( 'oo-ui-messageDialog-actions-vertical', value )
19565 .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value );
19566 }
19567
19568 return this;
19569 };
19570
19571 /**
19572 * @inheritdoc
19573 */
19574 OO.ui.MessageDialog.prototype.getActionProcess = function ( action ) {
19575 if ( action ) {
19576 return new OO.ui.Process( function () {
19577 this.close( { action: action } );
19578 }, this );
19579 }
19580 return OO.ui.MessageDialog.parent.prototype.getActionProcess.call( this, action );
19581 };
19582
19583 /**
19584 * @inheritdoc
19585 *
19586 * @param {Object} [data] Dialog opening data
19587 * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
19588 * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
19589 * @param {boolean} [data.verbose] Message is verbose and should be styled as a long message
19590 * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each
19591 * action item
19592 */
19593 OO.ui.MessageDialog.prototype.getSetupProcess = function ( data ) {
19594 data = data || {};
19595
19596 // Parent method
19597 return OO.ui.MessageDialog.parent.prototype.getSetupProcess.call( this, data )
19598 .next( function () {
19599 this.title.setLabel(
19600 data.title !== undefined ? data.title : this.constructor.static.title
19601 );
19602 this.message.setLabel(
19603 data.message !== undefined ? data.message : this.constructor.static.message
19604 );
19605 this.message.$element.toggleClass(
19606 'oo-ui-messageDialog-message-verbose',
19607 data.verbose !== undefined ? data.verbose : this.constructor.static.verbose
19608 );
19609 }, this );
19610 };
19611
19612 /**
19613 * @inheritdoc
19614 */
19615 OO.ui.MessageDialog.prototype.getReadyProcess = function ( data ) {
19616 data = data || {};
19617
19618 // Parent method
19619 return OO.ui.MessageDialog.parent.prototype.getReadyProcess.call( this, data )
19620 .next( function () {
19621 // Focus the primary action button
19622 var actions = this.actions.get();
19623 actions = actions.filter( function ( action ) {
19624 return action.getFlags().indexOf( 'primary' ) > -1;
19625 } );
19626 if ( actions.length > 0 ) {
19627 actions[ 0 ].$button.focus();
19628 }
19629 }, this );
19630 };
19631
19632 /**
19633 * @inheritdoc
19634 */
19635 OO.ui.MessageDialog.prototype.getBodyHeight = function () {
19636 var bodyHeight, oldOverflow,
19637 $scrollable = this.container.$element;
19638
19639 oldOverflow = $scrollable[ 0 ].style.overflow;
19640 $scrollable[ 0 ].style.overflow = 'hidden';
19641
19642 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
19643
19644 bodyHeight = this.text.$element.outerHeight( true );
19645 $scrollable[ 0 ].style.overflow = oldOverflow;
19646
19647 return bodyHeight;
19648 };
19649
19650 /**
19651 * @inheritdoc
19652 */
19653 OO.ui.MessageDialog.prototype.setDimensions = function ( dim ) {
19654 var $scrollable = this.container.$element;
19655 OO.ui.MessageDialog.parent.prototype.setDimensions.call( this, dim );
19656
19657 // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
19658 // Need to do it after transition completes (250ms), add 50ms just in case.
19659 setTimeout( function () {
19660 var oldOverflow = $scrollable[ 0 ].style.overflow;
19661 $scrollable[ 0 ].style.overflow = 'hidden';
19662
19663 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
19664
19665 $scrollable[ 0 ].style.overflow = oldOverflow;
19666 }, 300 );
19667
19668 return this;
19669 };
19670
19671 /**
19672 * @inheritdoc
19673 */
19674 OO.ui.MessageDialog.prototype.initialize = function () {
19675 // Parent method
19676 OO.ui.MessageDialog.parent.prototype.initialize.call( this );
19677
19678 // Properties
19679 this.$actions = $( '<div>' );
19680 this.container = new OO.ui.PanelLayout( {
19681 scrollable: true, classes: [ 'oo-ui-messageDialog-container' ]
19682 } );
19683 this.text = new OO.ui.PanelLayout( {
19684 padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ]
19685 } );
19686 this.message = new OO.ui.LabelWidget( {
19687 classes: [ 'oo-ui-messageDialog-message' ]
19688 } );
19689
19690 // Initialization
19691 this.title.$element.addClass( 'oo-ui-messageDialog-title' );
19692 this.$content.addClass( 'oo-ui-messageDialog-content' );
19693 this.container.$element.append( this.text.$element );
19694 this.text.$element.append( this.title.$element, this.message.$element );
19695 this.$body.append( this.container.$element );
19696 this.$actions.addClass( 'oo-ui-messageDialog-actions' );
19697 this.$foot.append( this.$actions );
19698 };
19699
19700 /**
19701 * @inheritdoc
19702 */
19703 OO.ui.MessageDialog.prototype.attachActions = function () {
19704 var i, len, other, special, others;
19705
19706 // Parent method
19707 OO.ui.MessageDialog.parent.prototype.attachActions.call( this );
19708
19709 special = this.actions.getSpecial();
19710 others = this.actions.getOthers();
19711
19712 if ( special.safe ) {
19713 this.$actions.append( special.safe.$element );
19714 special.safe.toggleFramed( false );
19715 }
19716 if ( others.length ) {
19717 for ( i = 0, len = others.length; i < len; i++ ) {
19718 other = others[ i ];
19719 this.$actions.append( other.$element );
19720 other.toggleFramed( false );
19721 }
19722 }
19723 if ( special.primary ) {
19724 this.$actions.append( special.primary.$element );
19725 special.primary.toggleFramed( false );
19726 }
19727
19728 if ( !this.isOpening() ) {
19729 // If the dialog is currently opening, this will be called automatically soon.
19730 // This also calls #fitActions.
19731 this.updateSize();
19732 }
19733 };
19734
19735 /**
19736 * Fit action actions into columns or rows.
19737 *
19738 * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
19739 *
19740 * @private
19741 */
19742 OO.ui.MessageDialog.prototype.fitActions = function () {
19743 var i, len, action,
19744 previous = this.verticalActionLayout,
19745 actions = this.actions.get();
19746
19747 // Detect clipping
19748 this.toggleVerticalActionLayout( false );
19749 for ( i = 0, len = actions.length; i < len; i++ ) {
19750 action = actions[ i ];
19751 if ( action.$element.innerWidth() < action.$label.outerWidth( true ) ) {
19752 this.toggleVerticalActionLayout( true );
19753 break;
19754 }
19755 }
19756
19757 // Move the body out of the way of the foot
19758 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
19759
19760 if ( this.verticalActionLayout !== previous ) {
19761 // We changed the layout, window height might need to be updated.
19762 this.updateSize();
19763 }
19764 };
19765
19766 /**
19767 * ProcessDialog windows encapsulate a {@link OO.ui.Process process} and all of the code necessary
19768 * to complete it. If the process terminates with an error, a customizable {@link OO.ui.Error error
19769 * interface} alerts users to the trouble, permitting the user to dismiss the error and try again when
19770 * relevant. The ProcessDialog class is always extended and customized with the actions and content
19771 * required for each process.
19772 *
19773 * The process dialog box consists of a header that visually represents the ‘working’ state of long
19774 * processes with an animation. The header contains the dialog title as well as
19775 * two {@link OO.ui.ActionWidget action widgets}: a ‘safe’ action on the left (e.g., ‘Cancel’) and
19776 * a ‘primary’ action on the right (e.g., ‘Done’).
19777 *
19778 * Like other windows, the process dialog is managed by a {@link OO.ui.WindowManager window manager}.
19779 * Please see the [OOjs UI documentation on MediaWiki][1] for more information and examples.
19780 *
19781 * @example
19782 * // Example: Creating and opening a process dialog window.
19783 * function MyProcessDialog( config ) {
19784 * MyProcessDialog.parent.call( this, config );
19785 * }
19786 * OO.inheritClass( MyProcessDialog, OO.ui.ProcessDialog );
19787 *
19788 * MyProcessDialog.static.title = 'Process dialog';
19789 * MyProcessDialog.static.actions = [
19790 * { action: 'save', label: 'Done', flags: 'primary' },
19791 * { label: 'Cancel', flags: 'safe' }
19792 * ];
19793 *
19794 * MyProcessDialog.prototype.initialize = function () {
19795 * MyProcessDialog.parent.prototype.initialize.apply( this, arguments );
19796 * this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
19797 * 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>' );
19798 * this.$body.append( this.content.$element );
19799 * };
19800 * MyProcessDialog.prototype.getActionProcess = function ( action ) {
19801 * var dialog = this;
19802 * if ( action ) {
19803 * return new OO.ui.Process( function () {
19804 * dialog.close( { action: action } );
19805 * } );
19806 * }
19807 * return MyProcessDialog.parent.prototype.getActionProcess.call( this, action );
19808 * };
19809 *
19810 * var windowManager = new OO.ui.WindowManager();
19811 * $( 'body' ).append( windowManager.$element );
19812 *
19813 * var dialog = new MyProcessDialog();
19814 * windowManager.addWindows( [ dialog ] );
19815 * windowManager.openWindow( dialog );
19816 *
19817 * [1]: https://www.mediawiki.org/wiki/OOjs_UI/Windows/Process_Dialogs
19818 *
19819 * @abstract
19820 * @class
19821 * @extends OO.ui.Dialog
19822 *
19823 * @constructor
19824 * @param {Object} [config] Configuration options
19825 */
19826 OO.ui.ProcessDialog = function OoUiProcessDialog( config ) {
19827 // Parent constructor
19828 OO.ui.ProcessDialog.parent.call( this, config );
19829
19830 // Properties
19831 this.fitOnOpen = false;
19832
19833 // Initialization
19834 this.$element.addClass( 'oo-ui-processDialog' );
19835 };
19836
19837 /* Setup */
19838
19839 OO.inheritClass( OO.ui.ProcessDialog, OO.ui.Dialog );
19840
19841 /* Methods */
19842
19843 /**
19844 * Handle dismiss button click events.
19845 *
19846 * Hides errors.
19847 *
19848 * @private
19849 */
19850 OO.ui.ProcessDialog.prototype.onDismissErrorButtonClick = function () {
19851 this.hideErrors();
19852 };
19853
19854 /**
19855 * Handle retry button click events.
19856 *
19857 * Hides errors and then tries again.
19858 *
19859 * @private
19860 */
19861 OO.ui.ProcessDialog.prototype.onRetryButtonClick = function () {
19862 this.hideErrors();
19863 this.executeAction( this.currentAction );
19864 };
19865
19866 /**
19867 * @inheritdoc
19868 */
19869 OO.ui.ProcessDialog.prototype.onActionResize = function ( action ) {
19870 if ( this.actions.isSpecial( action ) ) {
19871 this.fitLabel();
19872 }
19873 return OO.ui.ProcessDialog.parent.prototype.onActionResize.call( this, action );
19874 };
19875
19876 /**
19877 * @inheritdoc
19878 */
19879 OO.ui.ProcessDialog.prototype.initialize = function () {
19880 // Parent method
19881 OO.ui.ProcessDialog.parent.prototype.initialize.call( this );
19882
19883 // Properties
19884 this.$navigation = $( '<div>' );
19885 this.$location = $( '<div>' );
19886 this.$safeActions = $( '<div>' );
19887 this.$primaryActions = $( '<div>' );
19888 this.$otherActions = $( '<div>' );
19889 this.dismissButton = new OO.ui.ButtonWidget( {
19890 label: OO.ui.msg( 'ooui-dialog-process-dismiss' )
19891 } );
19892 this.retryButton = new OO.ui.ButtonWidget();
19893 this.$errors = $( '<div>' );
19894 this.$errorsTitle = $( '<div>' );
19895
19896 // Events
19897 this.dismissButton.connect( this, { click: 'onDismissErrorButtonClick' } );
19898 this.retryButton.connect( this, { click: 'onRetryButtonClick' } );
19899
19900 // Initialization
19901 this.title.$element.addClass( 'oo-ui-processDialog-title' );
19902 this.$location
19903 .append( this.title.$element )
19904 .addClass( 'oo-ui-processDialog-location' );
19905 this.$safeActions.addClass( 'oo-ui-processDialog-actions-safe' );
19906 this.$primaryActions.addClass( 'oo-ui-processDialog-actions-primary' );
19907 this.$otherActions.addClass( 'oo-ui-processDialog-actions-other' );
19908 this.$errorsTitle
19909 .addClass( 'oo-ui-processDialog-errors-title' )
19910 .text( OO.ui.msg( 'ooui-dialog-process-error' ) );
19911 this.$errors
19912 .addClass( 'oo-ui-processDialog-errors oo-ui-element-hidden' )
19913 .append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element );
19914 this.$content
19915 .addClass( 'oo-ui-processDialog-content' )
19916 .append( this.$errors );
19917 this.$navigation
19918 .addClass( 'oo-ui-processDialog-navigation' )
19919 .append( this.$safeActions, this.$location, this.$primaryActions );
19920 this.$head.append( this.$navigation );
19921 this.$foot.append( this.$otherActions );
19922 };
19923
19924 /**
19925 * @inheritdoc
19926 */
19927 OO.ui.ProcessDialog.prototype.getActionWidgets = function ( actions ) {
19928 var i, len, widgets = [];
19929 for ( i = 0, len = actions.length; i < len; i++ ) {
19930 widgets.push(
19931 new OO.ui.ActionWidget( $.extend( { framed: true }, actions[ i ] ) )
19932 );
19933 }
19934 return widgets;
19935 };
19936
19937 /**
19938 * @inheritdoc
19939 */
19940 OO.ui.ProcessDialog.prototype.attachActions = function () {
19941 var i, len, other, special, others;
19942
19943 // Parent method
19944 OO.ui.ProcessDialog.parent.prototype.attachActions.call( this );
19945
19946 special = this.actions.getSpecial();
19947 others = this.actions.getOthers();
19948 if ( special.primary ) {
19949 this.$primaryActions.append( special.primary.$element );
19950 }
19951 for ( i = 0, len = others.length; i < len; i++ ) {
19952 other = others[ i ];
19953 this.$otherActions.append( other.$element );
19954 }
19955 if ( special.safe ) {
19956 this.$safeActions.append( special.safe.$element );
19957 }
19958
19959 this.fitLabel();
19960 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
19961 };
19962
19963 /**
19964 * @inheritdoc
19965 */
19966 OO.ui.ProcessDialog.prototype.executeAction = function ( action ) {
19967 var process = this;
19968 return OO.ui.ProcessDialog.parent.prototype.executeAction.call( this, action )
19969 .fail( function ( errors ) {
19970 process.showErrors( errors || [] );
19971 } );
19972 };
19973
19974 /**
19975 * @inheritdoc
19976 */
19977 OO.ui.ProcessDialog.prototype.setDimensions = function () {
19978 // Parent method
19979 OO.ui.ProcessDialog.parent.prototype.setDimensions.apply( this, arguments );
19980
19981 this.fitLabel();
19982 };
19983
19984 /**
19985 * Fit label between actions.
19986 *
19987 * @private
19988 * @chainable
19989 */
19990 OO.ui.ProcessDialog.prototype.fitLabel = function () {
19991 var safeWidth, primaryWidth, biggerWidth, labelWidth, navigationWidth, leftWidth, rightWidth,
19992 size = this.getSizeProperties();
19993
19994 if ( typeof size.width !== 'number' ) {
19995 if ( this.isOpened() ) {
19996 navigationWidth = this.$head.width() - 20;
19997 } else if ( this.isOpening() ) {
19998 if ( !this.fitOnOpen ) {
19999 // Size is relative and the dialog isn't open yet, so wait.
20000 this.manager.opening.done( this.fitLabel.bind( this ) );
20001 this.fitOnOpen = true;
20002 }
20003 return;
20004 } else {
20005 return;
20006 }
20007 } else {
20008 navigationWidth = size.width - 20;
20009 }
20010
20011 safeWidth = this.$safeActions.is( ':visible' ) ? this.$safeActions.width() : 0;
20012 primaryWidth = this.$primaryActions.is( ':visible' ) ? this.$primaryActions.width() : 0;
20013 biggerWidth = Math.max( safeWidth, primaryWidth );
20014
20015 labelWidth = this.title.$element.width();
20016
20017 if ( 2 * biggerWidth + labelWidth < navigationWidth ) {
20018 // We have enough space to center the label
20019 leftWidth = rightWidth = biggerWidth;
20020 } else {
20021 // Let's hope we at least have enough space not to overlap, because we can't wrap the label…
20022 if ( this.getDir() === 'ltr' ) {
20023 leftWidth = safeWidth;
20024 rightWidth = primaryWidth;
20025 } else {
20026 leftWidth = primaryWidth;
20027 rightWidth = safeWidth;
20028 }
20029 }
20030
20031 this.$location.css( { paddingLeft: leftWidth, paddingRight: rightWidth } );
20032
20033 return this;
20034 };
20035
20036 /**
20037 * Handle errors that occurred during accept or reject processes.
20038 *
20039 * @private
20040 * @param {OO.ui.Error[]|OO.ui.Error} errors Errors to be handled
20041 */
20042 OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
20043 var i, len, $item, actions,
20044 items = [],
20045 abilities = {},
20046 recoverable = true,
20047 warning = false;
20048
20049 if ( errors instanceof OO.ui.Error ) {
20050 errors = [ errors ];
20051 }
20052
20053 for ( i = 0, len = errors.length; i < len; i++ ) {
20054 if ( !errors[ i ].isRecoverable() ) {
20055 recoverable = false;
20056 }
20057 if ( errors[ i ].isWarning() ) {
20058 warning = true;
20059 }
20060 $item = $( '<div>' )
20061 .addClass( 'oo-ui-processDialog-error' )
20062 .append( errors[ i ].getMessage() );
20063 items.push( $item[ 0 ] );
20064 }
20065 this.$errorItems = $( items );
20066 if ( recoverable ) {
20067 abilities[ this.currentAction ] = true;
20068 // Copy the flags from the first matching action
20069 actions = this.actions.get( { actions: this.currentAction } );
20070 if ( actions.length ) {
20071 this.retryButton.clearFlags().setFlags( actions[ 0 ].getFlags() );
20072 }
20073 } else {
20074 abilities[ this.currentAction ] = false;
20075 this.actions.setAbilities( abilities );
20076 }
20077 if ( warning ) {
20078 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-continue' ) );
20079 } else {
20080 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-retry' ) );
20081 }
20082 this.retryButton.toggle( recoverable );
20083 this.$errorsTitle.after( this.$errorItems );
20084 this.$errors.removeClass( 'oo-ui-element-hidden' ).scrollTop( 0 );
20085 };
20086
20087 /**
20088 * Hide errors.
20089 *
20090 * @private
20091 */
20092 OO.ui.ProcessDialog.prototype.hideErrors = function () {
20093 this.$errors.addClass( 'oo-ui-element-hidden' );
20094 if ( this.$errorItems ) {
20095 this.$errorItems.remove();
20096 this.$errorItems = null;
20097 }
20098 };
20099
20100 /**
20101 * @inheritdoc
20102 */
20103 OO.ui.ProcessDialog.prototype.getTeardownProcess = function ( data ) {
20104 // Parent method
20105 return OO.ui.ProcessDialog.parent.prototype.getTeardownProcess.call( this, data )
20106 .first( function () {
20107 // Make sure to hide errors
20108 this.hideErrors();
20109 this.fitOnOpen = false;
20110 }, this );
20111 };
20112
20113 /**
20114 * @class OO.ui
20115 */
20116
20117 /**
20118 * Lazy-initialize and return a global OO.ui.WindowManager instance, used by OO.ui.alert and
20119 * OO.ui.confirm.
20120 *
20121 * @private
20122 * @return {OO.ui.WindowManager}
20123 */
20124 OO.ui.getWindowManager = function () {
20125 if ( !OO.ui.windowManager ) {
20126 OO.ui.windowManager = new OO.ui.WindowManager();
20127 $( 'body' ).append( OO.ui.windowManager.$element );
20128 OO.ui.windowManager.addWindows( {
20129 messageDialog: new OO.ui.MessageDialog()
20130 } );
20131 }
20132 return OO.ui.windowManager;
20133 };
20134
20135 /**
20136 * Display a quick modal alert dialog, using a OO.ui.MessageDialog. While the dialog is open, the
20137 * rest of the page will be dimmed out and the user won't be able to interact with it. The dialog
20138 * has only one action button, labelled "OK", clicking it will simply close the dialog.
20139 *
20140 * A window manager is created automatically when this function is called for the first time.
20141 *
20142 * @example
20143 * OO.ui.alert( 'Something happened!' ).done( function () {
20144 * console.log( 'User closed the dialog.' );
20145 * } );
20146 *
20147 * @param {jQuery|string} text Message text to display
20148 * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
20149 * @return {jQuery.Promise} Promise resolved when the user closes the dialog
20150 */
20151 OO.ui.alert = function ( text, options ) {
20152 return OO.ui.getWindowManager().openWindow( 'messageDialog', $.extend( {
20153 message: text,
20154 verbose: true,
20155 actions: [ OO.ui.MessageDialog.static.actions[ 0 ] ]
20156 }, options ) ).then( function ( opened ) {
20157 return opened.then( function ( closing ) {
20158 return closing.then( function () {
20159 return $.Deferred().resolve();
20160 } );
20161 } );
20162 } );
20163 };
20164
20165 /**
20166 * Display a quick modal confirmation dialog, using a OO.ui.MessageDialog. While the dialog is open,
20167 * the rest of the page will be dimmed out and the user won't be able to interact with it. The
20168 * dialog has two action buttons, one to confirm an operation (labelled "OK") and one to cancel it
20169 * (labelled "Cancel").
20170 *
20171 * A window manager is created automatically when this function is called for the first time.
20172 *
20173 * @example
20174 * OO.ui.confirm( 'Are you sure?' ).done( function ( confirmed ) {
20175 * if ( confirmed ) {
20176 * console.log( 'User clicked "OK"!' );
20177 * } else {
20178 * console.log( 'User clicked "Cancel" or closed the dialog.' );
20179 * }
20180 * } );
20181 *
20182 * @param {jQuery|string} text Message text to display
20183 * @param {Object} [options] Additional options, see OO.ui.MessageDialog#getSetupProcess
20184 * @return {jQuery.Promise} Promise resolved when the user closes the dialog. If the user chose to
20185 * confirm, the promise will resolve to boolean `true`; otherwise, it will resolve to boolean
20186 * `false`.
20187 */
20188 OO.ui.confirm = function ( text, options ) {
20189 return OO.ui.getWindowManager().openWindow( 'messageDialog', $.extend( {
20190 message: text,
20191 verbose: true
20192 }, options ) ).then( function ( opened ) {
20193 return opened.then( function ( closing ) {
20194 return closing.then( function ( data ) {
20195 return $.Deferred().resolve( !!( data && data.action === 'accept' ) );
20196 } );
20197 } );
20198 } );
20199 };
20200
20201 }( OO ) );