Merge "update.php: Add option to not check if external dependencies are up to date"
[lhc/web/wiklou.git] / resources / lib / oojs-ui / oojs-ui.js
1 /*!
2 * OOjs UI v0.7.0
3 * https://www.mediawiki.org/wiki/OOjs_UI
4 *
5 * Copyright 2011–2015 OOjs Team and other contributors.
6 * Released under the MIT license
7 * http://oojs.mit-license.org
8 *
9 * Date: 2015-02-12T00:04:43Z
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 * Get the user's language and any fallback languages.
49 *
50 * These language codes are used to localize user interface elements in the user's language.
51 *
52 * In environments that provide a localization system, this function should be overridden to
53 * return the user's language(s). The default implementation returns English (en) only.
54 *
55 * @return {string[]} Language codes, in descending order of priority
56 */
57 OO.ui.getUserLanguages = function () {
58 return [ 'en' ];
59 };
60
61 /**
62 * Get a value in an object keyed by language code.
63 *
64 * @param {Object.<string,Mixed>} obj Object keyed by language code
65 * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
66 * @param {string} [fallback] Fallback code, used if no matching language can be found
67 * @return {Mixed} Local value
68 */
69 OO.ui.getLocalValue = function ( obj, lang, fallback ) {
70 var i, len, langs;
71
72 // Requested language
73 if ( obj[ lang ] ) {
74 return obj[ lang ];
75 }
76 // Known user language
77 langs = OO.ui.getUserLanguages();
78 for ( i = 0, len = langs.length; i < len; i++ ) {
79 lang = langs[ i ];
80 if ( obj[ lang ] ) {
81 return obj[ lang ];
82 }
83 }
84 // Fallback language
85 if ( obj[ fallback ] ) {
86 return obj[ fallback ];
87 }
88 // First existing language
89 for ( lang in obj ) {
90 return obj[ lang ];
91 }
92
93 return undefined;
94 };
95
96 /**
97 * Check if a node is contained within another node
98 *
99 * Similar to jQuery#contains except a list of containers can be supplied
100 * and a boolean argument allows you to include the container in the match list
101 *
102 * @param {HTMLElement|HTMLElement[]} containers Container node(s) to search in
103 * @param {HTMLElement} contained Node to find
104 * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match, otherwise only match descendants
105 * @return {boolean} The node is in the list of target nodes
106 */
107 OO.ui.contains = function ( containers, contained, matchContainers ) {
108 var i;
109 if ( !Array.isArray( containers ) ) {
110 containers = [ containers ];
111 }
112 for ( i = containers.length - 1; i >= 0; i-- ) {
113 if ( ( matchContainers && contained === containers[ i ] ) || $.contains( containers[ i ], contained ) ) {
114 return true;
115 }
116 }
117 return false;
118 };
119
120 ( function () {
121 /**
122 * Message store for the default implementation of OO.ui.msg
123 *
124 * Environments that provide a localization system should not use this, but should override
125 * OO.ui.msg altogether.
126 *
127 * @private
128 */
129 var messages = {
130 // Tool tip for a button that moves items in a list down one place
131 'ooui-outline-control-move-down': 'Move item down',
132 // Tool tip for a button that moves items in a list up one place
133 'ooui-outline-control-move-up': 'Move item up',
134 // Tool tip for a button that removes items from a list
135 'ooui-outline-control-remove': 'Remove item',
136 // Label for the toolbar group that contains a list of all other available tools
137 'ooui-toolbar-more': 'More',
138 // Label for the fake tool that expands the full list of tools in a toolbar group
139 'ooui-toolgroup-expand': 'More',
140 // Label for the fake tool that collapses the full list of tools in a toolbar group
141 'ooui-toolgroup-collapse': 'Fewer',
142 // Default label for the accept button of a confirmation dialog
143 'ooui-dialog-message-accept': 'OK',
144 // Default label for the reject button of a confirmation dialog
145 'ooui-dialog-message-reject': 'Cancel',
146 // Title for process dialog error description
147 'ooui-dialog-process-error': 'Something went wrong',
148 // Label for process dialog dismiss error button, visible when describing errors
149 'ooui-dialog-process-dismiss': 'Dismiss',
150 // Label for process dialog retry action button, visible when describing only recoverable errors
151 'ooui-dialog-process-retry': 'Try again',
152 // Label for process dialog retry action button, visible when describing only warnings
153 'ooui-dialog-process-continue': 'Continue'
154 };
155
156 /**
157 * Get a localized message.
158 *
159 * In environments that provide a localization system, this function should be overridden to
160 * return the message translated in the user's language. The default implementation always returns
161 * English messages.
162 *
163 * After the message key, message parameters may optionally be passed. In the default implementation,
164 * any occurrences of $1 are replaced with the first parameter, $2 with the second parameter, etc.
165 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long as
166 * they support unnamed, ordered message parameters.
167 *
168 * @abstract
169 * @param {string} key Message key
170 * @param {Mixed...} [params] Message parameters
171 * @return {string} Translated message with parameters substituted
172 */
173 OO.ui.msg = function ( key ) {
174 var message = messages[ key ],
175 params = Array.prototype.slice.call( arguments, 1 );
176 if ( typeof message === 'string' ) {
177 // Perform $1 substitution
178 message = message.replace( /\$(\d+)/g, function ( unused, n ) {
179 var i = parseInt( n, 10 );
180 return params[ i - 1 ] !== undefined ? params[ i - 1 ] : '$' + n;
181 } );
182 } else {
183 // Return placeholder if message not found
184 message = '[' + key + ']';
185 }
186 return message;
187 };
188
189 /**
190 * Package a message and arguments for deferred resolution.
191 *
192 * Use this when you are statically specifying a message and the message may not yet be present.
193 *
194 * @param {string} key Message key
195 * @param {Mixed...} [params] Message parameters
196 * @return {Function} Function that returns the resolved message when executed
197 */
198 OO.ui.deferMsg = function () {
199 var args = arguments;
200 return function () {
201 return OO.ui.msg.apply( OO.ui, args );
202 };
203 };
204
205 /**
206 * Resolve a message.
207 *
208 * If the message is a function it will be executed, otherwise it will pass through directly.
209 *
210 * @param {Function|string} msg Deferred message, or message text
211 * @return {string} Resolved message
212 */
213 OO.ui.resolveMsg = function ( msg ) {
214 if ( $.isFunction( msg ) ) {
215 return msg();
216 }
217 return msg;
218 };
219
220 } )();
221
222 /**
223 * Element that can be marked as pending.
224 *
225 * @abstract
226 * @class
227 *
228 * @constructor
229 * @param {Object} [config] Configuration options
230 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
231 */
232 OO.ui.PendingElement = function OoUiPendingElement( config ) {
233 // Configuration initialization
234 config = config || {};
235
236 // Properties
237 this.pending = 0;
238 this.$pending = null;
239
240 // Initialisation
241 this.setPendingElement( config.$pending || this.$element );
242 };
243
244 /* Setup */
245
246 OO.initClass( OO.ui.PendingElement );
247
248 /* Methods */
249
250 /**
251 * Set the pending element (and clean up any existing one).
252 *
253 * @param {jQuery} $pending The element to set to pending.
254 */
255 OO.ui.PendingElement.prototype.setPendingElement = function ( $pending ) {
256 if ( this.$pending ) {
257 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
258 }
259
260 this.$pending = $pending;
261 if ( this.pending > 0 ) {
262 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
263 }
264 };
265
266 /**
267 * Check if input is pending.
268 *
269 * @return {boolean}
270 */
271 OO.ui.PendingElement.prototype.isPending = function () {
272 return !!this.pending;
273 };
274
275 /**
276 * Increase the pending stack.
277 *
278 * @chainable
279 */
280 OO.ui.PendingElement.prototype.pushPending = function () {
281 if ( this.pending === 0 ) {
282 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
283 this.updateThemeClasses();
284 }
285 this.pending++;
286
287 return this;
288 };
289
290 /**
291 * Reduce the pending stack.
292 *
293 * Clamped at zero.
294 *
295 * @chainable
296 */
297 OO.ui.PendingElement.prototype.popPending = function () {
298 if ( this.pending === 1 ) {
299 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
300 this.updateThemeClasses();
301 }
302 this.pending = Math.max( 0, this.pending - 1 );
303
304 return this;
305 };
306
307 /**
308 * List of actions.
309 *
310 * @abstract
311 * @class
312 * @mixins OO.EventEmitter
313 *
314 * @constructor
315 * @param {Object} [config] Configuration options
316 */
317 OO.ui.ActionSet = function OoUiActionSet( config ) {
318 // Configuration initialization
319 config = config || {};
320
321 // Mixin constructors
322 OO.EventEmitter.call( this );
323
324 // Properties
325 this.list = [];
326 this.categories = {
327 actions: 'getAction',
328 flags: 'getFlags',
329 modes: 'getModes'
330 };
331 this.categorized = {};
332 this.special = {};
333 this.others = [];
334 this.organized = false;
335 this.changing = false;
336 this.changed = false;
337 };
338
339 /* Setup */
340
341 OO.mixinClass( OO.ui.ActionSet, OO.EventEmitter );
342
343 /* Static Properties */
344
345 /**
346 * Symbolic name of dialog.
347 *
348 * @abstract
349 * @static
350 * @inheritable
351 * @property {string}
352 */
353 OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ];
354
355 /* Events */
356
357 /**
358 * @event click
359 * @param {OO.ui.ActionWidget} action Action that was clicked
360 */
361
362 /**
363 * @event resize
364 * @param {OO.ui.ActionWidget} action Action that was resized
365 */
366
367 /**
368 * @event add
369 * @param {OO.ui.ActionWidget[]} added Actions added
370 */
371
372 /**
373 * @event remove
374 * @param {OO.ui.ActionWidget[]} added Actions removed
375 */
376
377 /**
378 * @event change
379 */
380
381 /* Methods */
382
383 /**
384 * Handle action change events.
385 *
386 * @fires change
387 */
388 OO.ui.ActionSet.prototype.onActionChange = function () {
389 this.organized = false;
390 if ( this.changing ) {
391 this.changed = true;
392 } else {
393 this.emit( 'change' );
394 }
395 };
396
397 /**
398 * Check if a action is one of the special actions.
399 *
400 * @param {OO.ui.ActionWidget} action Action to check
401 * @return {boolean} Action is special
402 */
403 OO.ui.ActionSet.prototype.isSpecial = function ( action ) {
404 var flag;
405
406 for ( flag in this.special ) {
407 if ( action === this.special[ flag ] ) {
408 return true;
409 }
410 }
411
412 return false;
413 };
414
415 /**
416 * Get actions.
417 *
418 * @param {Object} [filters] Filters to use, omit to get all actions
419 * @param {string|string[]} [filters.actions] Actions that actions must have
420 * @param {string|string[]} [filters.flags] Flags that actions must have
421 * @param {string|string[]} [filters.modes] Modes that actions must have
422 * @param {boolean} [filters.visible] Actions must be visible
423 * @param {boolean} [filters.disabled] Actions must be disabled
424 * @return {OO.ui.ActionWidget[]} Actions matching all criteria
425 */
426 OO.ui.ActionSet.prototype.get = function ( filters ) {
427 var i, len, list, category, actions, index, match, matches;
428
429 if ( filters ) {
430 this.organize();
431
432 // Collect category candidates
433 matches = [];
434 for ( category in this.categorized ) {
435 list = filters[ category ];
436 if ( list ) {
437 if ( !Array.isArray( list ) ) {
438 list = [ list ];
439 }
440 for ( i = 0, len = list.length; i < len; i++ ) {
441 actions = this.categorized[ category ][ list[ i ] ];
442 if ( Array.isArray( actions ) ) {
443 matches.push.apply( matches, actions );
444 }
445 }
446 }
447 }
448 // Remove by boolean filters
449 for ( i = 0, len = matches.length; i < len; i++ ) {
450 match = matches[ i ];
451 if (
452 ( filters.visible !== undefined && match.isVisible() !== filters.visible ) ||
453 ( filters.disabled !== undefined && match.isDisabled() !== filters.disabled )
454 ) {
455 matches.splice( i, 1 );
456 len--;
457 i--;
458 }
459 }
460 // Remove duplicates
461 for ( i = 0, len = matches.length; i < len; i++ ) {
462 match = matches[ i ];
463 index = matches.lastIndexOf( match );
464 while ( index !== i ) {
465 matches.splice( index, 1 );
466 len--;
467 index = matches.lastIndexOf( match );
468 }
469 }
470 return matches;
471 }
472 return this.list.slice();
473 };
474
475 /**
476 * Get special actions.
477 *
478 * Special actions are the first visible actions with special flags, such as 'safe' and 'primary'.
479 * Special flags can be configured by changing #static-specialFlags in a subclass.
480 *
481 * @return {OO.ui.ActionWidget|null} Safe action
482 */
483 OO.ui.ActionSet.prototype.getSpecial = function () {
484 this.organize();
485 return $.extend( {}, this.special );
486 };
487
488 /**
489 * Get other actions.
490 *
491 * Other actions include all non-special visible actions.
492 *
493 * @return {OO.ui.ActionWidget[]} Other actions
494 */
495 OO.ui.ActionSet.prototype.getOthers = function () {
496 this.organize();
497 return this.others.slice();
498 };
499
500 /**
501 * Toggle actions based on their modes.
502 *
503 * Unlike calling toggle on actions with matching flags, this will enforce mutually exclusive
504 * visibility; matching actions will be shown, non-matching actions will be hidden.
505 *
506 * @param {string} mode Mode actions must have
507 * @chainable
508 * @fires toggle
509 * @fires change
510 */
511 OO.ui.ActionSet.prototype.setMode = function ( mode ) {
512 var i, len, action;
513
514 this.changing = true;
515 for ( i = 0, len = this.list.length; i < len; i++ ) {
516 action = this.list[ i ];
517 action.toggle( action.hasMode( mode ) );
518 }
519
520 this.organized = false;
521 this.changing = false;
522 this.emit( 'change' );
523
524 return this;
525 };
526
527 /**
528 * Change which actions are able to be performed.
529 *
530 * Actions with matching actions will be disabled/enabled. Other actions will not be changed.
531 *
532 * @param {Object.<string,boolean>} actions List of abilities, keyed by action name, values
533 * indicate actions are able to be performed
534 * @chainable
535 */
536 OO.ui.ActionSet.prototype.setAbilities = function ( actions ) {
537 var i, len, action, item;
538
539 for ( i = 0, len = this.list.length; i < len; i++ ) {
540 item = this.list[ i ];
541 action = item.getAction();
542 if ( actions[ action ] !== undefined ) {
543 item.setDisabled( !actions[ action ] );
544 }
545 }
546
547 return this;
548 };
549
550 /**
551 * Executes a function once per action.
552 *
553 * When making changes to multiple actions, use this method instead of iterating over the actions
554 * manually to defer emitting a change event until after all actions have been changed.
555 *
556 * @param {Object|null} actions Filters to use for which actions to iterate over; see #get
557 * @param {Function} callback Callback to run for each action; callback is invoked with three
558 * arguments: the action, the action's index, the list of actions being iterated over
559 * @chainable
560 */
561 OO.ui.ActionSet.prototype.forEach = function ( filter, callback ) {
562 this.changed = false;
563 this.changing = true;
564 this.get( filter ).forEach( callback );
565 this.changing = false;
566 if ( this.changed ) {
567 this.emit( 'change' );
568 }
569
570 return this;
571 };
572
573 /**
574 * Add actions.
575 *
576 * @param {OO.ui.ActionWidget[]} actions Actions to add
577 * @chainable
578 * @fires add
579 * @fires change
580 */
581 OO.ui.ActionSet.prototype.add = function ( actions ) {
582 var i, len, action;
583
584 this.changing = true;
585 for ( i = 0, len = actions.length; i < len; i++ ) {
586 action = actions[ i ];
587 action.connect( this, {
588 click: [ 'emit', 'click', action ],
589 resize: [ 'emit', 'resize', action ],
590 toggle: [ 'onActionChange' ]
591 } );
592 this.list.push( action );
593 }
594 this.organized = false;
595 this.emit( 'add', actions );
596 this.changing = false;
597 this.emit( 'change' );
598
599 return this;
600 };
601
602 /**
603 * Remove actions.
604 *
605 * @param {OO.ui.ActionWidget[]} actions Actions to remove
606 * @chainable
607 * @fires remove
608 * @fires change
609 */
610 OO.ui.ActionSet.prototype.remove = function ( actions ) {
611 var i, len, index, action;
612
613 this.changing = true;
614 for ( i = 0, len = actions.length; i < len; i++ ) {
615 action = actions[ i ];
616 index = this.list.indexOf( action );
617 if ( index !== -1 ) {
618 action.disconnect( this );
619 this.list.splice( index, 1 );
620 }
621 }
622 this.organized = false;
623 this.emit( 'remove', actions );
624 this.changing = false;
625 this.emit( 'change' );
626
627 return this;
628 };
629
630 /**
631 * Remove all actions.
632 *
633 * @chainable
634 * @fires remove
635 * @fires change
636 */
637 OO.ui.ActionSet.prototype.clear = function () {
638 var i, len, action,
639 removed = this.list.slice();
640
641 this.changing = true;
642 for ( i = 0, len = this.list.length; i < len; i++ ) {
643 action = this.list[ i ];
644 action.disconnect( this );
645 }
646
647 this.list = [];
648
649 this.organized = false;
650 this.emit( 'remove', removed );
651 this.changing = false;
652 this.emit( 'change' );
653
654 return this;
655 };
656
657 /**
658 * Organize actions.
659 *
660 * This is called whenever organized information is requested. It will only reorganize the actions
661 * if something has changed since the last time it ran.
662 *
663 * @private
664 * @chainable
665 */
666 OO.ui.ActionSet.prototype.organize = function () {
667 var i, iLen, j, jLen, flag, action, category, list, item, special,
668 specialFlags = this.constructor.static.specialFlags;
669
670 if ( !this.organized ) {
671 this.categorized = {};
672 this.special = {};
673 this.others = [];
674 for ( i = 0, iLen = this.list.length; i < iLen; i++ ) {
675 action = this.list[ i ];
676 if ( action.isVisible() ) {
677 // Populate categories
678 for ( category in this.categories ) {
679 if ( !this.categorized[ category ] ) {
680 this.categorized[ category ] = {};
681 }
682 list = action[ this.categories[ category ] ]();
683 if ( !Array.isArray( list ) ) {
684 list = [ list ];
685 }
686 for ( j = 0, jLen = list.length; j < jLen; j++ ) {
687 item = list[ j ];
688 if ( !this.categorized[ category ][ item ] ) {
689 this.categorized[ category ][ item ] = [];
690 }
691 this.categorized[ category ][ item ].push( action );
692 }
693 }
694 // Populate special/others
695 special = false;
696 for ( j = 0, jLen = specialFlags.length; j < jLen; j++ ) {
697 flag = specialFlags[ j ];
698 if ( !this.special[ flag ] && action.hasFlag( flag ) ) {
699 this.special[ flag ] = action;
700 special = true;
701 break;
702 }
703 }
704 if ( !special ) {
705 this.others.push( action );
706 }
707 }
708 }
709 this.organized = true;
710 }
711
712 return this;
713 };
714
715 /**
716 * DOM element abstraction.
717 *
718 * @abstract
719 * @class
720 *
721 * @constructor
722 * @param {Object} [config] Configuration options
723 * @cfg {string[]} [classes] CSS class names to add
724 * @cfg {string} [id] HTML id attribute
725 * @cfg {string} [text] Text to insert
726 * @cfg {jQuery} [$content] Content elements to append (after text)
727 * @cfg {Mixed} [data] Element data
728 */
729 OO.ui.Element = function OoUiElement( config ) {
730 // Configuration initialization
731 config = config || {};
732
733 // Properties
734 this.$ = $;
735 this.data = config.data;
736 this.$element = $( document.createElement( this.getTagName() ) );
737 this.elementGroup = null;
738 this.debouncedUpdateThemeClassesHandler = this.debouncedUpdateThemeClasses.bind( this );
739 this.updateThemeClassesPending = false;
740
741 // Initialization
742 if ( $.isArray( config.classes ) ) {
743 this.$element.addClass( config.classes.join( ' ' ) );
744 }
745 if ( config.id ) {
746 this.$element.attr( 'id', config.id );
747 }
748 if ( config.text ) {
749 this.$element.text( config.text );
750 }
751 if ( config.$content ) {
752 this.$element.append( config.$content );
753 }
754 };
755
756 /* Setup */
757
758 OO.initClass( OO.ui.Element );
759
760 /* Static Properties */
761
762 /**
763 * HTML tag name.
764 *
765 * This may be ignored if #getTagName is overridden.
766 *
767 * @static
768 * @inheritable
769 * @property {string}
770 */
771 OO.ui.Element.static.tagName = 'div';
772
773 /* Static Methods */
774
775 /**
776 * Get a jQuery function within a specific document.
777 *
778 * @static
779 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
780 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
781 * not in an iframe
782 * @return {Function} Bound jQuery function
783 */
784 OO.ui.Element.static.getJQuery = function ( context, $iframe ) {
785 function wrapper( selector ) {
786 return $( selector, wrapper.context );
787 }
788
789 wrapper.context = this.getDocument( context );
790
791 if ( $iframe ) {
792 wrapper.$iframe = $iframe;
793 }
794
795 return wrapper;
796 };
797
798 /**
799 * Get the document of an element.
800 *
801 * @static
802 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
803 * @return {HTMLDocument|null} Document object
804 */
805 OO.ui.Element.static.getDocument = function ( obj ) {
806 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
807 return ( obj[ 0 ] && obj[ 0 ].ownerDocument ) ||
808 // Empty jQuery selections might have a context
809 obj.context ||
810 // HTMLElement
811 obj.ownerDocument ||
812 // Window
813 obj.document ||
814 // HTMLDocument
815 ( obj.nodeType === 9 && obj ) ||
816 null;
817 };
818
819 /**
820 * Get the window of an element or document.
821 *
822 * @static
823 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
824 * @return {Window} Window object
825 */
826 OO.ui.Element.static.getWindow = function ( obj ) {
827 var doc = this.getDocument( obj );
828 return doc.parentWindow || doc.defaultView;
829 };
830
831 /**
832 * Get the direction of an element or document.
833 *
834 * @static
835 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
836 * @return {string} Text direction, either 'ltr' or 'rtl'
837 */
838 OO.ui.Element.static.getDir = function ( obj ) {
839 var isDoc, isWin;
840
841 if ( obj instanceof jQuery ) {
842 obj = obj[ 0 ];
843 }
844 isDoc = obj.nodeType === 9;
845 isWin = obj.document !== undefined;
846 if ( isDoc || isWin ) {
847 if ( isWin ) {
848 obj = obj.document;
849 }
850 obj = obj.body;
851 }
852 return $( obj ).css( 'direction' );
853 };
854
855 /**
856 * Get the offset between two frames.
857 *
858 * TODO: Make this function not use recursion.
859 *
860 * @static
861 * @param {Window} from Window of the child frame
862 * @param {Window} [to=window] Window of the parent frame
863 * @param {Object} [offset] Offset to start with, used internally
864 * @return {Object} Offset object, containing left and top properties
865 */
866 OO.ui.Element.static.getFrameOffset = function ( from, to, offset ) {
867 var i, len, frames, frame, rect;
868
869 if ( !to ) {
870 to = window;
871 }
872 if ( !offset ) {
873 offset = { top: 0, left: 0 };
874 }
875 if ( from.parent === from ) {
876 return offset;
877 }
878
879 // Get iframe element
880 frames = from.parent.document.getElementsByTagName( 'iframe' );
881 for ( i = 0, len = frames.length; i < len; i++ ) {
882 if ( frames[ i ].contentWindow === from ) {
883 frame = frames[ i ];
884 break;
885 }
886 }
887
888 // Recursively accumulate offset values
889 if ( frame ) {
890 rect = frame.getBoundingClientRect();
891 offset.left += rect.left;
892 offset.top += rect.top;
893 if ( from !== to ) {
894 this.getFrameOffset( from.parent, offset );
895 }
896 }
897 return offset;
898 };
899
900 /**
901 * Get the offset between two elements.
902 *
903 * The two elements may be in a different frame, but in that case the frame $element is in must
904 * be contained in the frame $anchor is in.
905 *
906 * @static
907 * @param {jQuery} $element Element whose position to get
908 * @param {jQuery} $anchor Element to get $element's position relative to
909 * @return {Object} Translated position coordinates, containing top and left properties
910 */
911 OO.ui.Element.static.getRelativePosition = function ( $element, $anchor ) {
912 var iframe, iframePos,
913 pos = $element.offset(),
914 anchorPos = $anchor.offset(),
915 elementDocument = this.getDocument( $element ),
916 anchorDocument = this.getDocument( $anchor );
917
918 // If $element isn't in the same document as $anchor, traverse up
919 while ( elementDocument !== anchorDocument ) {
920 iframe = elementDocument.defaultView.frameElement;
921 if ( !iframe ) {
922 throw new Error( '$element frame is not contained in $anchor frame' );
923 }
924 iframePos = $( iframe ).offset();
925 pos.left += iframePos.left;
926 pos.top += iframePos.top;
927 elementDocument = iframe.ownerDocument;
928 }
929 pos.left -= anchorPos.left;
930 pos.top -= anchorPos.top;
931 return pos;
932 };
933
934 /**
935 * Get element border sizes.
936 *
937 * @static
938 * @param {HTMLElement} el Element to measure
939 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
940 */
941 OO.ui.Element.static.getBorders = function ( el ) {
942 var doc = el.ownerDocument,
943 win = doc.parentWindow || doc.defaultView,
944 style = win && win.getComputedStyle ?
945 win.getComputedStyle( el, null ) :
946 el.currentStyle,
947 $el = $( el ),
948 top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0,
949 left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0,
950 bottom = parseFloat( style ? style.borderBottomWidth : $el.css( 'borderBottomWidth' ) ) || 0,
951 right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0;
952
953 return {
954 top: top,
955 left: left,
956 bottom: bottom,
957 right: right
958 };
959 };
960
961 /**
962 * Get dimensions of an element or window.
963 *
964 * @static
965 * @param {HTMLElement|Window} el Element to measure
966 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
967 */
968 OO.ui.Element.static.getDimensions = function ( el ) {
969 var $el, $win,
970 doc = el.ownerDocument || el.document,
971 win = doc.parentWindow || doc.defaultView;
972
973 if ( win === el || el === doc.documentElement ) {
974 $win = $( win );
975 return {
976 borders: { top: 0, left: 0, bottom: 0, right: 0 },
977 scroll: {
978 top: $win.scrollTop(),
979 left: $win.scrollLeft()
980 },
981 scrollbar: { right: 0, bottom: 0 },
982 rect: {
983 top: 0,
984 left: 0,
985 bottom: $win.innerHeight(),
986 right: $win.innerWidth()
987 }
988 };
989 } else {
990 $el = $( el );
991 return {
992 borders: this.getBorders( el ),
993 scroll: {
994 top: $el.scrollTop(),
995 left: $el.scrollLeft()
996 },
997 scrollbar: {
998 right: $el.innerWidth() - el.clientWidth,
999 bottom: $el.innerHeight() - el.clientHeight
1000 },
1001 rect: el.getBoundingClientRect()
1002 };
1003 }
1004 };
1005
1006 /**
1007 * Get scrollable object parent
1008 *
1009 * documentElement can't be used to get or set the scrollTop
1010 * property on Blink. Changing and testing its value lets us
1011 * use 'body' or 'documentElement' based on what is working.
1012 *
1013 * https://code.google.com/p/chromium/issues/detail?id=303131
1014 *
1015 * @static
1016 * @param {HTMLElement} el Element to find scrollable parent for
1017 * @return {HTMLElement} Scrollable parent
1018 */
1019 OO.ui.Element.static.getRootScrollableElement = function ( el ) {
1020 var scrollTop, body;
1021
1022 if ( OO.ui.scrollableElement === undefined ) {
1023 body = el.ownerDocument.body;
1024 scrollTop = body.scrollTop;
1025 body.scrollTop = 1;
1026
1027 if ( body.scrollTop === 1 ) {
1028 body.scrollTop = scrollTop;
1029 OO.ui.scrollableElement = 'body';
1030 } else {
1031 OO.ui.scrollableElement = 'documentElement';
1032 }
1033 }
1034
1035 return el.ownerDocument[ OO.ui.scrollableElement ];
1036 };
1037
1038 /**
1039 * Get closest scrollable container.
1040 *
1041 * Traverses up until either a scrollable element or the root is reached, in which case the window
1042 * will be returned.
1043 *
1044 * @static
1045 * @param {HTMLElement} el Element to find scrollable container for
1046 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1047 * @return {HTMLElement} Closest scrollable container
1048 */
1049 OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) {
1050 var i, val,
1051 props = [ 'overflow' ],
1052 $parent = $( el ).parent();
1053
1054 if ( dimension === 'x' || dimension === 'y' ) {
1055 props.push( 'overflow-' + dimension );
1056 }
1057
1058 while ( $parent.length ) {
1059 if ( $parent[ 0 ] === this.getRootScrollableElement( el ) ) {
1060 return $parent[ 0 ];
1061 }
1062 i = props.length;
1063 while ( i-- ) {
1064 val = $parent.css( props[ i ] );
1065 if ( val === 'auto' || val === 'scroll' ) {
1066 return $parent[ 0 ];
1067 }
1068 }
1069 $parent = $parent.parent();
1070 }
1071 return this.getDocument( el ).body;
1072 };
1073
1074 /**
1075 * Scroll element into view.
1076 *
1077 * @static
1078 * @param {HTMLElement} el Element to scroll into view
1079 * @param {Object} [config] Configuration options
1080 * @param {string} [config.duration] jQuery animation duration value
1081 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1082 * to scroll in both directions
1083 * @param {Function} [config.complete] Function to call when scrolling completes
1084 */
1085 OO.ui.Element.static.scrollIntoView = function ( el, config ) {
1086 // Configuration initialization
1087 config = config || {};
1088
1089 var rel, anim = {},
1090 callback = typeof config.complete === 'function' && config.complete,
1091 sc = this.getClosestScrollableContainer( el, config.direction ),
1092 $sc = $( sc ),
1093 eld = this.getDimensions( el ),
1094 scd = this.getDimensions( sc ),
1095 $win = $( this.getWindow( el ) );
1096
1097 // Compute the distances between the edges of el and the edges of the scroll viewport
1098 if ( $sc.is( 'html, body' ) ) {
1099 // If the scrollable container is the root, this is easy
1100 rel = {
1101 top: eld.rect.top,
1102 bottom: $win.innerHeight() - eld.rect.bottom,
1103 left: eld.rect.left,
1104 right: $win.innerWidth() - eld.rect.right
1105 };
1106 } else {
1107 // Otherwise, we have to subtract el's coordinates from sc's coordinates
1108 rel = {
1109 top: eld.rect.top - ( scd.rect.top + scd.borders.top ),
1110 bottom: scd.rect.bottom - scd.borders.bottom - scd.scrollbar.bottom - eld.rect.bottom,
1111 left: eld.rect.left - ( scd.rect.left + scd.borders.left ),
1112 right: scd.rect.right - scd.borders.right - scd.scrollbar.right - eld.rect.right
1113 };
1114 }
1115
1116 if ( !config.direction || config.direction === 'y' ) {
1117 if ( rel.top < 0 ) {
1118 anim.scrollTop = scd.scroll.top + rel.top;
1119 } else if ( rel.top > 0 && rel.bottom < 0 ) {
1120 anim.scrollTop = scd.scroll.top + Math.min( rel.top, -rel.bottom );
1121 }
1122 }
1123 if ( !config.direction || config.direction === 'x' ) {
1124 if ( rel.left < 0 ) {
1125 anim.scrollLeft = scd.scroll.left + rel.left;
1126 } else if ( rel.left > 0 && rel.right < 0 ) {
1127 anim.scrollLeft = scd.scroll.left + Math.min( rel.left, -rel.right );
1128 }
1129 }
1130 if ( !$.isEmptyObject( anim ) ) {
1131 $sc.stop( true ).animate( anim, config.duration || 'fast' );
1132 if ( callback ) {
1133 $sc.queue( function ( next ) {
1134 callback();
1135 next();
1136 } );
1137 }
1138 } else {
1139 if ( callback ) {
1140 callback();
1141 }
1142 }
1143 };
1144
1145 /**
1146 * Force the browser to reconsider whether it really needs to render scrollbars inside the element
1147 * and reserve space for them, because it probably doesn't.
1148 *
1149 * Workaround primarily for <https://code.google.com/p/chromium/issues/detail?id=387290>, but also
1150 * similar bugs in other browsers. "Just" forcing a reflow is not sufficient in all cases, we need
1151 * to first actually detach (or hide, but detaching is simpler) all children, *then* force a reflow,
1152 * and then reattach (or show) them back.
1153 *
1154 * @static
1155 * @param {HTMLElement} el Element to reconsider the scrollbars on
1156 */
1157 OO.ui.Element.static.reconsiderScrollbars = function ( el ) {
1158 var i, len, nodes = [];
1159 // Detach all children
1160 while ( el.firstChild ) {
1161 nodes.push( el.firstChild );
1162 el.removeChild( el.firstChild );
1163 }
1164 // Force reflow
1165 void el.offsetHeight;
1166 // Reattach all children
1167 for ( i = 0, len = nodes.length; i < len; i++ ) {
1168 el.appendChild( nodes[ i ] );
1169 }
1170 };
1171
1172 /* Methods */
1173
1174 /**
1175 * Get element data.
1176 *
1177 * @return {Mixed} Element data
1178 */
1179 OO.ui.Element.prototype.getData = function () {
1180 return this.data;
1181 };
1182
1183 /**
1184 * Set element data.
1185 *
1186 * @param {Mixed} Element data
1187 * @chainable
1188 */
1189 OO.ui.Element.prototype.setData = function ( data ) {
1190 this.data = data;
1191 return this;
1192 };
1193
1194 /**
1195 * Check if element supports one or more methods.
1196 *
1197 * @param {string|string[]} methods Method or list of methods to check
1198 * @return {boolean} All methods are supported
1199 */
1200 OO.ui.Element.prototype.supports = function ( methods ) {
1201 var i, len,
1202 support = 0;
1203
1204 methods = $.isArray( methods ) ? methods : [ methods ];
1205 for ( i = 0, len = methods.length; i < len; i++ ) {
1206 if ( $.isFunction( this[ methods[ i ] ] ) ) {
1207 support++;
1208 }
1209 }
1210
1211 return methods.length === support;
1212 };
1213
1214 /**
1215 * Update the theme-provided classes.
1216 *
1217 * @localdoc This is called in element mixins and widget classes any time state changes.
1218 * Updating is debounced, minimizing overhead of changing multiple attributes and
1219 * guaranteeing that theme updates do not occur within an element's constructor
1220 */
1221 OO.ui.Element.prototype.updateThemeClasses = function () {
1222 if ( !this.updateThemeClassesPending ) {
1223 this.updateThemeClassesPending = true;
1224 setTimeout( this.debouncedUpdateThemeClassesHandler );
1225 }
1226 };
1227
1228 /**
1229 * @private
1230 */
1231 OO.ui.Element.prototype.debouncedUpdateThemeClasses = function () {
1232 OO.ui.theme.updateElementClasses( this );
1233 this.updateThemeClassesPending = false;
1234 };
1235
1236 /**
1237 * Get the HTML tag name.
1238 *
1239 * Override this method to base the result on instance information.
1240 *
1241 * @return {string} HTML tag name
1242 */
1243 OO.ui.Element.prototype.getTagName = function () {
1244 return this.constructor.static.tagName;
1245 };
1246
1247 /**
1248 * Check if the element is attached to the DOM
1249 * @return {boolean} The element is attached to the DOM
1250 */
1251 OO.ui.Element.prototype.isElementAttached = function () {
1252 return $.contains( this.getElementDocument(), this.$element[ 0 ] );
1253 };
1254
1255 /**
1256 * Get the DOM document.
1257 *
1258 * @return {HTMLDocument} Document object
1259 */
1260 OO.ui.Element.prototype.getElementDocument = function () {
1261 // Don't cache this in other ways either because subclasses could can change this.$element
1262 return OO.ui.Element.static.getDocument( this.$element );
1263 };
1264
1265 /**
1266 * Get the DOM window.
1267 *
1268 * @return {Window} Window object
1269 */
1270 OO.ui.Element.prototype.getElementWindow = function () {
1271 return OO.ui.Element.static.getWindow( this.$element );
1272 };
1273
1274 /**
1275 * Get closest scrollable container.
1276 */
1277 OO.ui.Element.prototype.getClosestScrollableElementContainer = function () {
1278 return OO.ui.Element.static.getClosestScrollableContainer( this.$element[ 0 ] );
1279 };
1280
1281 /**
1282 * Get group element is in.
1283 *
1284 * @return {OO.ui.GroupElement|null} Group element, null if none
1285 */
1286 OO.ui.Element.prototype.getElementGroup = function () {
1287 return this.elementGroup;
1288 };
1289
1290 /**
1291 * Set group element is in.
1292 *
1293 * @param {OO.ui.GroupElement|null} group Group element, null if none
1294 * @chainable
1295 */
1296 OO.ui.Element.prototype.setElementGroup = function ( group ) {
1297 this.elementGroup = group;
1298 return this;
1299 };
1300
1301 /**
1302 * Scroll element into view.
1303 *
1304 * @param {Object} [config] Configuration options
1305 */
1306 OO.ui.Element.prototype.scrollElementIntoView = function ( config ) {
1307 return OO.ui.Element.static.scrollIntoView( this.$element[ 0 ], config );
1308 };
1309
1310 /**
1311 * Container for elements.
1312 *
1313 * @abstract
1314 * @class
1315 * @extends OO.ui.Element
1316 * @mixins OO.EventEmitter
1317 *
1318 * @constructor
1319 * @param {Object} [config] Configuration options
1320 */
1321 OO.ui.Layout = function OoUiLayout( config ) {
1322 // Configuration initialization
1323 config = config || {};
1324
1325 // Parent constructor
1326 OO.ui.Layout.super.call( this, config );
1327
1328 // Mixin constructors
1329 OO.EventEmitter.call( this );
1330
1331 // Initialization
1332 this.$element.addClass( 'oo-ui-layout' );
1333 };
1334
1335 /* Setup */
1336
1337 OO.inheritClass( OO.ui.Layout, OO.ui.Element );
1338 OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
1339
1340 /**
1341 * User interface control.
1342 *
1343 * @abstract
1344 * @class
1345 * @extends OO.ui.Element
1346 * @mixins OO.EventEmitter
1347 *
1348 * @constructor
1349 * @param {Object} [config] Configuration options
1350 * @cfg {boolean} [disabled=false] Disable
1351 */
1352 OO.ui.Widget = function OoUiWidget( config ) {
1353 // Initialize config
1354 config = $.extend( { disabled: false }, config );
1355
1356 // Parent constructor
1357 OO.ui.Widget.super.call( this, config );
1358
1359 // Mixin constructors
1360 OO.EventEmitter.call( this );
1361
1362 // Properties
1363 this.visible = true;
1364 this.disabled = null;
1365 this.wasDisabled = null;
1366
1367 // Initialization
1368 this.$element.addClass( 'oo-ui-widget' );
1369 this.setDisabled( !!config.disabled );
1370 };
1371
1372 /* Setup */
1373
1374 OO.inheritClass( OO.ui.Widget, OO.ui.Element );
1375 OO.mixinClass( OO.ui.Widget, OO.EventEmitter );
1376
1377 /* Events */
1378
1379 /**
1380 * @event disable
1381 * @param {boolean} disabled Widget is disabled
1382 */
1383
1384 /**
1385 * @event toggle
1386 * @param {boolean} visible Widget is visible
1387 */
1388
1389 /* Methods */
1390
1391 /**
1392 * Check if the widget is disabled.
1393 *
1394 * @return {boolean} Button is disabled
1395 */
1396 OO.ui.Widget.prototype.isDisabled = function () {
1397 return this.disabled;
1398 };
1399
1400 /**
1401 * Check if widget is visible.
1402 *
1403 * @return {boolean} Widget is visible
1404 */
1405 OO.ui.Widget.prototype.isVisible = function () {
1406 return this.visible;
1407 };
1408
1409 /**
1410 * Set the disabled state of the widget.
1411 *
1412 * This should probably change the widgets' appearance and prevent it from being used.
1413 *
1414 * @param {boolean} disabled Disable widget
1415 * @chainable
1416 */
1417 OO.ui.Widget.prototype.setDisabled = function ( disabled ) {
1418 var isDisabled;
1419
1420 this.disabled = !!disabled;
1421 isDisabled = this.isDisabled();
1422 if ( isDisabled !== this.wasDisabled ) {
1423 this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled );
1424 this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled );
1425 this.$element.attr( 'aria-disabled', isDisabled.toString() );
1426 this.emit( 'disable', isDisabled );
1427 this.updateThemeClasses();
1428 }
1429 this.wasDisabled = isDisabled;
1430
1431 return this;
1432 };
1433
1434 /**
1435 * Toggle visibility of widget.
1436 *
1437 * @param {boolean} [show] Make widget visible, omit to toggle visibility
1438 * @fires visible
1439 * @chainable
1440 */
1441 OO.ui.Widget.prototype.toggle = function ( show ) {
1442 show = show === undefined ? !this.visible : !!show;
1443
1444 if ( show !== this.isVisible() ) {
1445 this.visible = show;
1446 this.$element.toggleClass( 'oo-ui-element-hidden', !this.visible );
1447 this.emit( 'toggle', show );
1448 }
1449
1450 return this;
1451 };
1452
1453 /**
1454 * Update the disabled state, in case of changes in parent widget.
1455 *
1456 * @chainable
1457 */
1458 OO.ui.Widget.prototype.updateDisabled = function () {
1459 this.setDisabled( this.disabled );
1460 return this;
1461 };
1462
1463 /**
1464 * Encapsulation of an user interface.
1465 *
1466 * Use together with OO.ui.WindowManager.
1467 *
1468 * @abstract
1469 * @class
1470 * @extends OO.ui.Element
1471 * @mixins OO.EventEmitter
1472 *
1473 * When a window is opened, the setup and ready processes are executed. Similarly, the hold and
1474 * teardown processes are executed when the window is closed.
1475 *
1476 * - {@link OO.ui.WindowManager#openWindow} or {@link #open} methods are used to start opening
1477 * - Window manager begins opening window
1478 * - {@link #getSetupProcess} method is called and its result executed
1479 * - {@link #getReadyProcess} method is called and its result executed
1480 * - Window is now open
1481 *
1482 * - {@link OO.ui.WindowManager#closeWindow} or {@link #close} methods are used to start closing
1483 * - Window manager begins closing window
1484 * - {@link #getHoldProcess} method is called and its result executed
1485 * - {@link #getTeardownProcess} method is called and its result executed
1486 * - Window is now closed
1487 *
1488 * Each process (setup, ready, hold and teardown) can be extended in subclasses by overriding
1489 * {@link #getSetupProcess}, {@link #getReadyProcess}, {@link #getHoldProcess} and
1490 * {@link #getTeardownProcess} respectively. Each process is executed in series, so asynchronous
1491 * processing can complete. Always assume window processes are executed asynchronously. See
1492 * OO.ui.Process for more details about how to work with processes. Some events, as well as the
1493 * #open and #close methods, provide promises which are resolved when the window enters a new state.
1494 *
1495 * Sizing of windows is specified using symbolic names which are interpreted by the window manager.
1496 * If the requested size is not recognized, the window manager will choose a sensible fallback.
1497 *
1498 * @constructor
1499 * @param {Object} [config] Configuration options
1500 * @cfg {string} [size] Symbolic name of dialog size, `small`, `medium`, `large`, `larger` or
1501 * `full`; omit to use #static-size
1502 */
1503 OO.ui.Window = function OoUiWindow( config ) {
1504 // Configuration initialization
1505 config = config || {};
1506
1507 // Parent constructor
1508 OO.ui.Window.super.call( this, config );
1509
1510 // Mixin constructors
1511 OO.EventEmitter.call( this );
1512
1513 // Properties
1514 this.manager = null;
1515 this.size = config.size || this.constructor.static.size;
1516 this.$frame = $( '<div>' );
1517 this.$overlay = $( '<div>' );
1518 this.$content = $( '<div>' );
1519
1520 // Initialization
1521 this.$overlay.addClass( 'oo-ui-window-overlay' );
1522 this.$content
1523 .addClass( 'oo-ui-window-content' )
1524 .attr( 'tabIndex', 0 );
1525 this.$frame
1526 .addClass( 'oo-ui-window-frame' )
1527 .append( this.$content );
1528
1529 this.$element
1530 .addClass( 'oo-ui-window' )
1531 .append( this.$frame, this.$overlay );
1532
1533 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
1534 // that reference properties not initialized at that time of parent class construction
1535 // TODO: Find a better way to handle post-constructor setup
1536 this.visible = false;
1537 this.$element.addClass( 'oo-ui-element-hidden' );
1538 };
1539
1540 /* Setup */
1541
1542 OO.inheritClass( OO.ui.Window, OO.ui.Element );
1543 OO.mixinClass( OO.ui.Window, OO.EventEmitter );
1544
1545 /* Static Properties */
1546
1547 /**
1548 * Symbolic name of size.
1549 *
1550 * Size is used if no size is configured during construction.
1551 *
1552 * @static
1553 * @inheritable
1554 * @property {string}
1555 */
1556 OO.ui.Window.static.size = 'medium';
1557
1558 /* Methods */
1559
1560 /**
1561 * Handle mouse down events.
1562 *
1563 * @param {jQuery.Event} e Mouse down event
1564 */
1565 OO.ui.Window.prototype.onMouseDown = function ( e ) {
1566 // Prevent clicking on the click-block from stealing focus
1567 if ( e.target === this.$element[ 0 ] ) {
1568 return false;
1569 }
1570 };
1571
1572 /**
1573 * Check if window has been initialized.
1574 *
1575 * Initialization occurs when a window is added to a manager.
1576 *
1577 * @return {boolean} Window has been initialized
1578 */
1579 OO.ui.Window.prototype.isInitialized = function () {
1580 return !!this.manager;
1581 };
1582
1583 /**
1584 * Check if window is visible.
1585 *
1586 * @return {boolean} Window is visible
1587 */
1588 OO.ui.Window.prototype.isVisible = function () {
1589 return this.visible;
1590 };
1591
1592 /**
1593 * Check if window is opening.
1594 *
1595 * This is a wrapper around OO.ui.WindowManager#isOpening.
1596 *
1597 * @return {boolean} Window is opening
1598 */
1599 OO.ui.Window.prototype.isOpening = function () {
1600 return this.manager.isOpening( this );
1601 };
1602
1603 /**
1604 * Check if window is closing.
1605 *
1606 * This is a wrapper around OO.ui.WindowManager#isClosing.
1607 *
1608 * @return {boolean} Window is closing
1609 */
1610 OO.ui.Window.prototype.isClosing = function () {
1611 return this.manager.isClosing( this );
1612 };
1613
1614 /**
1615 * Check if window is opened.
1616 *
1617 * This is a wrapper around OO.ui.WindowManager#isOpened.
1618 *
1619 * @return {boolean} Window is opened
1620 */
1621 OO.ui.Window.prototype.isOpened = function () {
1622 return this.manager.isOpened( this );
1623 };
1624
1625 /**
1626 * Get the window manager.
1627 *
1628 * @return {OO.ui.WindowManager} Manager of window
1629 */
1630 OO.ui.Window.prototype.getManager = function () {
1631 return this.manager;
1632 };
1633
1634 /**
1635 * Get the window size.
1636 *
1637 * @return {string} Symbolic size name, e.g. `small`, `medium`, `large`, `larger`, `full`
1638 */
1639 OO.ui.Window.prototype.getSize = function () {
1640 return this.size;
1641 };
1642
1643 /**
1644 * Disable transitions on window's frame for the duration of the callback function, then enable them
1645 * back.
1646 *
1647 * @private
1648 * @param {Function} callback Function to call while transitions are disabled
1649 */
1650 OO.ui.Window.prototype.withoutSizeTransitions = function ( callback ) {
1651 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
1652 // Disable transitions first, otherwise we'll get values from when the window was animating.
1653 var oldTransition,
1654 styleObj = this.$frame[ 0 ].style;
1655 oldTransition = styleObj.transition || styleObj.OTransition || styleObj.MsTransition ||
1656 styleObj.MozTransition || styleObj.WebkitTransition;
1657 styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
1658 styleObj.MozTransition = styleObj.WebkitTransition = 'none';
1659 callback();
1660 // Force reflow to make sure the style changes done inside callback really are not transitioned
1661 this.$frame.height();
1662 styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
1663 styleObj.MozTransition = styleObj.WebkitTransition = oldTransition;
1664 };
1665
1666 /**
1667 * Get the height of the dialog contents.
1668 *
1669 * @return {number} Content height
1670 */
1671 OO.ui.Window.prototype.getContentHeight = function () {
1672 var bodyHeight,
1673 win = this,
1674 bodyStyleObj = this.$body[ 0 ].style,
1675 frameStyleObj = this.$frame[ 0 ].style;
1676
1677 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
1678 // Disable transitions first, otherwise we'll get values from when the window was animating.
1679 this.withoutSizeTransitions( function () {
1680 var oldHeight = frameStyleObj.height,
1681 oldPosition = bodyStyleObj.position;
1682 frameStyleObj.height = '1px';
1683 // Force body to resize to new width
1684 bodyStyleObj.position = 'relative';
1685 bodyHeight = win.getBodyHeight();
1686 frameStyleObj.height = oldHeight;
1687 bodyStyleObj.position = oldPosition;
1688 } );
1689
1690 return (
1691 // Add buffer for border
1692 ( this.$frame.outerHeight() - this.$frame.innerHeight() ) +
1693 // Use combined heights of children
1694 ( this.$head.outerHeight( true ) + bodyHeight + this.$foot.outerHeight( true ) )
1695 );
1696 };
1697
1698 /**
1699 * Get the height of the dialog contents.
1700 *
1701 * When this function is called, the dialog will temporarily have been resized
1702 * to height=1px, so .scrollHeight measurements can be taken accurately.
1703 *
1704 * @return {number} Height of content
1705 */
1706 OO.ui.Window.prototype.getBodyHeight = function () {
1707 return this.$body[ 0 ].scrollHeight;
1708 };
1709
1710 /**
1711 * Get the directionality of the frame
1712 *
1713 * @return {string} Directionality, 'ltr' or 'rtl'
1714 */
1715 OO.ui.Window.prototype.getDir = function () {
1716 return this.dir;
1717 };
1718
1719 /**
1720 * Get a process for setting up a window for use.
1721 *
1722 * Each time the window is opened this process will set it up for use in a particular context, based
1723 * on the `data` argument.
1724 *
1725 * When you override this method, you can add additional setup steps to the process the parent
1726 * method provides using the 'first' and 'next' methods.
1727 *
1728 * @abstract
1729 * @param {Object} [data] Window opening data
1730 * @return {OO.ui.Process} Setup process
1731 */
1732 OO.ui.Window.prototype.getSetupProcess = function () {
1733 return new OO.ui.Process();
1734 };
1735
1736 /**
1737 * Get a process for readying a window for use.
1738 *
1739 * Each time the window is open and setup, this process will ready it up for use in a particular
1740 * context, based on the `data` argument.
1741 *
1742 * When you override this method, you can add additional setup steps to the process the parent
1743 * method provides using the 'first' and 'next' methods.
1744 *
1745 * @abstract
1746 * @param {Object} [data] Window opening data
1747 * @return {OO.ui.Process} Setup process
1748 */
1749 OO.ui.Window.prototype.getReadyProcess = function () {
1750 return new OO.ui.Process();
1751 };
1752
1753 /**
1754 * Get a process for holding a window from use.
1755 *
1756 * Each time the window is closed, this process will hold it from use in a particular context, based
1757 * on the `data` argument.
1758 *
1759 * When you override this method, you can add additional setup steps to the process the parent
1760 * method provides using the 'first' and 'next' methods.
1761 *
1762 * @abstract
1763 * @param {Object} [data] Window closing data
1764 * @return {OO.ui.Process} Hold process
1765 */
1766 OO.ui.Window.prototype.getHoldProcess = function () {
1767 return new OO.ui.Process();
1768 };
1769
1770 /**
1771 * Get a process for tearing down a window after use.
1772 *
1773 * Each time the window is closed this process will tear it down and do something with the user's
1774 * interactions within the window, based on the `data` argument.
1775 *
1776 * When you override this method, you can add additional teardown steps to the process the parent
1777 * method provides using the 'first' and 'next' methods.
1778 *
1779 * @abstract
1780 * @param {Object} [data] Window closing data
1781 * @return {OO.ui.Process} Teardown process
1782 */
1783 OO.ui.Window.prototype.getTeardownProcess = function () {
1784 return new OO.ui.Process();
1785 };
1786
1787 /**
1788 * Toggle visibility of window.
1789 *
1790 * @param {boolean} [show] Make window visible, omit to toggle visibility
1791 * @fires toggle
1792 * @chainable
1793 */
1794 OO.ui.Window.prototype.toggle = function ( show ) {
1795 show = show === undefined ? !this.visible : !!show;
1796
1797 if ( show !== this.isVisible() ) {
1798 this.visible = show;
1799 this.$element.toggleClass( 'oo-ui-element-hidden', !this.visible );
1800 this.emit( 'toggle', show );
1801 }
1802
1803 return this;
1804 };
1805
1806 /**
1807 * Set the window manager.
1808 *
1809 * This will cause the window to initialize. Calling it more than once will cause an error.
1810 *
1811 * @param {OO.ui.WindowManager} manager Manager for this window
1812 * @throws {Error} If called more than once
1813 * @chainable
1814 */
1815 OO.ui.Window.prototype.setManager = function ( manager ) {
1816 if ( this.manager ) {
1817 throw new Error( 'Cannot set window manager, window already has a manager' );
1818 }
1819
1820 this.manager = manager;
1821 this.initialize();
1822
1823 return this;
1824 };
1825
1826 /**
1827 * Set the window size.
1828 *
1829 * @param {string} size Symbolic size name, e.g. 'small', 'medium', 'large', 'full'
1830 * @chainable
1831 */
1832 OO.ui.Window.prototype.setSize = function ( size ) {
1833 this.size = size;
1834 this.updateSize();
1835 return this;
1836 };
1837
1838 /**
1839 * Update the window size.
1840 *
1841 * @throws {Error} If not attached to a manager
1842 * @chainable
1843 */
1844 OO.ui.Window.prototype.updateSize = function () {
1845 if ( !this.manager ) {
1846 throw new Error( 'Cannot update window size, must be attached to a manager' );
1847 }
1848
1849 this.manager.updateWindowSize( this );
1850
1851 return this;
1852 };
1853
1854 /**
1855 * Set window dimensions.
1856 *
1857 * Properties are applied to the frame container.
1858 *
1859 * @param {Object} dim CSS dimension properties
1860 * @param {string|number} [dim.width] Width
1861 * @param {string|number} [dim.minWidth] Minimum width
1862 * @param {string|number} [dim.maxWidth] Maximum width
1863 * @param {string|number} [dim.width] Height, omit to set based on height of contents
1864 * @param {string|number} [dim.minWidth] Minimum height
1865 * @param {string|number} [dim.maxWidth] Maximum height
1866 * @chainable
1867 */
1868 OO.ui.Window.prototype.setDimensions = function ( dim ) {
1869 var height,
1870 win = this,
1871 styleObj = this.$frame[ 0 ].style;
1872
1873 // Calculate the height we need to set using the correct width
1874 if ( dim.height === undefined ) {
1875 this.withoutSizeTransitions( function () {
1876 var oldWidth = styleObj.width;
1877 win.$frame.css( 'width', dim.width || '' );
1878 height = win.getContentHeight();
1879 styleObj.width = oldWidth;
1880 } );
1881 } else {
1882 height = dim.height;
1883 }
1884
1885 this.$frame.css( {
1886 width: dim.width || '',
1887 minWidth: dim.minWidth || '',
1888 maxWidth: dim.maxWidth || '',
1889 height: height || '',
1890 minHeight: dim.minHeight || '',
1891 maxHeight: dim.maxHeight || ''
1892 } );
1893
1894 return this;
1895 };
1896
1897 /**
1898 * Initialize window contents.
1899 *
1900 * The first time the window is opened, #initialize is called so that changes to the window that
1901 * will persist between openings can be made. See #getSetupProcess for a way to make changes each
1902 * time the window opens.
1903 *
1904 * @throws {Error} If not attached to a manager
1905 * @chainable
1906 */
1907 OO.ui.Window.prototype.initialize = function () {
1908 if ( !this.manager ) {
1909 throw new Error( 'Cannot initialize window, must be attached to a manager' );
1910 }
1911
1912 // Properties
1913 this.$head = $( '<div>' );
1914 this.$body = $( '<div>' );
1915 this.$foot = $( '<div>' );
1916 this.$innerOverlay = $( '<div>' );
1917 this.dir = OO.ui.Element.static.getDir( this.$content ) || 'ltr';
1918 this.$document = $( this.getElementDocument() );
1919
1920 // Events
1921 this.$element.on( 'mousedown', this.onMouseDown.bind( this ) );
1922
1923 // Initialization
1924 this.$head.addClass( 'oo-ui-window-head' );
1925 this.$body.addClass( 'oo-ui-window-body' );
1926 this.$foot.addClass( 'oo-ui-window-foot' );
1927 this.$innerOverlay.addClass( 'oo-ui-window-inner-overlay' );
1928 this.$content.append( this.$head, this.$body, this.$foot, this.$innerOverlay );
1929
1930 return this;
1931 };
1932
1933 /**
1934 * Open window.
1935 *
1936 * This is a wrapper around calling {@link OO.ui.WindowManager#openWindow} on the window manager.
1937 * To do something each time the window opens, use #getSetupProcess or #getReadyProcess.
1938 *
1939 * @param {Object} [data] Window opening data
1940 * @return {jQuery.Promise} Promise resolved when window is opened; when the promise is resolved the
1941 * first argument will be a promise which will be resolved when the window begins closing
1942 * @throws {Error} If not attached to a manager
1943 */
1944 OO.ui.Window.prototype.open = function ( data ) {
1945 if ( !this.manager ) {
1946 throw new Error( 'Cannot open window, must be attached to a manager' );
1947 }
1948
1949 return this.manager.openWindow( this, data );
1950 };
1951
1952 /**
1953 * Close window.
1954 *
1955 * This is a wrapper around calling OO.ui.WindowManager#closeWindow on the window manager.
1956 * To do something each time the window closes, use #getHoldProcess or #getTeardownProcess.
1957 *
1958 * @param {Object} [data] Window closing data
1959 * @return {jQuery.Promise} Promise resolved when window is closed
1960 * @throws {Error} If not attached to a manager
1961 */
1962 OO.ui.Window.prototype.close = function ( data ) {
1963 if ( !this.manager ) {
1964 throw new Error( 'Cannot close window, must be attached to a manager' );
1965 }
1966
1967 return this.manager.closeWindow( this, data );
1968 };
1969
1970 /**
1971 * Setup window.
1972 *
1973 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
1974 * by other systems.
1975 *
1976 * @param {Object} [data] Window opening data
1977 * @return {jQuery.Promise} Promise resolved when window is setup
1978 */
1979 OO.ui.Window.prototype.setup = function ( data ) {
1980 var win = this,
1981 deferred = $.Deferred();
1982
1983 this.toggle( true );
1984
1985 this.getSetupProcess( data ).execute().done( function () {
1986 // Force redraw by asking the browser to measure the elements' widths
1987 win.$element.addClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
1988 win.$content.addClass( 'oo-ui-window-content-setup' ).width();
1989 deferred.resolve();
1990 } );
1991
1992 return deferred.promise();
1993 };
1994
1995 /**
1996 * Ready window.
1997 *
1998 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
1999 * by other systems.
2000 *
2001 * @param {Object} [data] Window opening data
2002 * @return {jQuery.Promise} Promise resolved when window is ready
2003 */
2004 OO.ui.Window.prototype.ready = function ( data ) {
2005 var win = this,
2006 deferred = $.Deferred();
2007
2008 this.$content.focus();
2009 this.getReadyProcess( data ).execute().done( function () {
2010 // Force redraw by asking the browser to measure the elements' widths
2011 win.$element.addClass( 'oo-ui-window-ready' ).width();
2012 win.$content.addClass( 'oo-ui-window-content-ready' ).width();
2013 deferred.resolve();
2014 } );
2015
2016 return deferred.promise();
2017 };
2018
2019 /**
2020 * Hold window.
2021 *
2022 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2023 * by other systems.
2024 *
2025 * @param {Object} [data] Window closing data
2026 * @return {jQuery.Promise} Promise resolved when window is held
2027 */
2028 OO.ui.Window.prototype.hold = function ( data ) {
2029 var win = this,
2030 deferred = $.Deferred();
2031
2032 this.getHoldProcess( data ).execute().done( function () {
2033 // Get the focused element within the window's content
2034 var $focus = win.$content.find( OO.ui.Element.static.getDocument( win.$content ).activeElement );
2035
2036 // Blur the focused element
2037 if ( $focus.length ) {
2038 $focus[ 0 ].blur();
2039 }
2040
2041 // Force redraw by asking the browser to measure the elements' widths
2042 win.$element.removeClass( 'oo-ui-window-ready' ).width();
2043 win.$content.removeClass( 'oo-ui-window-content-ready' ).width();
2044 deferred.resolve();
2045 } );
2046
2047 return deferred.promise();
2048 };
2049
2050 /**
2051 * Teardown window.
2052 *
2053 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2054 * by other systems.
2055 *
2056 * @param {Object} [data] Window closing data
2057 * @return {jQuery.Promise} Promise resolved when window is torn down
2058 */
2059 OO.ui.Window.prototype.teardown = function ( data ) {
2060 var win = this;
2061
2062 return this.getTeardownProcess( data ).execute()
2063 .done( function () {
2064 // Force redraw by asking the browser to measure the elements' widths
2065 win.$element.removeClass( 'oo-ui-window-active oo-ui-window-setup' ).width();
2066 win.$content.removeClass( 'oo-ui-window-content-setup' ).width();
2067 win.toggle( false );
2068 } );
2069 };
2070
2071 /**
2072 * Base class for all dialogs.
2073 *
2074 * Logic:
2075 * - Manage the window (open and close, etc.).
2076 * - Store the internal name and display title.
2077 * - A stack to track one or more pending actions.
2078 * - Manage a set of actions that can be performed.
2079 * - Configure and create action widgets.
2080 *
2081 * User interface:
2082 * - Close the dialog with Escape key.
2083 * - Visually lock the dialog while an action is in
2084 * progress (aka "pending").
2085 *
2086 * Subclass responsibilities:
2087 * - Display the title somewhere.
2088 * - Add content to the dialog.
2089 * - Provide a UI to close the dialog.
2090 * - Display the action widgets somewhere.
2091 *
2092 * @abstract
2093 * @class
2094 * @extends OO.ui.Window
2095 * @mixins OO.ui.PendingElement
2096 *
2097 * @constructor
2098 * @param {Object} [config] Configuration options
2099 */
2100 OO.ui.Dialog = function OoUiDialog( config ) {
2101 // Parent constructor
2102 OO.ui.Dialog.super.call( this, config );
2103
2104 // Mixin constructors
2105 OO.ui.PendingElement.call( this );
2106
2107 // Properties
2108 this.actions = new OO.ui.ActionSet();
2109 this.attachedActions = [];
2110 this.currentAction = null;
2111 this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
2112
2113 // Events
2114 this.actions.connect( this, {
2115 click: 'onActionClick',
2116 resize: 'onActionResize',
2117 change: 'onActionsChange'
2118 } );
2119
2120 // Initialization
2121 this.$element
2122 .addClass( 'oo-ui-dialog' )
2123 .attr( 'role', 'dialog' );
2124 };
2125
2126 /* Setup */
2127
2128 OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
2129 OO.mixinClass( OO.ui.Dialog, OO.ui.PendingElement );
2130
2131 /* Static Properties */
2132
2133 /**
2134 * Symbolic name of dialog.
2135 *
2136 * @abstract
2137 * @static
2138 * @inheritable
2139 * @property {string}
2140 */
2141 OO.ui.Dialog.static.name = '';
2142
2143 /**
2144 * Dialog title.
2145 *
2146 * @abstract
2147 * @static
2148 * @inheritable
2149 * @property {jQuery|string|Function} Label nodes, text or a function that returns nodes or text
2150 */
2151 OO.ui.Dialog.static.title = '';
2152
2153 /**
2154 * List of OO.ui.ActionWidget configuration options.
2155 *
2156 * @static
2157 * inheritable
2158 * @property {Object[]}
2159 */
2160 OO.ui.Dialog.static.actions = [];
2161
2162 /**
2163 * Close dialog when the escape key is pressed.
2164 *
2165 * @static
2166 * @abstract
2167 * @inheritable
2168 * @property {boolean}
2169 */
2170 OO.ui.Dialog.static.escapable = true;
2171
2172 /* Methods */
2173
2174 /**
2175 * Handle frame document key down events.
2176 *
2177 * @param {jQuery.Event} e Key down event
2178 */
2179 OO.ui.Dialog.prototype.onDocumentKeyDown = function ( e ) {
2180 if ( e.which === OO.ui.Keys.ESCAPE ) {
2181 this.close();
2182 e.preventDefault();
2183 e.stopPropagation();
2184 }
2185 };
2186
2187 /**
2188 * Handle action resized events.
2189 *
2190 * @param {OO.ui.ActionWidget} action Action that was resized
2191 */
2192 OO.ui.Dialog.prototype.onActionResize = function () {
2193 // Override in subclass
2194 };
2195
2196 /**
2197 * Handle action click events.
2198 *
2199 * @param {OO.ui.ActionWidget} action Action that was clicked
2200 */
2201 OO.ui.Dialog.prototype.onActionClick = function ( action ) {
2202 if ( !this.isPending() ) {
2203 this.currentAction = action;
2204 this.executeAction( action.getAction() );
2205 }
2206 };
2207
2208 /**
2209 * Handle actions change event.
2210 */
2211 OO.ui.Dialog.prototype.onActionsChange = function () {
2212 this.detachActions();
2213 if ( !this.isClosing() ) {
2214 this.attachActions();
2215 }
2216 };
2217
2218 /**
2219 * Get set of actions.
2220 *
2221 * @return {OO.ui.ActionSet}
2222 */
2223 OO.ui.Dialog.prototype.getActions = function () {
2224 return this.actions;
2225 };
2226
2227 /**
2228 * Get a process for taking action.
2229 *
2230 * When you override this method, you can add additional accept steps to the process the parent
2231 * method provides using the 'first' and 'next' methods.
2232 *
2233 * @abstract
2234 * @param {string} [action] Symbolic name of action
2235 * @return {OO.ui.Process} Action process
2236 */
2237 OO.ui.Dialog.prototype.getActionProcess = function ( action ) {
2238 return new OO.ui.Process()
2239 .next( function () {
2240 if ( !action ) {
2241 // An empty action always closes the dialog without data, which should always be
2242 // safe and make no changes
2243 this.close();
2244 }
2245 }, this );
2246 };
2247
2248 /**
2249 * @inheritdoc
2250 *
2251 * @param {Object} [data] Dialog opening data
2252 * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use #static-title
2253 * @param {Object[]} [data.actions] List of OO.ui.ActionWidget configuration options for each
2254 * action item, omit to use #static-actions
2255 */
2256 OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
2257 data = data || {};
2258
2259 // Parent method
2260 return OO.ui.Dialog.super.prototype.getSetupProcess.call( this, data )
2261 .next( function () {
2262 var i, len,
2263 items = [],
2264 config = this.constructor.static,
2265 actions = data.actions !== undefined ? data.actions : config.actions;
2266
2267 this.title.setLabel(
2268 data.title !== undefined ? data.title : this.constructor.static.title
2269 );
2270 for ( i = 0, len = actions.length; i < len; i++ ) {
2271 items.push(
2272 new OO.ui.ActionWidget( actions[ i ] )
2273 );
2274 }
2275 this.actions.add( items );
2276
2277 if ( this.constructor.static.escapable ) {
2278 this.$document.on( 'keydown', this.onDocumentKeyDownHandler );
2279 }
2280 }, this );
2281 };
2282
2283 /**
2284 * @inheritdoc
2285 */
2286 OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
2287 // Parent method
2288 return OO.ui.Dialog.super.prototype.getTeardownProcess.call( this, data )
2289 .first( function () {
2290 if ( this.constructor.static.escapable ) {
2291 this.$document.off( 'keydown', this.onDocumentKeyDownHandler );
2292 }
2293
2294 this.actions.clear();
2295 this.currentAction = null;
2296 }, this );
2297 };
2298
2299 /**
2300 * @inheritdoc
2301 */
2302 OO.ui.Dialog.prototype.initialize = function () {
2303 // Parent method
2304 OO.ui.Dialog.super.prototype.initialize.call( this );
2305
2306 // Properties
2307 this.title = new OO.ui.LabelWidget();
2308
2309 // Initialization
2310 this.$content.addClass( 'oo-ui-dialog-content' );
2311 this.setPendingElement( this.$head );
2312 };
2313
2314 /**
2315 * Attach action actions.
2316 */
2317 OO.ui.Dialog.prototype.attachActions = function () {
2318 // Remember the list of potentially attached actions
2319 this.attachedActions = this.actions.get();
2320 };
2321
2322 /**
2323 * Detach action actions.
2324 *
2325 * @chainable
2326 */
2327 OO.ui.Dialog.prototype.detachActions = function () {
2328 var i, len;
2329
2330 // Detach all actions that may have been previously attached
2331 for ( i = 0, len = this.attachedActions.length; i < len; i++ ) {
2332 this.attachedActions[ i ].$element.detach();
2333 }
2334 this.attachedActions = [];
2335 };
2336
2337 /**
2338 * Execute an action.
2339 *
2340 * @param {string} action Symbolic name of action to execute
2341 * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
2342 */
2343 OO.ui.Dialog.prototype.executeAction = function ( action ) {
2344 this.pushPending();
2345 return this.getActionProcess( action ).execute()
2346 .always( this.popPending.bind( this ) );
2347 };
2348
2349 /**
2350 * Collection of windows.
2351 *
2352 * @class
2353 * @extends OO.ui.Element
2354 * @mixins OO.EventEmitter
2355 *
2356 * Managed windows are mutually exclusive. If a window is opened while there is a current window
2357 * already opening or opened, the current window will be closed without data. Empty closing data
2358 * should always result in the window being closed without causing constructive or destructive
2359 * action.
2360 *
2361 * As a window is opened and closed, it passes through several stages and the manager emits several
2362 * corresponding events.
2363 *
2364 * - {@link #openWindow} or {@link OO.ui.Window#open} methods are used to start opening
2365 * - {@link #event-opening} is emitted with `opening` promise
2366 * - {@link #getSetupDelay} is called the returned value is used to time a pause in execution
2367 * - {@link OO.ui.Window#getSetupProcess} method is called on the window and its result executed
2368 * - `setup` progress notification is emitted from opening promise
2369 * - {@link #getReadyDelay} is called the returned value is used to time a pause in execution
2370 * - {@link OO.ui.Window#getReadyProcess} method is called on the window and its result executed
2371 * - `ready` progress notification is emitted from opening promise
2372 * - `opening` promise is resolved with `opened` promise
2373 * - Window is now open
2374 *
2375 * - {@link #closeWindow} or {@link OO.ui.Window#close} methods are used to start closing
2376 * - `opened` promise is resolved with `closing` promise
2377 * - {@link #event-closing} is emitted with `closing` promise
2378 * - {@link #getHoldDelay} is called the returned value is used to time a pause in execution
2379 * - {@link OO.ui.Window#getHoldProcess} method is called on the window and its result executed
2380 * - `hold` progress notification is emitted from opening promise
2381 * - {@link #getTeardownDelay} is called the returned value is used to time a pause in execution
2382 * - {@link OO.ui.Window#getTeardownProcess} method is called on the window and its result executed
2383 * - `teardown` progress notification is emitted from opening promise
2384 * - Closing promise is resolved
2385 * - Window is now closed
2386 *
2387 * @constructor
2388 * @param {Object} [config] Configuration options
2389 * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
2390 * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
2391 */
2392 OO.ui.WindowManager = function OoUiWindowManager( config ) {
2393 // Configuration initialization
2394 config = config || {};
2395
2396 // Parent constructor
2397 OO.ui.WindowManager.super.call( this, config );
2398
2399 // Mixin constructors
2400 OO.EventEmitter.call( this );
2401
2402 // Properties
2403 this.factory = config.factory;
2404 this.modal = config.modal === undefined || !!config.modal;
2405 this.windows = {};
2406 this.opening = null;
2407 this.opened = null;
2408 this.closing = null;
2409 this.preparingToOpen = null;
2410 this.preparingToClose = null;
2411 this.currentWindow = null;
2412 this.$ariaHidden = null;
2413 this.onWindowResizeTimeout = null;
2414 this.onWindowResizeHandler = this.onWindowResize.bind( this );
2415 this.afterWindowResizeHandler = this.afterWindowResize.bind( this );
2416
2417 // Initialization
2418 this.$element
2419 .addClass( 'oo-ui-windowManager' )
2420 .toggleClass( 'oo-ui-windowManager-modal', this.modal );
2421 };
2422
2423 /* Setup */
2424
2425 OO.inheritClass( OO.ui.WindowManager, OO.ui.Element );
2426 OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter );
2427
2428 /* Events */
2429
2430 /**
2431 * Window is opening.
2432 *
2433 * Fired when the window begins to be opened.
2434 *
2435 * @event opening
2436 * @param {OO.ui.Window} win Window that's being opened
2437 * @param {jQuery.Promise} opening Promise resolved when window is opened; when the promise is
2438 * resolved the first argument will be a promise which will be resolved when the window begins
2439 * closing, the second argument will be the opening data; progress notifications will be fired on
2440 * the promise for `setup` and `ready` when those processes are completed respectively.
2441 * @param {Object} data Window opening data
2442 */
2443
2444 /**
2445 * Window is closing.
2446 *
2447 * Fired when the window begins to be closed.
2448 *
2449 * @event closing
2450 * @param {OO.ui.Window} win Window that's being closed
2451 * @param {jQuery.Promise} opening Promise resolved when window is closed; when the promise
2452 * is resolved the first argument will be a the closing data; progress notifications will be fired
2453 * on the promise for `hold` and `teardown` when those processes are completed respectively.
2454 * @param {Object} data Window closing data
2455 */
2456
2457 /**
2458 * Window was resized.
2459 *
2460 * @event resize
2461 * @param {OO.ui.Window} win Window that was resized
2462 */
2463
2464 /* Static Properties */
2465
2466 /**
2467 * Map of symbolic size names and CSS properties.
2468 *
2469 * @static
2470 * @inheritable
2471 * @property {Object}
2472 */
2473 OO.ui.WindowManager.static.sizes = {
2474 small: {
2475 width: 300
2476 },
2477 medium: {
2478 width: 500
2479 },
2480 large: {
2481 width: 700
2482 },
2483 larger: {
2484 width: 900
2485 },
2486 full: {
2487 // These can be non-numeric because they are never used in calculations
2488 width: '100%',
2489 height: '100%'
2490 }
2491 };
2492
2493 /**
2494 * Symbolic name of default size.
2495 *
2496 * Default size is used if the window's requested size is not recognized.
2497 *
2498 * @static
2499 * @inheritable
2500 * @property {string}
2501 */
2502 OO.ui.WindowManager.static.defaultSize = 'medium';
2503
2504 /* Methods */
2505
2506 /**
2507 * Handle window resize events.
2508 *
2509 * @param {jQuery.Event} e Window resize event
2510 */
2511 OO.ui.WindowManager.prototype.onWindowResize = function () {
2512 clearTimeout( this.onWindowResizeTimeout );
2513 this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 );
2514 };
2515
2516 /**
2517 * Handle window resize events.
2518 *
2519 * @param {jQuery.Event} e Window resize event
2520 */
2521 OO.ui.WindowManager.prototype.afterWindowResize = function () {
2522 if ( this.currentWindow ) {
2523 this.updateWindowSize( this.currentWindow );
2524 }
2525 };
2526
2527 /**
2528 * Check if window is opening.
2529 *
2530 * @return {boolean} Window is opening
2531 */
2532 OO.ui.WindowManager.prototype.isOpening = function ( win ) {
2533 return win === this.currentWindow && !!this.opening && this.opening.state() === 'pending';
2534 };
2535
2536 /**
2537 * Check if window is closing.
2538 *
2539 * @return {boolean} Window is closing
2540 */
2541 OO.ui.WindowManager.prototype.isClosing = function ( win ) {
2542 return win === this.currentWindow && !!this.closing && this.closing.state() === 'pending';
2543 };
2544
2545 /**
2546 * Check if window is opened.
2547 *
2548 * @return {boolean} Window is opened
2549 */
2550 OO.ui.WindowManager.prototype.isOpened = function ( win ) {
2551 return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending';
2552 };
2553
2554 /**
2555 * Check if a window is being managed.
2556 *
2557 * @param {OO.ui.Window} win Window to check
2558 * @return {boolean} Window is being managed
2559 */
2560 OO.ui.WindowManager.prototype.hasWindow = function ( win ) {
2561 var name;
2562
2563 for ( name in this.windows ) {
2564 if ( this.windows[ name ] === win ) {
2565 return true;
2566 }
2567 }
2568
2569 return false;
2570 };
2571
2572 /**
2573 * Get the number of milliseconds to wait between beginning opening and executing setup process.
2574 *
2575 * @param {OO.ui.Window} win Window being opened
2576 * @param {Object} [data] Window opening data
2577 * @return {number} Milliseconds to wait
2578 */
2579 OO.ui.WindowManager.prototype.getSetupDelay = function () {
2580 return 0;
2581 };
2582
2583 /**
2584 * Get the number of milliseconds to wait between finishing setup and executing ready process.
2585 *
2586 * @param {OO.ui.Window} win Window being opened
2587 * @param {Object} [data] Window opening data
2588 * @return {number} Milliseconds to wait
2589 */
2590 OO.ui.WindowManager.prototype.getReadyDelay = function () {
2591 return 0;
2592 };
2593
2594 /**
2595 * Get the number of milliseconds to wait between beginning closing and executing hold process.
2596 *
2597 * @param {OO.ui.Window} win Window being closed
2598 * @param {Object} [data] Window closing data
2599 * @return {number} Milliseconds to wait
2600 */
2601 OO.ui.WindowManager.prototype.getHoldDelay = function () {
2602 return 0;
2603 };
2604
2605 /**
2606 * Get the number of milliseconds to wait between finishing hold and executing teardown process.
2607 *
2608 * @param {OO.ui.Window} win Window being closed
2609 * @param {Object} [data] Window closing data
2610 * @return {number} Milliseconds to wait
2611 */
2612 OO.ui.WindowManager.prototype.getTeardownDelay = function () {
2613 return this.modal ? 250 : 0;
2614 };
2615
2616 /**
2617 * Get managed window by symbolic name.
2618 *
2619 * If window is not yet instantiated, it will be instantiated and added automatically.
2620 *
2621 * @param {string} name Symbolic window name
2622 * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
2623 * @throws {Error} If the symbolic name is unrecognized by the factory
2624 * @throws {Error} If the symbolic name unrecognized as a managed window
2625 */
2626 OO.ui.WindowManager.prototype.getWindow = function ( name ) {
2627 var deferred = $.Deferred(),
2628 win = this.windows[ name ];
2629
2630 if ( !( win instanceof OO.ui.Window ) ) {
2631 if ( this.factory ) {
2632 if ( !this.factory.lookup( name ) ) {
2633 deferred.reject( new OO.ui.Error(
2634 'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
2635 ) );
2636 } else {
2637 win = this.factory.create( name, this );
2638 this.addWindows( [ win ] );
2639 deferred.resolve( win );
2640 }
2641 } else {
2642 deferred.reject( new OO.ui.Error(
2643 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
2644 ) );
2645 }
2646 } else {
2647 deferred.resolve( win );
2648 }
2649
2650 return deferred.promise();
2651 };
2652
2653 /**
2654 * Get current window.
2655 *
2656 * @return {OO.ui.Window|null} Currently opening/opened/closing window
2657 */
2658 OO.ui.WindowManager.prototype.getCurrentWindow = function () {
2659 return this.currentWindow;
2660 };
2661
2662 /**
2663 * Open a window.
2664 *
2665 * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
2666 * @param {Object} [data] Window opening data
2667 * @return {jQuery.Promise} Promise resolved when window is done opening; see {@link #event-opening}
2668 * for more details about the `opening` promise
2669 * @fires opening
2670 */
2671 OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
2672 var manager = this,
2673 opening = $.Deferred();
2674
2675 // Argument handling
2676 if ( typeof win === 'string' ) {
2677 return this.getWindow( win ).then( function ( win ) {
2678 return manager.openWindow( win, data );
2679 } );
2680 }
2681
2682 // Error handling
2683 if ( !this.hasWindow( win ) ) {
2684 opening.reject( new OO.ui.Error(
2685 'Cannot open window: window is not attached to manager'
2686 ) );
2687 } else if ( this.preparingToOpen || this.opening || this.opened ) {
2688 opening.reject( new OO.ui.Error(
2689 'Cannot open window: another window is opening or open'
2690 ) );
2691 }
2692
2693 // Window opening
2694 if ( opening.state() !== 'rejected' ) {
2695 // If a window is currently closing, wait for it to complete
2696 this.preparingToOpen = $.when( this.closing );
2697 // Ensure handlers get called after preparingToOpen is set
2698 this.preparingToOpen.done( function () {
2699 if ( manager.modal ) {
2700 manager.toggleGlobalEvents( true );
2701 manager.toggleAriaIsolation( true );
2702 }
2703 manager.currentWindow = win;
2704 manager.opening = opening;
2705 manager.preparingToOpen = null;
2706 manager.emit( 'opening', win, opening, data );
2707 setTimeout( function () {
2708 win.setup( data ).then( function () {
2709 manager.updateWindowSize( win );
2710 manager.opening.notify( { state: 'setup' } );
2711 setTimeout( function () {
2712 win.ready( data ).then( function () {
2713 manager.opening.notify( { state: 'ready' } );
2714 manager.opening = null;
2715 manager.opened = $.Deferred();
2716 opening.resolve( manager.opened.promise(), data );
2717 } );
2718 }, manager.getReadyDelay() );
2719 } );
2720 }, manager.getSetupDelay() );
2721 } );
2722 }
2723
2724 return opening.promise();
2725 };
2726
2727 /**
2728 * Close a window.
2729 *
2730 * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
2731 * @param {Object} [data] Window closing data
2732 * @return {jQuery.Promise} Promise resolved when window is done closing; see {@link #event-closing}
2733 * for more details about the `closing` promise
2734 * @throws {Error} If no window by that name is being managed
2735 * @fires closing
2736 */
2737 OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
2738 var manager = this,
2739 closing = $.Deferred(),
2740 opened;
2741
2742 // Argument handling
2743 if ( typeof win === 'string' ) {
2744 win = this.windows[ win ];
2745 } else if ( !this.hasWindow( win ) ) {
2746 win = null;
2747 }
2748
2749 // Error handling
2750 if ( !win ) {
2751 closing.reject( new OO.ui.Error(
2752 'Cannot close window: window is not attached to manager'
2753 ) );
2754 } else if ( win !== this.currentWindow ) {
2755 closing.reject( new OO.ui.Error(
2756 'Cannot close window: window already closed with different data'
2757 ) );
2758 } else if ( this.preparingToClose || this.closing ) {
2759 closing.reject( new OO.ui.Error(
2760 'Cannot close window: window already closing with different data'
2761 ) );
2762 }
2763
2764 // Window closing
2765 if ( closing.state() !== 'rejected' ) {
2766 // If the window is currently opening, close it when it's done
2767 this.preparingToClose = $.when( this.opening );
2768 // Ensure handlers get called after preparingToClose is set
2769 this.preparingToClose.done( function () {
2770 manager.closing = closing;
2771 manager.preparingToClose = null;
2772 manager.emit( 'closing', win, closing, data );
2773 opened = manager.opened;
2774 manager.opened = null;
2775 opened.resolve( closing.promise(), data );
2776 setTimeout( function () {
2777 win.hold( data ).then( function () {
2778 closing.notify( { state: 'hold' } );
2779 setTimeout( function () {
2780 win.teardown( data ).then( function () {
2781 closing.notify( { state: 'teardown' } );
2782 if ( manager.modal ) {
2783 manager.toggleGlobalEvents( false );
2784 manager.toggleAriaIsolation( false );
2785 }
2786 manager.closing = null;
2787 manager.currentWindow = null;
2788 closing.resolve( data );
2789 } );
2790 }, manager.getTeardownDelay() );
2791 } );
2792 }, manager.getHoldDelay() );
2793 } );
2794 }
2795
2796 return closing.promise();
2797 };
2798
2799 /**
2800 * Add windows.
2801 *
2802 * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows Windows to add
2803 * @throws {Error} If one of the windows being added without an explicit symbolic name does not have
2804 * a statically configured symbolic name
2805 */
2806 OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
2807 var i, len, win, name, list;
2808
2809 if ( $.isArray( windows ) ) {
2810 // Convert to map of windows by looking up symbolic names from static configuration
2811 list = {};
2812 for ( i = 0, len = windows.length; i < len; i++ ) {
2813 name = windows[ i ].constructor.static.name;
2814 if ( typeof name !== 'string' ) {
2815 throw new Error( 'Cannot add window' );
2816 }
2817 list[ name ] = windows[ i ];
2818 }
2819 } else if ( $.isPlainObject( windows ) ) {
2820 list = windows;
2821 }
2822
2823 // Add windows
2824 for ( name in list ) {
2825 win = list[ name ];
2826 this.windows[ name ] = win.toggle( false );
2827 this.$element.append( win.$element );
2828 win.setManager( this );
2829 }
2830 };
2831
2832 /**
2833 * Remove windows.
2834 *
2835 * Windows will be closed before they are removed.
2836 *
2837 * @param {string[]} names Symbolic names of windows to remove
2838 * @return {jQuery.Promise} Promise resolved when window is closed and removed
2839 * @throws {Error} If windows being removed are not being managed
2840 */
2841 OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
2842 var i, len, win, name, cleanupWindow,
2843 manager = this,
2844 promises = [],
2845 cleanup = function ( name, win ) {
2846 delete manager.windows[ name ];
2847 win.$element.detach();
2848 };
2849
2850 for ( i = 0, len = names.length; i < len; i++ ) {
2851 name = names[ i ];
2852 win = this.windows[ name ];
2853 if ( !win ) {
2854 throw new Error( 'Cannot remove window' );
2855 }
2856 cleanupWindow = cleanup.bind( null, name, win );
2857 promises.push( this.closeWindow( name ).then( cleanupWindow, cleanupWindow ) );
2858 }
2859
2860 return $.when.apply( $, promises );
2861 };
2862
2863 /**
2864 * Remove all windows.
2865 *
2866 * Windows will be closed before they are removed.
2867 *
2868 * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
2869 */
2870 OO.ui.WindowManager.prototype.clearWindows = function () {
2871 return this.removeWindows( Object.keys( this.windows ) );
2872 };
2873
2874 /**
2875 * Set dialog size.
2876 *
2877 * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
2878 *
2879 * @chainable
2880 */
2881 OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) {
2882 // Bypass for non-current, and thus invisible, windows
2883 if ( win !== this.currentWindow ) {
2884 return;
2885 }
2886
2887 var viewport = OO.ui.Element.static.getDimensions( win.getElementWindow() ),
2888 sizes = this.constructor.static.sizes,
2889 size = win.getSize();
2890
2891 if ( !sizes[ size ] ) {
2892 size = this.constructor.static.defaultSize;
2893 }
2894 if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[ size ].width ) {
2895 size = 'full';
2896 }
2897
2898 this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', size === 'full' );
2899 this.$element.toggleClass( 'oo-ui-windowManager-floating', size !== 'full' );
2900 win.setDimensions( sizes[ size ] );
2901
2902 this.emit( 'resize', win );
2903
2904 return this;
2905 };
2906
2907 /**
2908 * Bind or unbind global events for scrolling.
2909 *
2910 * @param {boolean} [on] Bind global events
2911 * @chainable
2912 */
2913 OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) {
2914 on = on === undefined ? !!this.globalEvents : !!on;
2915
2916 if ( on ) {
2917 if ( !this.globalEvents ) {
2918 $( this.getElementWindow() ).on( {
2919 // Start listening for top-level window dimension changes
2920 'orientationchange resize': this.onWindowResizeHandler
2921 } );
2922 $( this.getElementDocument().body ).css( 'overflow', 'hidden' );
2923 this.globalEvents = true;
2924 }
2925 } else if ( this.globalEvents ) {
2926 $( this.getElementWindow() ).off( {
2927 // Stop listening for top-level window dimension changes
2928 'orientationchange resize': this.onWindowResizeHandler
2929 } );
2930 $( this.getElementDocument().body ).css( 'overflow', '' );
2931 this.globalEvents = false;
2932 }
2933
2934 return this;
2935 };
2936
2937 /**
2938 * Toggle screen reader visibility of content other than the window manager.
2939 *
2940 * @param {boolean} [isolate] Make only the window manager visible to screen readers
2941 * @chainable
2942 */
2943 OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) {
2944 isolate = isolate === undefined ? !this.$ariaHidden : !!isolate;
2945
2946 if ( isolate ) {
2947 if ( !this.$ariaHidden ) {
2948 // Hide everything other than the window manager from screen readers
2949 this.$ariaHidden = $( 'body' )
2950 .children()
2951 .not( this.$element.parentsUntil( 'body' ).last() )
2952 .attr( 'aria-hidden', '' );
2953 }
2954 } else if ( this.$ariaHidden ) {
2955 // Restore screen reader visibility
2956 this.$ariaHidden.removeAttr( 'aria-hidden' );
2957 this.$ariaHidden = null;
2958 }
2959
2960 return this;
2961 };
2962
2963 /**
2964 * Destroy window manager.
2965 */
2966 OO.ui.WindowManager.prototype.destroy = function () {
2967 this.toggleGlobalEvents( false );
2968 this.toggleAriaIsolation( false );
2969 this.clearWindows();
2970 this.$element.remove();
2971 };
2972
2973 /**
2974 * @class
2975 *
2976 * @constructor
2977 * @param {string|jQuery} message Description of error
2978 * @param {Object} [config] Configuration options
2979 * @cfg {boolean} [recoverable=true] Error is recoverable
2980 * @cfg {boolean} [warning=false] Whether this error is a warning or not.
2981 */
2982 OO.ui.Error = function OoUiElement( message, config ) {
2983 // Configuration initialization
2984 config = config || {};
2985
2986 // Properties
2987 this.message = message instanceof jQuery ? message : String( message );
2988 this.recoverable = config.recoverable === undefined || !!config.recoverable;
2989 this.warning = !!config.warning;
2990 };
2991
2992 /* Setup */
2993
2994 OO.initClass( OO.ui.Error );
2995
2996 /* Methods */
2997
2998 /**
2999 * Check if error can be recovered from.
3000 *
3001 * @return {boolean} Error is recoverable
3002 */
3003 OO.ui.Error.prototype.isRecoverable = function () {
3004 return this.recoverable;
3005 };
3006
3007 /**
3008 * Check if the error is a warning
3009 *
3010 * @return {boolean} Error is warning
3011 */
3012 OO.ui.Error.prototype.isWarning = function () {
3013 return this.warning;
3014 };
3015
3016 /**
3017 * Get error message as DOM nodes.
3018 *
3019 * @return {jQuery} Error message in DOM nodes
3020 */
3021 OO.ui.Error.prototype.getMessage = function () {
3022 return this.message instanceof jQuery ?
3023 this.message.clone() :
3024 $( '<div>' ).text( this.message ).contents();
3025 };
3026
3027 /**
3028 * Get error message as text.
3029 *
3030 * @return {string} Error message
3031 */
3032 OO.ui.Error.prototype.getMessageText = function () {
3033 return this.message instanceof jQuery ? this.message.text() : this.message;
3034 };
3035
3036 /**
3037 * A list of functions, called in sequence.
3038 *
3039 * If a function added to a process returns boolean false the process will stop; if it returns an
3040 * object with a `promise` method the process will use the promise to either continue to the next
3041 * step when the promise is resolved or stop when the promise is rejected.
3042 *
3043 * @class
3044 *
3045 * @constructor
3046 * @param {number|jQuery.Promise|Function} step Time to wait, promise to wait for or function to
3047 * call, see #createStep for more information
3048 * @param {Object} [context=null] Context to call the step function in, ignored if step is a number
3049 * or a promise
3050 * @return {Object} Step object, with `callback` and `context` properties
3051 */
3052 OO.ui.Process = function ( step, context ) {
3053 // Properties
3054 this.steps = [];
3055
3056 // Initialization
3057 if ( step !== undefined ) {
3058 this.next( step, context );
3059 }
3060 };
3061
3062 /* Setup */
3063
3064 OO.initClass( OO.ui.Process );
3065
3066 /* Methods */
3067
3068 /**
3069 * Start the process.
3070 *
3071 * @return {jQuery.Promise} Promise that is resolved when all steps have completed or rejected when
3072 * any of the steps return boolean false or a promise which gets rejected; upon stopping the
3073 * process, the remaining steps will not be taken
3074 */
3075 OO.ui.Process.prototype.execute = function () {
3076 var i, len, promise;
3077
3078 /**
3079 * Continue execution.
3080 *
3081 * @ignore
3082 * @param {Array} step A function and the context it should be called in
3083 * @return {Function} Function that continues the process
3084 */
3085 function proceed( step ) {
3086 return function () {
3087 // Execute step in the correct context
3088 var deferred,
3089 result = step.callback.call( step.context );
3090
3091 if ( result === false ) {
3092 // Use rejected promise for boolean false results
3093 return $.Deferred().reject( [] ).promise();
3094 }
3095 if ( typeof result === 'number' ) {
3096 if ( result < 0 ) {
3097 throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
3098 }
3099 // Use a delayed promise for numbers, expecting them to be in milliseconds
3100 deferred = $.Deferred();
3101 setTimeout( deferred.resolve, result );
3102 return deferred.promise();
3103 }
3104 if ( result instanceof OO.ui.Error ) {
3105 // Use rejected promise for error
3106 return $.Deferred().reject( [ result ] ).promise();
3107 }
3108 if ( $.isArray( result ) && result.length && result[ 0 ] instanceof OO.ui.Error ) {
3109 // Use rejected promise for list of errors
3110 return $.Deferred().reject( result ).promise();
3111 }
3112 // Duck-type the object to see if it can produce a promise
3113 if ( result && $.isFunction( result.promise ) ) {
3114 // Use a promise generated from the result
3115 return result.promise();
3116 }
3117 // Use resolved promise for other results
3118 return $.Deferred().resolve().promise();
3119 };
3120 }
3121
3122 if ( this.steps.length ) {
3123 // Generate a chain reaction of promises
3124 promise = proceed( this.steps[ 0 ] )();
3125 for ( i = 1, len = this.steps.length; i < len; i++ ) {
3126 promise = promise.then( proceed( this.steps[ i ] ) );
3127 }
3128 } else {
3129 promise = $.Deferred().resolve().promise();
3130 }
3131
3132 return promise;
3133 };
3134
3135 /**
3136 * Create a process step.
3137 *
3138 * @private
3139 * @param {number|jQuery.Promise|Function} step
3140 *
3141 * - Number of milliseconds to wait; or
3142 * - Promise to wait to be resolved; or
3143 * - Function to execute
3144 * - If it returns boolean false the process will stop
3145 * - If it returns an object with a `promise` method the process will use the promise to either
3146 * continue to the next step when the promise is resolved or stop when the promise is rejected
3147 * - If it returns a number, the process will wait for that number of milliseconds before
3148 * proceeding
3149 * @param {Object} [context=null] Context to call the step function in, ignored if step is a number
3150 * or a promise
3151 * @return {Object} Step object, with `callback` and `context` properties
3152 */
3153 OO.ui.Process.prototype.createStep = function ( step, context ) {
3154 if ( typeof step === 'number' || $.isFunction( step.promise ) ) {
3155 return {
3156 callback: function () {
3157 return step;
3158 },
3159 context: null
3160 };
3161 }
3162 if ( $.isFunction( step ) ) {
3163 return {
3164 callback: step,
3165 context: context
3166 };
3167 }
3168 throw new Error( 'Cannot create process step: number, promise or function expected' );
3169 };
3170
3171 /**
3172 * Add step to the beginning of the process.
3173 *
3174 * @inheritdoc #createStep
3175 * @return {OO.ui.Process} this
3176 * @chainable
3177 */
3178 OO.ui.Process.prototype.first = function ( step, context ) {
3179 this.steps.unshift( this.createStep( step, context ) );
3180 return this;
3181 };
3182
3183 /**
3184 * Add step to the end of the process.
3185 *
3186 * @inheritdoc #createStep
3187 * @return {OO.ui.Process} this
3188 * @chainable
3189 */
3190 OO.ui.Process.prototype.next = function ( step, context ) {
3191 this.steps.push( this.createStep( step, context ) );
3192 return this;
3193 };
3194
3195 /**
3196 * Factory for tools.
3197 *
3198 * @class
3199 * @extends OO.Factory
3200 * @constructor
3201 */
3202 OO.ui.ToolFactory = function OoUiToolFactory() {
3203 // Parent constructor
3204 OO.ui.ToolFactory.super.call( this );
3205 };
3206
3207 /* Setup */
3208
3209 OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
3210
3211 /* Methods */
3212
3213 /**
3214 * Get tools from the factory
3215 *
3216 * @param {Array} include Included tools
3217 * @param {Array} exclude Excluded tools
3218 * @param {Array} promote Promoted tools
3219 * @param {Array} demote Demoted tools
3220 * @return {string[]} List of tools
3221 */
3222 OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
3223 var i, len, included, promoted, demoted,
3224 auto = [],
3225 used = {};
3226
3227 // Collect included and not excluded tools
3228 included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
3229
3230 // Promotion
3231 promoted = this.extract( promote, used );
3232 demoted = this.extract( demote, used );
3233
3234 // Auto
3235 for ( i = 0, len = included.length; i < len; i++ ) {
3236 if ( !used[ included[ i ] ] ) {
3237 auto.push( included[ i ] );
3238 }
3239 }
3240
3241 return promoted.concat( auto ).concat( demoted );
3242 };
3243
3244 /**
3245 * Get a flat list of names from a list of names or groups.
3246 *
3247 * Tools can be specified in the following ways:
3248 *
3249 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
3250 * - All tools in a group: `{ group: 'group-name' }`
3251 * - All tools: `'*'`
3252 *
3253 * @private
3254 * @param {Array|string} collection List of tools
3255 * @param {Object} [used] Object with names that should be skipped as properties; extracted
3256 * names will be added as properties
3257 * @return {string[]} List of extracted names
3258 */
3259 OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
3260 var i, len, item, name, tool,
3261 names = [];
3262
3263 if ( collection === '*' ) {
3264 for ( name in this.registry ) {
3265 tool = this.registry[ name ];
3266 if (
3267 // Only add tools by group name when auto-add is enabled
3268 tool.static.autoAddToCatchall &&
3269 // Exclude already used tools
3270 ( !used || !used[ name ] )
3271 ) {
3272 names.push( name );
3273 if ( used ) {
3274 used[ name ] = true;
3275 }
3276 }
3277 }
3278 } else if ( $.isArray( collection ) ) {
3279 for ( i = 0, len = collection.length; i < len; i++ ) {
3280 item = collection[ i ];
3281 // Allow plain strings as shorthand for named tools
3282 if ( typeof item === 'string' ) {
3283 item = { name: item };
3284 }
3285 if ( OO.isPlainObject( item ) ) {
3286 if ( item.group ) {
3287 for ( name in this.registry ) {
3288 tool = this.registry[ name ];
3289 if (
3290 // Include tools with matching group
3291 tool.static.group === item.group &&
3292 // Only add tools by group name when auto-add is enabled
3293 tool.static.autoAddToGroup &&
3294 // Exclude already used tools
3295 ( !used || !used[ name ] )
3296 ) {
3297 names.push( name );
3298 if ( used ) {
3299 used[ name ] = true;
3300 }
3301 }
3302 }
3303 // Include tools with matching name and exclude already used tools
3304 } else if ( item.name && ( !used || !used[ item.name ] ) ) {
3305 names.push( item.name );
3306 if ( used ) {
3307 used[ item.name ] = true;
3308 }
3309 }
3310 }
3311 }
3312 }
3313 return names;
3314 };
3315
3316 /**
3317 * Factory for tool groups.
3318 *
3319 * @class
3320 * @extends OO.Factory
3321 * @constructor
3322 */
3323 OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() {
3324 // Parent constructor
3325 OO.Factory.call( this );
3326
3327 var i, l,
3328 defaultClasses = this.constructor.static.getDefaultClasses();
3329
3330 // Register default toolgroups
3331 for ( i = 0, l = defaultClasses.length; i < l; i++ ) {
3332 this.register( defaultClasses[ i ] );
3333 }
3334 };
3335
3336 /* Setup */
3337
3338 OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory );
3339
3340 /* Static Methods */
3341
3342 /**
3343 * Get a default set of classes to be registered on construction
3344 *
3345 * @return {Function[]} Default classes
3346 */
3347 OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
3348 return [
3349 OO.ui.BarToolGroup,
3350 OO.ui.ListToolGroup,
3351 OO.ui.MenuToolGroup
3352 ];
3353 };
3354
3355 /**
3356 * Theme logic.
3357 *
3358 * @abstract
3359 * @class
3360 *
3361 * @constructor
3362 * @param {Object} [config] Configuration options
3363 */
3364 OO.ui.Theme = function OoUiTheme( config ) {
3365 // Configuration initialization
3366 config = config || {};
3367 };
3368
3369 /* Setup */
3370
3371 OO.initClass( OO.ui.Theme );
3372
3373 /* Methods */
3374
3375 /**
3376 * Get a list of classes to be applied to a widget.
3377 *
3378 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
3379 * otherwise state transitions will not work properly.
3380 *
3381 * @param {OO.ui.Element} element Element for which to get classes
3382 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
3383 */
3384 OO.ui.Theme.prototype.getElementClasses = function ( /* element */ ) {
3385 return { on: [], off: [] };
3386 };
3387
3388 /**
3389 * Update CSS classes provided by the theme.
3390 *
3391 * For elements with theme logic hooks, this should be called any time there's a state change.
3392 *
3393 * @param {OO.ui.Element} element Element for which to update classes
3394 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
3395 */
3396 OO.ui.Theme.prototype.updateElementClasses = function ( element ) {
3397 var classes = this.getElementClasses( element );
3398
3399 element.$element
3400 .removeClass( classes.off.join( ' ' ) )
3401 .addClass( classes.on.join( ' ' ) );
3402 };
3403
3404 /**
3405 * Element supporting "sequential focus navigation" using the 'tabindex' attribute.
3406 *
3407 * @abstract
3408 * @class
3409 *
3410 * @constructor
3411 * @param {Object} [config] Configuration options
3412 * @cfg {jQuery} [$tabIndexed] tabIndexed node, assigned to #$tabIndexed, omit to use #$element
3413 * @cfg {number|Function} [tabIndex=0] Tab index value. Use 0 to use default ordering, use -1 to
3414 * prevent tab focusing. (default: 0)
3415 */
3416 OO.ui.TabIndexedElement = function OoUiTabIndexedElement( config ) {
3417 // Configuration initialization
3418 config = config || {};
3419
3420 // Properties
3421 this.$tabIndexed = null;
3422 this.tabIndex = null;
3423
3424 // Events
3425 this.connect( this, { disable: 'onDisable' } );
3426
3427 // Initialization
3428 this.setTabIndex( config.tabIndex || 0 );
3429 this.setTabIndexedElement( config.$tabIndexed || this.$element );
3430 };
3431
3432 /* Setup */
3433
3434 OO.initClass( OO.ui.TabIndexedElement );
3435
3436 /* Methods */
3437
3438 /**
3439 * Set the element with 'tabindex' attribute.
3440 *
3441 * If an element is already set, it will be cleaned up before setting up the new element.
3442 *
3443 * @param {jQuery} $tabIndexed Element to set tab index on
3444 */
3445 OO.ui.TabIndexedElement.prototype.setTabIndexedElement = function ( $tabIndexed ) {
3446 if ( this.$tabIndexed ) {
3447 this.$tabIndexed.removeAttr( 'tabindex aria-disabled' );
3448 }
3449
3450 this.$tabIndexed = $tabIndexed;
3451 if ( this.tabIndex !== null ) {
3452 this.$tabIndexed.attr( {
3453 // Do not index over disabled elements
3454 tabindex: this.isDisabled() ? -1 : this.tabIndex,
3455 // ChromeVox and NVDA do not seem to inherit this from parent elements
3456 'aria-disabled': this.isDisabled().toString()
3457 } );
3458 }
3459 };
3460
3461 /**
3462 * Set tab index value.
3463 *
3464 * @param {number|null} tabIndex Tab index value or null for no tabIndex
3465 * @chainable
3466 */
3467 OO.ui.TabIndexedElement.prototype.setTabIndex = function ( tabIndex ) {
3468 tabIndex = typeof tabIndex === 'number' ? tabIndex : null;
3469
3470 if ( this.tabIndex !== tabIndex ) {
3471 if ( this.$tabIndexed ) {
3472 if ( tabIndex !== null ) {
3473 this.$tabIndexed.attr( {
3474 // Do not index over disabled elements
3475 tabindex: this.isDisabled() ? -1 : tabIndex,
3476 // ChromeVox and NVDA do not seem to inherit this from parent elements
3477 'aria-disabled': this.isDisabled().toString()
3478 } );
3479 } else {
3480 this.$tabIndexed.removeAttr( 'tabindex aria-disabled' );
3481 }
3482 }
3483 this.tabIndex = tabIndex;
3484 }
3485
3486 return this;
3487 };
3488
3489 /**
3490 * Handle disable events.
3491 *
3492 * @param {boolean} disabled Element is disabled
3493 */
3494 OO.ui.TabIndexedElement.prototype.onDisable = function ( disabled ) {
3495 if ( this.$tabIndexed && this.tabIndex !== null ) {
3496 this.$tabIndexed.attr( {
3497 // Do not index over disabled elements
3498 tabindex: disabled ? -1 : this.tabIndex,
3499 // ChromeVox and NVDA do not seem to inherit this from parent elements
3500 'aria-disabled': disabled.toString()
3501 } );
3502 }
3503 };
3504
3505 /**
3506 * Get tab index value.
3507 *
3508 * @return {number} Tab index value
3509 */
3510 OO.ui.TabIndexedElement.prototype.getTabIndex = function () {
3511 return this.tabIndex;
3512 };
3513
3514 /**
3515 * Element with a button.
3516 *
3517 * Buttons are used for controls which can be clicked. They can be configured to use tab indexing
3518 * and access keys for accessibility purposes.
3519 *
3520 * @abstract
3521 * @class
3522 *
3523 * @constructor
3524 * @param {Object} [config] Configuration options
3525 * @cfg {jQuery} [$button] Button node, assigned to #$button, omit to use a generated `<a>`
3526 * @cfg {boolean} [framed=true] Render button with a frame
3527 * @cfg {string} [accessKey] Button's access key
3528 */
3529 OO.ui.ButtonElement = function OoUiButtonElement( config ) {
3530 // Configuration initialization
3531 config = config || {};
3532
3533 // Properties
3534 this.$button = config.$button || $( '<a>' );
3535 this.framed = null;
3536 this.accessKey = null;
3537 this.active = false;
3538 this.onMouseUpHandler = this.onMouseUp.bind( this );
3539 this.onMouseDownHandler = this.onMouseDown.bind( this );
3540 this.onKeyDownHandler = this.onKeyDown.bind( this );
3541 this.onKeyUpHandler = this.onKeyUp.bind( this );
3542 this.onClickHandler = this.onClick.bind( this );
3543 this.onKeyPressHandler = this.onKeyPress.bind( this );
3544
3545 // Initialization
3546 this.$element.addClass( 'oo-ui-buttonElement' );
3547 this.toggleFramed( config.framed === undefined || config.framed );
3548 this.setAccessKey( config.accessKey );
3549 this.setButtonElement( this.$button );
3550 };
3551
3552 /* Setup */
3553
3554 OO.initClass( OO.ui.ButtonElement );
3555
3556 /* Static Properties */
3557
3558 /**
3559 * Cancel mouse down events.
3560 *
3561 * @static
3562 * @inheritable
3563 * @property {boolean}
3564 */
3565 OO.ui.ButtonElement.static.cancelButtonMouseDownEvents = true;
3566
3567 /* Events */
3568
3569 /**
3570 * @event click
3571 */
3572
3573 /* Methods */
3574
3575 /**
3576 * Set the button element.
3577 *
3578 * If an element is already set, it will be cleaned up before setting up the new element.
3579 *
3580 * @param {jQuery} $button Element to use as button
3581 */
3582 OO.ui.ButtonElement.prototype.setButtonElement = function ( $button ) {
3583 if ( this.$button ) {
3584 this.$button
3585 .removeClass( 'oo-ui-buttonElement-button' )
3586 .removeAttr( 'role accesskey' )
3587 .off( {
3588 mousedown: this.onMouseDownHandler,
3589 keydown: this.onKeyDownHandler,
3590 click: this.onClickHandler,
3591 keypress: this.onKeyPressHandler
3592 } );
3593 }
3594
3595 this.$button = $button
3596 .addClass( 'oo-ui-buttonElement-button' )
3597 .attr( { role: 'button', accesskey: this.accessKey } )
3598 .on( {
3599 mousedown: this.onMouseDownHandler,
3600 keydown: this.onKeyDownHandler,
3601 click: this.onClickHandler,
3602 keypress: this.onKeyPressHandler
3603 } );
3604 };
3605
3606 /**
3607 * Handles mouse down events.
3608 *
3609 * @param {jQuery.Event} e Mouse down event
3610 */
3611 OO.ui.ButtonElement.prototype.onMouseDown = function ( e ) {
3612 if ( this.isDisabled() || e.which !== 1 ) {
3613 return;
3614 }
3615 this.$element.addClass( 'oo-ui-buttonElement-pressed' );
3616 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
3617 // reliably remove the pressed class
3618 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
3619 // Prevent change of focus unless specifically configured otherwise
3620 if ( this.constructor.static.cancelButtonMouseDownEvents ) {
3621 return false;
3622 }
3623 };
3624
3625 /**
3626 * Handles mouse up events.
3627 *
3628 * @param {jQuery.Event} e Mouse up event
3629 */
3630 OO.ui.ButtonElement.prototype.onMouseUp = function ( e ) {
3631 if ( this.isDisabled() || e.which !== 1 ) {
3632 return;
3633 }
3634 this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
3635 // Stop listening for mouseup, since we only needed this once
3636 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
3637 };
3638
3639 /**
3640 * Handles mouse click events.
3641 *
3642 * @param {jQuery.Event} e Mouse click event
3643 * @fires click
3644 */
3645 OO.ui.ButtonElement.prototype.onClick = function ( e ) {
3646 if ( !this.isDisabled() && e.which === 1 ) {
3647 this.emit( 'click' );
3648 }
3649 return false;
3650 };
3651
3652 /**
3653 * Handles key down events.
3654 *
3655 * @param {jQuery.Event} e Key down event
3656 */
3657 OO.ui.ButtonElement.prototype.onKeyDown = function ( e ) {
3658 if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
3659 return;
3660 }
3661 this.$element.addClass( 'oo-ui-buttonElement-pressed' );
3662 // Run the keyup handler no matter where the key is when the button is let go, so we can
3663 // reliably remove the pressed class
3664 this.getElementDocument().addEventListener( 'keyup', this.onKeyUpHandler, true );
3665 };
3666
3667 /**
3668 * Handles key up events.
3669 *
3670 * @param {jQuery.Event} e Key up event
3671 */
3672 OO.ui.ButtonElement.prototype.onKeyUp = function ( e ) {
3673 if ( this.isDisabled() || ( e.which !== OO.ui.Keys.SPACE && e.which !== OO.ui.Keys.ENTER ) ) {
3674 return;
3675 }
3676 this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
3677 // Stop listening for keyup, since we only needed this once
3678 this.getElementDocument().removeEventListener( 'keyup', this.onKeyUpHandler, true );
3679 };
3680
3681 /**
3682 * Handles key press events.
3683 *
3684 * @param {jQuery.Event} e Key press event
3685 * @fires click
3686 */
3687 OO.ui.ButtonElement.prototype.onKeyPress = function ( e ) {
3688 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
3689 this.emit( 'click' );
3690 }
3691 return false;
3692 };
3693
3694 /**
3695 * Check if button has a frame.
3696 *
3697 * @return {boolean} Button is framed
3698 */
3699 OO.ui.ButtonElement.prototype.isFramed = function () {
3700 return this.framed;
3701 };
3702
3703 /**
3704 * Toggle frame.
3705 *
3706 * @param {boolean} [framed] Make button framed, omit to toggle
3707 * @chainable
3708 */
3709 OO.ui.ButtonElement.prototype.toggleFramed = function ( framed ) {
3710 framed = framed === undefined ? !this.framed : !!framed;
3711 if ( framed !== this.framed ) {
3712 this.framed = framed;
3713 this.$element
3714 .toggleClass( 'oo-ui-buttonElement-frameless', !framed )
3715 .toggleClass( 'oo-ui-buttonElement-framed', framed );
3716 this.updateThemeClasses();
3717 }
3718
3719 return this;
3720 };
3721
3722 /**
3723 * Set access key.
3724 *
3725 * @param {string} accessKey Button's access key, use empty string to remove
3726 * @chainable
3727 */
3728 OO.ui.ButtonElement.prototype.setAccessKey = function ( accessKey ) {
3729 accessKey = typeof accessKey === 'string' && accessKey.length ? accessKey : null;
3730
3731 if ( this.accessKey !== accessKey ) {
3732 if ( this.$button ) {
3733 if ( accessKey !== null ) {
3734 this.$button.attr( 'accesskey', accessKey );
3735 } else {
3736 this.$button.removeAttr( 'accesskey' );
3737 }
3738 }
3739 this.accessKey = accessKey;
3740 }
3741
3742 return this;
3743 };
3744
3745 /**
3746 * Set active state.
3747 *
3748 * @param {boolean} [value] Make button active
3749 * @chainable
3750 */
3751 OO.ui.ButtonElement.prototype.setActive = function ( value ) {
3752 this.$element.toggleClass( 'oo-ui-buttonElement-active', !!value );
3753 return this;
3754 };
3755
3756 /**
3757 * Element containing a sequence of child elements.
3758 *
3759 * @abstract
3760 * @class
3761 *
3762 * @constructor
3763 * @param {Object} [config] Configuration options
3764 * @cfg {jQuery} [$group] Container node, assigned to #$group, omit to use a generated `<div>`
3765 */
3766 OO.ui.GroupElement = function OoUiGroupElement( config ) {
3767 // Configuration initialization
3768 config = config || {};
3769
3770 // Properties
3771 this.$group = null;
3772 this.items = [];
3773 this.aggregateItemEvents = {};
3774
3775 // Initialization
3776 this.setGroupElement( config.$group || $( '<div>' ) );
3777 };
3778
3779 /* Methods */
3780
3781 /**
3782 * Set the group element.
3783 *
3784 * If an element is already set, items will be moved to the new element.
3785 *
3786 * @param {jQuery} $group Element to use as group
3787 */
3788 OO.ui.GroupElement.prototype.setGroupElement = function ( $group ) {
3789 var i, len;
3790
3791 this.$group = $group;
3792 for ( i = 0, len = this.items.length; i < len; i++ ) {
3793 this.$group.append( this.items[ i ].$element );
3794 }
3795 };
3796
3797 /**
3798 * Check if there are no items.
3799 *
3800 * @return {boolean} Group is empty
3801 */
3802 OO.ui.GroupElement.prototype.isEmpty = function () {
3803 return !this.items.length;
3804 };
3805
3806 /**
3807 * Get items.
3808 *
3809 * @return {OO.ui.Element[]} Items
3810 */
3811 OO.ui.GroupElement.prototype.getItems = function () {
3812 return this.items.slice( 0 );
3813 };
3814
3815 /**
3816 * Get an item by its data.
3817 *
3818 * Data is compared by a hash of its value. Only the first item with matching data will be returned.
3819 *
3820 * @param {Object} data Item data to search for
3821 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
3822 */
3823 OO.ui.GroupElement.prototype.getItemFromData = function ( data ) {
3824 var i, len, item,
3825 hash = OO.getHash( data );
3826
3827 for ( i = 0, len = this.items.length; i < len; i++ ) {
3828 item = this.items[ i ];
3829 if ( hash === OO.getHash( item.getData() ) ) {
3830 return item;
3831 }
3832 }
3833
3834 return null;
3835 };
3836
3837 /**
3838 * Get items by their data.
3839 *
3840 * Data is compared by a hash of its value. All items with matching data will be returned.
3841 *
3842 * @param {Object} data Item data to search for
3843 * @return {OO.ui.Element[]} Items with equivalent data
3844 */
3845 OO.ui.GroupElement.prototype.getItemsFromData = function ( data ) {
3846 var i, len, item,
3847 hash = OO.getHash( data ),
3848 items = [];
3849
3850 for ( i = 0, len = this.items.length; i < len; i++ ) {
3851 item = this.items[ i ];
3852 if ( hash === OO.getHash( item.getData() ) ) {
3853 items.push( item );
3854 }
3855 }
3856
3857 return items;
3858 };
3859
3860 /**
3861 * Add an aggregate item event.
3862 *
3863 * Aggregated events are listened to on each item and then emitted by the group under a new name,
3864 * and with an additional leading parameter containing the item that emitted the original event.
3865 * Other arguments that were emitted from the original event are passed through.
3866 *
3867 * @param {Object.<string,string|null>} events Aggregate events emitted by group, keyed by item
3868 * event, use null value to remove aggregation
3869 * @throws {Error} If aggregation already exists
3870 */
3871 OO.ui.GroupElement.prototype.aggregate = function ( events ) {
3872 var i, len, item, add, remove, itemEvent, groupEvent;
3873
3874 for ( itemEvent in events ) {
3875 groupEvent = events[ itemEvent ];
3876
3877 // Remove existing aggregated event
3878 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
3879 // Don't allow duplicate aggregations
3880 if ( groupEvent ) {
3881 throw new Error( 'Duplicate item event aggregation for ' + itemEvent );
3882 }
3883 // Remove event aggregation from existing items
3884 for ( i = 0, len = this.items.length; i < len; i++ ) {
3885 item = this.items[ i ];
3886 if ( item.connect && item.disconnect ) {
3887 remove = {};
3888 remove[ itemEvent ] = [ 'emit', groupEvent, item ];
3889 item.disconnect( this, remove );
3890 }
3891 }
3892 // Prevent future items from aggregating event
3893 delete this.aggregateItemEvents[ itemEvent ];
3894 }
3895
3896 // Add new aggregate event
3897 if ( groupEvent ) {
3898 // Make future items aggregate event
3899 this.aggregateItemEvents[ itemEvent ] = groupEvent;
3900 // Add event aggregation to existing items
3901 for ( i = 0, len = this.items.length; i < len; i++ ) {
3902 item = this.items[ i ];
3903 if ( item.connect && item.disconnect ) {
3904 add = {};
3905 add[ itemEvent ] = [ 'emit', groupEvent, item ];
3906 item.connect( this, add );
3907 }
3908 }
3909 }
3910 }
3911 };
3912
3913 /**
3914 * Add items.
3915 *
3916 * Adding an existing item will move it.
3917 *
3918 * @param {OO.ui.Element[]} items Items
3919 * @param {number} [index] Index to insert items at
3920 * @chainable
3921 */
3922 OO.ui.GroupElement.prototype.addItems = function ( items, index ) {
3923 var i, len, item, event, events, currentIndex,
3924 itemElements = [];
3925
3926 for ( i = 0, len = items.length; i < len; i++ ) {
3927 item = items[ i ];
3928
3929 // Check if item exists then remove it first, effectively "moving" it
3930 currentIndex = $.inArray( item, this.items );
3931 if ( currentIndex >= 0 ) {
3932 this.removeItems( [ item ] );
3933 // Adjust index to compensate for removal
3934 if ( currentIndex < index ) {
3935 index--;
3936 }
3937 }
3938 // Add the item
3939 if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
3940 events = {};
3941 for ( event in this.aggregateItemEvents ) {
3942 events[ event ] = [ 'emit', this.aggregateItemEvents[ event ], item ];
3943 }
3944 item.connect( this, events );
3945 }
3946 item.setElementGroup( this );
3947 itemElements.push( item.$element.get( 0 ) );
3948 }
3949
3950 if ( index === undefined || index < 0 || index >= this.items.length ) {
3951 this.$group.append( itemElements );
3952 this.items.push.apply( this.items, items );
3953 } else if ( index === 0 ) {
3954 this.$group.prepend( itemElements );
3955 this.items.unshift.apply( this.items, items );
3956 } else {
3957 this.items[ index ].$element.before( itemElements );
3958 this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
3959 }
3960
3961 return this;
3962 };
3963
3964 /**
3965 * Remove items.
3966 *
3967 * Items will be detached, not removed, so they can be used later.
3968 *
3969 * @param {OO.ui.Element[]} items Items to remove
3970 * @chainable
3971 */
3972 OO.ui.GroupElement.prototype.removeItems = function ( items ) {
3973 var i, len, item, index, remove, itemEvent;
3974
3975 // Remove specific items
3976 for ( i = 0, len = items.length; i < len; i++ ) {
3977 item = items[ i ];
3978 index = $.inArray( item, this.items );
3979 if ( index !== -1 ) {
3980 if (
3981 item.connect && item.disconnect &&
3982 !$.isEmptyObject( this.aggregateItemEvents )
3983 ) {
3984 remove = {};
3985 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
3986 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
3987 }
3988 item.disconnect( this, remove );
3989 }
3990 item.setElementGroup( null );
3991 this.items.splice( index, 1 );
3992 item.$element.detach();
3993 }
3994 }
3995
3996 return this;
3997 };
3998
3999 /**
4000 * Clear all items.
4001 *
4002 * Items will be detached, not removed, so they can be used later.
4003 *
4004 * @chainable
4005 */
4006 OO.ui.GroupElement.prototype.clearItems = function () {
4007 var i, len, item, remove, itemEvent;
4008
4009 // Remove all items
4010 for ( i = 0, len = this.items.length; i < len; i++ ) {
4011 item = this.items[ i ];
4012 if (
4013 item.connect && item.disconnect &&
4014 !$.isEmptyObject( this.aggregateItemEvents )
4015 ) {
4016 remove = {};
4017 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4018 remove[ itemEvent ] = [ 'emit', this.aggregateItemEvents[ itemEvent ], item ];
4019 }
4020 item.disconnect( this, remove );
4021 }
4022 item.setElementGroup( null );
4023 item.$element.detach();
4024 }
4025
4026 this.items = [];
4027 return this;
4028 };
4029
4030 /**
4031 * A mixin for an element that can be dragged and dropped.
4032 * Use in conjunction with DragGroupWidget
4033 *
4034 * @abstract
4035 * @class
4036 *
4037 * @constructor
4038 */
4039 OO.ui.DraggableElement = function OoUiDraggableElement() {
4040 // Properties
4041 this.index = null;
4042
4043 // Initialize and events
4044 this.$element
4045 .attr( 'draggable', true )
4046 .addClass( 'oo-ui-draggableElement' )
4047 .on( {
4048 dragstart: this.onDragStart.bind( this ),
4049 dragover: this.onDragOver.bind( this ),
4050 dragend: this.onDragEnd.bind( this ),
4051 drop: this.onDrop.bind( this )
4052 } );
4053 };
4054
4055 OO.initClass( OO.ui.DraggableElement );
4056
4057 /* Events */
4058
4059 /**
4060 * @event dragstart
4061 * @param {OO.ui.DraggableElement} item Dragging item
4062 */
4063
4064 /**
4065 * @event dragend
4066 */
4067
4068 /**
4069 * @event drop
4070 */
4071
4072 /* Static Properties */
4073
4074 /**
4075 * @inheritdoc OO.ui.ButtonElement
4076 */
4077 OO.ui.DraggableElement.static.cancelButtonMouseDownEvents = false;
4078
4079 /* Methods */
4080
4081 /**
4082 * Respond to dragstart event.
4083 * @param {jQuery.Event} event jQuery event
4084 * @fires dragstart
4085 */
4086 OO.ui.DraggableElement.prototype.onDragStart = function ( e ) {
4087 var dataTransfer = e.originalEvent.dataTransfer;
4088 // Define drop effect
4089 dataTransfer.dropEffect = 'none';
4090 dataTransfer.effectAllowed = 'move';
4091 // We must set up a dataTransfer data property or Firefox seems to
4092 // ignore the fact the element is draggable.
4093 try {
4094 dataTransfer.setData( 'application-x/OOjs-UI-draggable', this.getIndex() );
4095 } catch ( err ) {
4096 // The above is only for firefox. No need to set a catch clause
4097 // if it fails, move on.
4098 }
4099 // Add dragging class
4100 this.$element.addClass( 'oo-ui-draggableElement-dragging' );
4101 // Emit event
4102 this.emit( 'dragstart', this );
4103 return true;
4104 };
4105
4106 /**
4107 * Respond to dragend event.
4108 * @fires dragend
4109 */
4110 OO.ui.DraggableElement.prototype.onDragEnd = function () {
4111 this.$element.removeClass( 'oo-ui-draggableElement-dragging' );
4112 this.emit( 'dragend' );
4113 };
4114
4115 /**
4116 * Handle drop event.
4117 * @param {jQuery.Event} event jQuery event
4118 * @fires drop
4119 */
4120 OO.ui.DraggableElement.prototype.onDrop = function ( e ) {
4121 e.preventDefault();
4122 this.emit( 'drop', e );
4123 };
4124
4125 /**
4126 * In order for drag/drop to work, the dragover event must
4127 * return false and stop propogation.
4128 */
4129 OO.ui.DraggableElement.prototype.onDragOver = function ( e ) {
4130 e.preventDefault();
4131 };
4132
4133 /**
4134 * Set item index.
4135 * Store it in the DOM so we can access from the widget drag event
4136 * @param {number} Item index
4137 */
4138 OO.ui.DraggableElement.prototype.setIndex = function ( index ) {
4139 if ( this.index !== index ) {
4140 this.index = index;
4141 this.$element.data( 'index', index );
4142 }
4143 };
4144
4145 /**
4146 * Get item index
4147 * @return {number} Item index
4148 */
4149 OO.ui.DraggableElement.prototype.getIndex = function () {
4150 return this.index;
4151 };
4152
4153 /**
4154 * Element containing a sequence of child elements that can be dragged
4155 * and dropped.
4156 *
4157 * @abstract
4158 * @class
4159 *
4160 * @constructor
4161 * @param {Object} [config] Configuration options
4162 * @cfg {jQuery} [$group] Container node, assigned to #$group, omit to use a generated `<div>`
4163 * @cfg {string} [orientation] Item orientation, 'horizontal' or 'vertical'. Defaults to 'vertical'
4164 */
4165 OO.ui.DraggableGroupElement = function OoUiDraggableGroupElement( config ) {
4166 // Configuration initialization
4167 config = config || {};
4168
4169 // Parent constructor
4170 OO.ui.GroupElement.call( this, config );
4171
4172 // Properties
4173 this.orientation = config.orientation || 'vertical';
4174 this.dragItem = null;
4175 this.itemDragOver = null;
4176 this.itemKeys = {};
4177 this.sideInsertion = '';
4178
4179 // Events
4180 this.aggregate( {
4181 dragstart: 'itemDragStart',
4182 dragend: 'itemDragEnd',
4183 drop: 'itemDrop'
4184 } );
4185 this.connect( this, {
4186 itemDragStart: 'onItemDragStart',
4187 itemDrop: 'onItemDrop',
4188 itemDragEnd: 'onItemDragEnd'
4189 } );
4190 this.$element.on( {
4191 dragover: $.proxy( this.onDragOver, this ),
4192 dragleave: $.proxy( this.onDragLeave, this )
4193 } );
4194
4195 // Initialize
4196 if ( $.isArray( config.items ) ) {
4197 this.addItems( config.items );
4198 }
4199 this.$placeholder = $( '<div>' )
4200 .addClass( 'oo-ui-draggableGroupElement-placeholder' );
4201 this.$element
4202 .addClass( 'oo-ui-draggableGroupElement' )
4203 .append( this.$status )
4204 .toggleClass( 'oo-ui-draggableGroupElement-horizontal', this.orientation === 'horizontal' )
4205 .prepend( this.$placeholder );
4206 };
4207
4208 /* Setup */
4209 OO.mixinClass( OO.ui.DraggableGroupElement, OO.ui.GroupElement );
4210
4211 /* Events */
4212
4213 /**
4214 * @event reorder
4215 * @param {OO.ui.DraggableElement} item Reordered item
4216 * @param {number} [newIndex] New index for the item
4217 */
4218
4219 /* Methods */
4220
4221 /**
4222 * Respond to item drag start event
4223 * @param {OO.ui.DraggableElement} item Dragged item
4224 */
4225 OO.ui.DraggableGroupElement.prototype.onItemDragStart = function ( item ) {
4226 var i, len;
4227
4228 // Map the index of each object
4229 for ( i = 0, len = this.items.length; i < len; i++ ) {
4230 this.items[ i ].setIndex( i );
4231 }
4232
4233 if ( this.orientation === 'horizontal' ) {
4234 // Set the height of the indicator
4235 this.$placeholder.css( {
4236 height: item.$element.outerHeight(),
4237 width: 2
4238 } );
4239 } else {
4240 // Set the width of the indicator
4241 this.$placeholder.css( {
4242 height: 2,
4243 width: item.$element.outerWidth()
4244 } );
4245 }
4246 this.setDragItem( item );
4247 };
4248
4249 /**
4250 * Respond to item drag end event
4251 */
4252 OO.ui.DraggableGroupElement.prototype.onItemDragEnd = function () {
4253 this.unsetDragItem();
4254 return false;
4255 };
4256
4257 /**
4258 * Handle drop event and switch the order of the items accordingly
4259 * @param {OO.ui.DraggableElement} item Dropped item
4260 * @fires reorder
4261 */
4262 OO.ui.DraggableGroupElement.prototype.onItemDrop = function ( item ) {
4263 var toIndex = item.getIndex();
4264 // Check if the dropped item is from the current group
4265 // TODO: Figure out a way to configure a list of legally droppable
4266 // elements even if they are not yet in the list
4267 if ( this.getDragItem() ) {
4268 // If the insertion point is 'after', the insertion index
4269 // is shifted to the right (or to the left in RTL, hence 'after')
4270 if ( this.sideInsertion === 'after' ) {
4271 toIndex++;
4272 }
4273 // Emit change event
4274 this.emit( 'reorder', this.getDragItem(), toIndex );
4275 }
4276 this.unsetDragItem();
4277 // Return false to prevent propogation
4278 return false;
4279 };
4280
4281 /**
4282 * Handle dragleave event.
4283 */
4284 OO.ui.DraggableGroupElement.prototype.onDragLeave = function () {
4285 // This means the item was dragged outside the widget
4286 this.$placeholder
4287 .css( 'left', 0 )
4288 .addClass( 'oo-ui-element-hidden' );
4289 };
4290
4291 /**
4292 * Respond to dragover event
4293 * @param {jQuery.Event} event Event details
4294 */
4295 OO.ui.DraggableGroupElement.prototype.onDragOver = function ( e ) {
4296 var dragOverObj, $optionWidget, itemOffset, itemMidpoint, itemBoundingRect,
4297 itemSize, cssOutput, dragPosition, itemIndex, itemPosition,
4298 clientX = e.originalEvent.clientX,
4299 clientY = e.originalEvent.clientY;
4300
4301 // Get the OptionWidget item we are dragging over
4302 dragOverObj = this.getElementDocument().elementFromPoint( clientX, clientY );
4303 $optionWidget = $( dragOverObj ).closest( '.oo-ui-draggableElement' );
4304 if ( $optionWidget[ 0 ] ) {
4305 itemOffset = $optionWidget.offset();
4306 itemBoundingRect = $optionWidget[ 0 ].getBoundingClientRect();
4307 itemPosition = $optionWidget.position();
4308 itemIndex = $optionWidget.data( 'index' );
4309 }
4310
4311 if (
4312 itemOffset &&
4313 this.isDragging() &&
4314 itemIndex !== this.getDragItem().getIndex()
4315 ) {
4316 if ( this.orientation === 'horizontal' ) {
4317 // Calculate where the mouse is relative to the item width
4318 itemSize = itemBoundingRect.width;
4319 itemMidpoint = itemBoundingRect.left + itemSize / 2;
4320 dragPosition = clientX;
4321 // Which side of the item we hover over will dictate
4322 // where the placeholder will appear, on the left or
4323 // on the right
4324 cssOutput = {
4325 left: dragPosition < itemMidpoint ? itemPosition.left : itemPosition.left + itemSize,
4326 top: itemPosition.top
4327 };
4328 } else {
4329 // Calculate where the mouse is relative to the item height
4330 itemSize = itemBoundingRect.height;
4331 itemMidpoint = itemBoundingRect.top + itemSize / 2;
4332 dragPosition = clientY;
4333 // Which side of the item we hover over will dictate
4334 // where the placeholder will appear, on the top or
4335 // on the bottom
4336 cssOutput = {
4337 top: dragPosition < itemMidpoint ? itemPosition.top : itemPosition.top + itemSize,
4338 left: itemPosition.left
4339 };
4340 }
4341 // Store whether we are before or after an item to rearrange
4342 // For horizontal layout, we need to account for RTL, as this is flipped
4343 if ( this.orientation === 'horizontal' && this.$element.css( 'direction' ) === 'rtl' ) {
4344 this.sideInsertion = dragPosition < itemMidpoint ? 'after' : 'before';
4345 } else {
4346 this.sideInsertion = dragPosition < itemMidpoint ? 'before' : 'after';
4347 }
4348 // Add drop indicator between objects
4349 this.$placeholder
4350 .css( cssOutput )
4351 .removeClass( 'oo-ui-element-hidden' );
4352 } else {
4353 // This means the item was dragged outside the widget
4354 this.$placeholder
4355 .css( 'left', 0 )
4356 .addClass( 'oo-ui-element-hidden' );
4357 }
4358 // Prevent default
4359 e.preventDefault();
4360 };
4361
4362 /**
4363 * Set a dragged item
4364 * @param {OO.ui.DraggableElement} item Dragged item
4365 */
4366 OO.ui.DraggableGroupElement.prototype.setDragItem = function ( item ) {
4367 this.dragItem = item;
4368 };
4369
4370 /**
4371 * Unset the current dragged item
4372 */
4373 OO.ui.DraggableGroupElement.prototype.unsetDragItem = function () {
4374 this.dragItem = null;
4375 this.itemDragOver = null;
4376 this.$placeholder.addClass( 'oo-ui-element-hidden' );
4377 this.sideInsertion = '';
4378 };
4379
4380 /**
4381 * Get the current dragged item
4382 * @return {OO.ui.DraggableElement|null} item Dragged item or null if no item is dragged
4383 */
4384 OO.ui.DraggableGroupElement.prototype.getDragItem = function () {
4385 return this.dragItem;
4386 };
4387
4388 /**
4389 * Check if there's an item being dragged.
4390 * @return {Boolean} Item is being dragged
4391 */
4392 OO.ui.DraggableGroupElement.prototype.isDragging = function () {
4393 return this.getDragItem() !== null;
4394 };
4395
4396 /**
4397 * Element containing an icon.
4398 *
4399 * Icons are graphics, about the size of normal text. They can be used to aid the user in locating
4400 * a control or convey information in a more space efficient way. Icons should rarely be used
4401 * without labels; such as in a toolbar where space is at a premium or within a context where the
4402 * meaning is very clear to the user.
4403 *
4404 * @abstract
4405 * @class
4406 *
4407 * @constructor
4408 * @param {Object} [config] Configuration options
4409 * @cfg {jQuery} [$icon] Icon node, assigned to #$icon, omit to use a generated `<span>`
4410 * @cfg {Object|string} [icon=''] Symbolic icon name, or map of icon names keyed by language ID;
4411 * use the 'default' key to specify the icon to be used when there is no icon in the user's
4412 * language
4413 * @cfg {string} [iconTitle] Icon title text or a function that returns text
4414 */
4415 OO.ui.IconElement = function OoUiIconElement( config ) {
4416 // Configuration initialization
4417 config = config || {};
4418
4419 // Properties
4420 this.$icon = null;
4421 this.icon = null;
4422 this.iconTitle = null;
4423
4424 // Initialization
4425 this.setIcon( config.icon || this.constructor.static.icon );
4426 this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle );
4427 this.setIconElement( config.$icon || $( '<span>' ) );
4428 };
4429
4430 /* Setup */
4431
4432 OO.initClass( OO.ui.IconElement );
4433
4434 /* Static Properties */
4435
4436 /**
4437 * Icon.
4438 *
4439 * Value should be the unique portion of an icon CSS class name, such as 'up' for 'oo-ui-icon-up'.
4440 *
4441 * For i18n purposes, this property can be an object containing a `default` icon name property and
4442 * additional icon names keyed by language code.
4443 *
4444 * Example of i18n icon definition:
4445 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
4446 *
4447 * @static
4448 * @inheritable
4449 * @property {Object|string} Symbolic icon name, or map of icon names keyed by language ID;
4450 * use the 'default' key to specify the icon to be used when there is no icon in the user's
4451 * language
4452 */
4453 OO.ui.IconElement.static.icon = null;
4454
4455 /**
4456 * Icon title.
4457 *
4458 * @static
4459 * @inheritable
4460 * @property {string|Function|null} Icon title text, a function that returns text or null for no
4461 * icon title
4462 */
4463 OO.ui.IconElement.static.iconTitle = null;
4464
4465 /* Methods */
4466
4467 /**
4468 * Set the icon element.
4469 *
4470 * If an element is already set, it will be cleaned up before setting up the new element.
4471 *
4472 * @param {jQuery} $icon Element to use as icon
4473 */
4474 OO.ui.IconElement.prototype.setIconElement = function ( $icon ) {
4475 if ( this.$icon ) {
4476 this.$icon
4477 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon )
4478 .removeAttr( 'title' );
4479 }
4480
4481 this.$icon = $icon
4482 .addClass( 'oo-ui-iconElement-icon' )
4483 .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon );
4484 if ( this.iconTitle !== null ) {
4485 this.$icon.attr( 'title', this.iconTitle );
4486 }
4487 };
4488
4489 /**
4490 * Set icon name.
4491 *
4492 * @param {Object|string|null} icon Symbolic icon name, or map of icon names keyed by language ID;
4493 * use the 'default' key to specify the icon to be used when there is no icon in the user's
4494 * language, use null to remove icon
4495 * @chainable
4496 */
4497 OO.ui.IconElement.prototype.setIcon = function ( icon ) {
4498 icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
4499 icon = typeof icon === 'string' && icon.trim().length ? icon.trim() : null;
4500
4501 if ( this.icon !== icon ) {
4502 if ( this.$icon ) {
4503 if ( this.icon !== null ) {
4504 this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
4505 }
4506 if ( icon !== null ) {
4507 this.$icon.addClass( 'oo-ui-icon-' + icon );
4508 }
4509 }
4510 this.icon = icon;
4511 }
4512
4513 this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon );
4514 this.updateThemeClasses();
4515
4516 return this;
4517 };
4518
4519 /**
4520 * Set icon title.
4521 *
4522 * @param {string|Function|null} icon Icon title text, a function that returns text or null
4523 * for no icon title
4524 * @chainable
4525 */
4526 OO.ui.IconElement.prototype.setIconTitle = function ( iconTitle ) {
4527 iconTitle = typeof iconTitle === 'function' ||
4528 ( typeof iconTitle === 'string' && iconTitle.length ) ?
4529 OO.ui.resolveMsg( iconTitle ) : null;
4530
4531 if ( this.iconTitle !== iconTitle ) {
4532 this.iconTitle = iconTitle;
4533 if ( this.$icon ) {
4534 if ( this.iconTitle !== null ) {
4535 this.$icon.attr( 'title', iconTitle );
4536 } else {
4537 this.$icon.removeAttr( 'title' );
4538 }
4539 }
4540 }
4541
4542 return this;
4543 };
4544
4545 /**
4546 * Get icon name.
4547 *
4548 * @return {string} Icon name
4549 */
4550 OO.ui.IconElement.prototype.getIcon = function () {
4551 return this.icon;
4552 };
4553
4554 /**
4555 * Get icon title.
4556 *
4557 * @return {string} Icon title text
4558 */
4559 OO.ui.IconElement.prototype.getIconTitle = function () {
4560 return this.iconTitle;
4561 };
4562
4563 /**
4564 * Element containing an indicator.
4565 *
4566 * Indicators are graphics, smaller than normal text. They can be used to describe unique status or
4567 * behavior. Indicators should only be used in exceptional cases; such as a button that opens a menu
4568 * instead of performing an action directly, or an item in a list which has errors that need to be
4569 * resolved.
4570 *
4571 * @abstract
4572 * @class
4573 *
4574 * @constructor
4575 * @param {Object} [config] Configuration options
4576 * @cfg {jQuery} [$indicator] Indicator node, assigned to #$indicator, omit to use a generated
4577 * `<span>`
4578 * @cfg {string} [indicator] Symbolic indicator name
4579 * @cfg {string} [indicatorTitle] Indicator title text or a function that returns text
4580 */
4581 OO.ui.IndicatorElement = function OoUiIndicatorElement( config ) {
4582 // Configuration initialization
4583 config = config || {};
4584
4585 // Properties
4586 this.$indicator = null;
4587 this.indicator = null;
4588 this.indicatorTitle = null;
4589
4590 // Initialization
4591 this.setIndicator( config.indicator || this.constructor.static.indicator );
4592 this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle );
4593 this.setIndicatorElement( config.$indicator || $( '<span>' ) );
4594 };
4595
4596 /* Setup */
4597
4598 OO.initClass( OO.ui.IndicatorElement );
4599
4600 /* Static Properties */
4601
4602 /**
4603 * indicator.
4604 *
4605 * @static
4606 * @inheritable
4607 * @property {string|null} Symbolic indicator name
4608 */
4609 OO.ui.IndicatorElement.static.indicator = null;
4610
4611 /**
4612 * Indicator title.
4613 *
4614 * @static
4615 * @inheritable
4616 * @property {string|Function|null} Indicator title text, a function that returns text or null for no
4617 * indicator title
4618 */
4619 OO.ui.IndicatorElement.static.indicatorTitle = null;
4620
4621 /* Methods */
4622
4623 /**
4624 * Set the indicator element.
4625 *
4626 * If an element is already set, it will be cleaned up before setting up the new element.
4627 *
4628 * @param {jQuery} $indicator Element to use as indicator
4629 */
4630 OO.ui.IndicatorElement.prototype.setIndicatorElement = function ( $indicator ) {
4631 if ( this.$indicator ) {
4632 this.$indicator
4633 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator )
4634 .removeAttr( 'title' );
4635 }
4636
4637 this.$indicator = $indicator
4638 .addClass( 'oo-ui-indicatorElement-indicator' )
4639 .toggleClass( 'oo-ui-indicator-' + this.indicator, !!this.indicator );
4640 if ( this.indicatorTitle !== null ) {
4641 this.$indicator.attr( 'title', this.indicatorTitle );
4642 }
4643 };
4644
4645 /**
4646 * Set indicator name.
4647 *
4648 * @param {string|null} indicator Symbolic name of indicator to use or null for no indicator
4649 * @chainable
4650 */
4651 OO.ui.IndicatorElement.prototype.setIndicator = function ( indicator ) {
4652 indicator = typeof indicator === 'string' && indicator.length ? indicator.trim() : null;
4653
4654 if ( this.indicator !== indicator ) {
4655 if ( this.$indicator ) {
4656 if ( this.indicator !== null ) {
4657 this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
4658 }
4659 if ( indicator !== null ) {
4660 this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
4661 }
4662 }
4663 this.indicator = indicator;
4664 }
4665
4666 this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator );
4667 this.updateThemeClasses();
4668
4669 return this;
4670 };
4671
4672 /**
4673 * Set indicator title.
4674 *
4675 * @param {string|Function|null} indicator Indicator title text, a function that returns text or
4676 * null for no indicator title
4677 * @chainable
4678 */
4679 OO.ui.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
4680 indicatorTitle = typeof indicatorTitle === 'function' ||
4681 ( typeof indicatorTitle === 'string' && indicatorTitle.length ) ?
4682 OO.ui.resolveMsg( indicatorTitle ) : null;
4683
4684 if ( this.indicatorTitle !== indicatorTitle ) {
4685 this.indicatorTitle = indicatorTitle;
4686 if ( this.$indicator ) {
4687 if ( this.indicatorTitle !== null ) {
4688 this.$indicator.attr( 'title', indicatorTitle );
4689 } else {
4690 this.$indicator.removeAttr( 'title' );
4691 }
4692 }
4693 }
4694
4695 return this;
4696 };
4697
4698 /**
4699 * Get indicator name.
4700 *
4701 * @return {string} Symbolic name of indicator
4702 */
4703 OO.ui.IndicatorElement.prototype.getIndicator = function () {
4704 return this.indicator;
4705 };
4706
4707 /**
4708 * Get indicator title.
4709 *
4710 * @return {string} Indicator title text
4711 */
4712 OO.ui.IndicatorElement.prototype.getIndicatorTitle = function () {
4713 return this.indicatorTitle;
4714 };
4715
4716 /**
4717 * Element containing a label.
4718 *
4719 * @abstract
4720 * @class
4721 *
4722 * @constructor
4723 * @param {Object} [config] Configuration options
4724 * @cfg {jQuery} [$label] Label node, assigned to #$label, omit to use a generated `<span>`
4725 * @cfg {jQuery|string|Function} [label] Label nodes, text or a function that returns nodes or text
4726 * @cfg {boolean} [autoFitLabel=true] Whether to fit the label or not.
4727 */
4728 OO.ui.LabelElement = function OoUiLabelElement( config ) {
4729 // Configuration initialization
4730 config = config || {};
4731
4732 // Properties
4733 this.$label = null;
4734 this.label = null;
4735 this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel;
4736
4737 // Initialization
4738 this.setLabel( config.label || this.constructor.static.label );
4739 this.setLabelElement( config.$label || $( '<span>' ) );
4740 };
4741
4742 /* Setup */
4743
4744 OO.initClass( OO.ui.LabelElement );
4745
4746 /* Events */
4747
4748 /**
4749 * @event labelChange
4750 * @param {string} value
4751 */
4752
4753 /* Static Properties */
4754
4755 /**
4756 * Label.
4757 *
4758 * @static
4759 * @inheritable
4760 * @property {string|Function|null} Label text; a function that returns nodes or text; or null for
4761 * no label
4762 */
4763 OO.ui.LabelElement.static.label = null;
4764
4765 /* Methods */
4766
4767 /**
4768 * Set the label element.
4769 *
4770 * If an element is already set, it will be cleaned up before setting up the new element.
4771 *
4772 * @param {jQuery} $label Element to use as label
4773 */
4774 OO.ui.LabelElement.prototype.setLabelElement = function ( $label ) {
4775 if ( this.$label ) {
4776 this.$label.removeClass( 'oo-ui-labelElement-label' ).empty();
4777 }
4778
4779 this.$label = $label.addClass( 'oo-ui-labelElement-label' );
4780 this.setLabelContent( this.label );
4781 };
4782
4783 /**
4784 * Set the label.
4785 *
4786 * An empty string will result in the label being hidden. A string containing only whitespace will
4787 * be converted to a single `&nbsp;`.
4788 *
4789 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
4790 * text; or null for no label
4791 * @chainable
4792 */
4793 OO.ui.LabelElement.prototype.setLabel = function ( label ) {
4794 label = typeof label === 'function' ? OO.ui.resolveMsg( label ) : label;
4795 label = ( typeof label === 'string' && label.length ) || label instanceof jQuery ? label : null;
4796
4797 this.$element.toggleClass( 'oo-ui-labelElement', !!label );
4798
4799 if ( this.label !== label ) {
4800 if ( this.$label ) {
4801 this.setLabelContent( label );
4802 }
4803 this.label = label;
4804 this.emit( 'labelChange' );
4805 }
4806
4807 return this;
4808 };
4809
4810 /**
4811 * Get the label.
4812 *
4813 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
4814 * text; or null for no label
4815 */
4816 OO.ui.LabelElement.prototype.getLabel = function () {
4817 return this.label;
4818 };
4819
4820 /**
4821 * Fit the label.
4822 *
4823 * @chainable
4824 */
4825 OO.ui.LabelElement.prototype.fitLabel = function () {
4826 if ( this.$label && this.$label.autoEllipsis && this.autoFitLabel ) {
4827 this.$label.autoEllipsis( { hasSpan: false, tooltip: true } );
4828 }
4829
4830 return this;
4831 };
4832
4833 /**
4834 * Set the content of the label.
4835 *
4836 * Do not call this method until after the label element has been set by #setLabelElement.
4837 *
4838 * @private
4839 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
4840 * text; or null for no label
4841 */
4842 OO.ui.LabelElement.prototype.setLabelContent = function ( label ) {
4843 if ( typeof label === 'string' ) {
4844 if ( label.match( /^\s*$/ ) ) {
4845 // Convert whitespace only string to a single non-breaking space
4846 this.$label.html( '&nbsp;' );
4847 } else {
4848 this.$label.text( label );
4849 }
4850 } else if ( label instanceof jQuery ) {
4851 this.$label.empty().append( label );
4852 } else {
4853 this.$label.empty();
4854 }
4855 };
4856
4857 /**
4858 * Mixin that adds a menu showing suggested values for a OO.ui.TextInputWidget.
4859 *
4860 * Subclasses that set the value of #lookupInput from #onLookupMenuItemChoose should
4861 * be aware that this will cause new suggestions to be looked up for the new value. If this is
4862 * not desired, disable lookups with #setLookupsDisabled, then set the value, then re-enable lookups.
4863 *
4864 * @class
4865 * @abstract
4866 *
4867 * @constructor
4868 * @param {Object} [config] Configuration options
4869 * @cfg {jQuery} [$overlay] Overlay for dropdown; defaults to relative positioning
4870 * @cfg {jQuery} [$container=this.$element] Element to render menu under
4871 */
4872 OO.ui.LookupElement = function OoUiLookupElement( config ) {
4873 // Configuration initialization
4874 config = config || {};
4875
4876 // Properties
4877 this.$overlay = config.$overlay || this.$element;
4878 this.lookupMenu = new OO.ui.TextInputMenuSelectWidget( this, {
4879 $container: config.$container
4880 } );
4881 this.lookupCache = {};
4882 this.lookupQuery = null;
4883 this.lookupRequest = null;
4884 this.lookupsDisabled = false;
4885 this.lookupInputFocused = false;
4886
4887 // Events
4888 this.$input.on( {
4889 focus: this.onLookupInputFocus.bind( this ),
4890 blur: this.onLookupInputBlur.bind( this ),
4891 mousedown: this.onLookupInputMouseDown.bind( this )
4892 } );
4893 this.connect( this, { change: 'onLookupInputChange' } );
4894 this.lookupMenu.connect( this, {
4895 toggle: 'onLookupMenuToggle',
4896 choose: 'onLookupMenuItemChoose'
4897 } );
4898
4899 // Initialization
4900 this.$element.addClass( 'oo-ui-lookupElement' );
4901 this.lookupMenu.$element.addClass( 'oo-ui-lookupElement-menu' );
4902 this.$overlay.append( this.lookupMenu.$element );
4903 };
4904
4905 /* Methods */
4906
4907 /**
4908 * Handle input focus event.
4909 *
4910 * @param {jQuery.Event} e Input focus event
4911 */
4912 OO.ui.LookupElement.prototype.onLookupInputFocus = function () {
4913 this.lookupInputFocused = true;
4914 this.populateLookupMenu();
4915 };
4916
4917 /**
4918 * Handle input blur event.
4919 *
4920 * @param {jQuery.Event} e Input blur event
4921 */
4922 OO.ui.LookupElement.prototype.onLookupInputBlur = function () {
4923 this.closeLookupMenu();
4924 this.lookupInputFocused = false;
4925 };
4926
4927 /**
4928 * Handle input mouse down event.
4929 *
4930 * @param {jQuery.Event} e Input mouse down event
4931 */
4932 OO.ui.LookupElement.prototype.onLookupInputMouseDown = function () {
4933 // Only open the menu if the input was already focused.
4934 // This way we allow the user to open the menu again after closing it with Esc
4935 // by clicking in the input. Opening (and populating) the menu when initially
4936 // clicking into the input is handled by the focus handler.
4937 if ( this.lookupInputFocused && !this.lookupMenu.isVisible() ) {
4938 this.populateLookupMenu();
4939 }
4940 };
4941
4942 /**
4943 * Handle input change event.
4944 *
4945 * @param {string} value New input value
4946 */
4947 OO.ui.LookupElement.prototype.onLookupInputChange = function () {
4948 if ( this.lookupInputFocused ) {
4949 this.populateLookupMenu();
4950 }
4951 };
4952
4953 /**
4954 * Handle the lookup menu being shown/hidden.
4955 *
4956 * @param {boolean} visible Whether the lookup menu is now visible.
4957 */
4958 OO.ui.LookupElement.prototype.onLookupMenuToggle = function ( visible ) {
4959 if ( !visible ) {
4960 // When the menu is hidden, abort any active request and clear the menu.
4961 // This has to be done here in addition to closeLookupMenu(), because
4962 // MenuSelectWidget will close itself when the user presses Esc.
4963 this.abortLookupRequest();
4964 this.lookupMenu.clearItems();
4965 }
4966 };
4967
4968 /**
4969 * Handle menu item 'choose' event, updating the text input value to the value of the clicked item.
4970 *
4971 * @param {OO.ui.MenuOptionWidget|null} item Selected item
4972 */
4973 OO.ui.LookupElement.prototype.onLookupMenuItemChoose = function ( item ) {
4974 if ( item ) {
4975 this.setValue( item.getData() );
4976 }
4977 };
4978
4979 /**
4980 * Get lookup menu.
4981 *
4982 * @return {OO.ui.TextInputMenuSelectWidget}
4983 */
4984 OO.ui.LookupElement.prototype.getLookupMenu = function () {
4985 return this.lookupMenu;
4986 };
4987
4988 /**
4989 * Disable or re-enable lookups.
4990 *
4991 * When lookups are disabled, calls to #populateLookupMenu will be ignored.
4992 *
4993 * @param {boolean} disabled Disable lookups
4994 */
4995 OO.ui.LookupElement.prototype.setLookupsDisabled = function ( disabled ) {
4996 this.lookupsDisabled = !!disabled;
4997 };
4998
4999 /**
5000 * Open the menu. If there are no entries in the menu, this does nothing.
5001 *
5002 * @chainable
5003 */
5004 OO.ui.LookupElement.prototype.openLookupMenu = function () {
5005 if ( !this.lookupMenu.isEmpty() ) {
5006 this.lookupMenu.toggle( true );
5007 }
5008 return this;
5009 };
5010
5011 /**
5012 * Close the menu, empty it, and abort any pending request.
5013 *
5014 * @chainable
5015 */
5016 OO.ui.LookupElement.prototype.closeLookupMenu = function () {
5017 this.lookupMenu.toggle( false );
5018 this.abortLookupRequest();
5019 this.lookupMenu.clearItems();
5020 return this;
5021 };
5022
5023 /**
5024 * Request menu items based on the input's current value, and when they arrive,
5025 * populate the menu with these items and show the menu.
5026 *
5027 * If lookups have been disabled with #setLookupsDisabled, this function does nothing.
5028 *
5029 * @chainable
5030 */
5031 OO.ui.LookupElement.prototype.populateLookupMenu = function () {
5032 var widget = this,
5033 value = this.getValue();
5034
5035 if ( this.lookupsDisabled ) {
5036 return;
5037 }
5038
5039 // If the input is empty, clear the menu
5040 if ( value === '' ) {
5041 this.closeLookupMenu();
5042 // Skip population if there is already a request pending for the current value
5043 } else if ( value !== this.lookupQuery ) {
5044 this.getLookupMenuItems()
5045 .done( function ( items ) {
5046 widget.lookupMenu.clearItems();
5047 if ( items.length ) {
5048 widget.lookupMenu
5049 .addItems( items )
5050 .toggle( true );
5051 widget.initializeLookupMenuSelection();
5052 } else {
5053 widget.lookupMenu.toggle( false );
5054 }
5055 } )
5056 .fail( function () {
5057 widget.lookupMenu.clearItems();
5058 } );
5059 }
5060
5061 return this;
5062 };
5063
5064 /**
5065 * Select and highlight the first selectable item in the menu.
5066 *
5067 * @chainable
5068 */
5069 OO.ui.LookupElement.prototype.initializeLookupMenuSelection = function () {
5070 if ( !this.lookupMenu.getSelectedItem() ) {
5071 this.lookupMenu.selectItem( this.lookupMenu.getFirstSelectableItem() );
5072 }
5073 this.lookupMenu.highlightItem( this.lookupMenu.getSelectedItem() );
5074 };
5075
5076 /**
5077 * Get lookup menu items for the current query.
5078 *
5079 * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument of
5080 * the done event. If the request was aborted to make way for a subsequent request, this promise
5081 * will not be rejected: it will remain pending forever.
5082 */
5083 OO.ui.LookupElement.prototype.getLookupMenuItems = function () {
5084 var widget = this,
5085 value = this.getValue(),
5086 deferred = $.Deferred(),
5087 ourRequest;
5088
5089 this.abortLookupRequest();
5090 if ( Object.prototype.hasOwnProperty.call( this.lookupCache, value ) ) {
5091 deferred.resolve( this.getLookupMenuOptionsFromData( this.lookupCache[ value ] ) );
5092 } else {
5093 this.pushPending();
5094 this.lookupQuery = value;
5095 ourRequest = this.lookupRequest = this.getLookupRequest();
5096 ourRequest
5097 .always( function () {
5098 // We need to pop pending even if this is an old request, otherwise
5099 // the widget will remain pending forever.
5100 // TODO: this assumes that an aborted request will fail or succeed soon after
5101 // being aborted, or at least eventually. It would be nice if we could popPending()
5102 // at abort time, but only if we knew that we hadn't already called popPending()
5103 // for that request.
5104 widget.popPending();
5105 } )
5106 .done( function ( data ) {
5107 // If this is an old request (and aborting it somehow caused it to still succeed),
5108 // ignore its success completely
5109 if ( ourRequest === widget.lookupRequest ) {
5110 widget.lookupQuery = null;
5111 widget.lookupRequest = null;
5112 widget.lookupCache[ value ] = widget.getLookupCacheDataFromResponse( data );
5113 deferred.resolve( widget.getLookupMenuOptionsFromData( widget.lookupCache[ value ] ) );
5114 }
5115 } )
5116 .fail( function () {
5117 // If this is an old request (or a request failing because it's being aborted),
5118 // ignore its failure completely
5119 if ( ourRequest === widget.lookupRequest ) {
5120 widget.lookupQuery = null;
5121 widget.lookupRequest = null;
5122 deferred.reject();
5123 }
5124 } );
5125 }
5126 return deferred.promise();
5127 };
5128
5129 /**
5130 * Abort the currently pending lookup request, if any.
5131 */
5132 OO.ui.LookupElement.prototype.abortLookupRequest = function () {
5133 var oldRequest = this.lookupRequest;
5134 if ( oldRequest ) {
5135 // First unset this.lookupRequest to the fail handler will notice
5136 // that the request is no longer current
5137 this.lookupRequest = null;
5138 this.lookupQuery = null;
5139 oldRequest.abort();
5140 }
5141 };
5142
5143 /**
5144 * Get a new request object of the current lookup query value.
5145 *
5146 * @abstract
5147 * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
5148 */
5149 OO.ui.LookupElement.prototype.getLookupRequest = function () {
5150 // Stub, implemented in subclass
5151 return null;
5152 };
5153
5154 /**
5155 * Pre-process data returned by the request from #getLookupRequest.
5156 *
5157 * The return value of this function will be cached, and any further queries for the given value
5158 * will use the cache rather than doing API requests.
5159 *
5160 * @abstract
5161 * @param {Mixed} data Response from server
5162 * @return {Mixed} Cached result data
5163 */
5164 OO.ui.LookupElement.prototype.getLookupCacheDataFromResponse = function () {
5165 // Stub, implemented in subclass
5166 return [];
5167 };
5168
5169 /**
5170 * Get a list of menu option widgets from the (possibly cached) data returned by
5171 * #getLookupCacheDataFromResponse.
5172 *
5173 * @abstract
5174 * @param {Mixed} data Cached result data, usually an array
5175 * @return {OO.ui.MenuOptionWidget[]} Menu items
5176 */
5177 OO.ui.LookupElement.prototype.getLookupMenuOptionsFromData = function () {
5178 // Stub, implemented in subclass
5179 return [];
5180 };
5181
5182 /**
5183 * Element containing an OO.ui.PopupWidget object.
5184 *
5185 * @abstract
5186 * @class
5187 *
5188 * @constructor
5189 * @param {Object} [config] Configuration options
5190 * @cfg {Object} [popup] Configuration to pass to popup
5191 * @cfg {boolean} [popup.autoClose=true] Popup auto-closes when it loses focus
5192 */
5193 OO.ui.PopupElement = function OoUiPopupElement( config ) {
5194 // Configuration initialization
5195 config = config || {};
5196
5197 // Properties
5198 this.popup = new OO.ui.PopupWidget( $.extend(
5199 { autoClose: true },
5200 config.popup,
5201 { $autoCloseIgnore: this.$element }
5202 ) );
5203 };
5204
5205 /* Methods */
5206
5207 /**
5208 * Get popup.
5209 *
5210 * @return {OO.ui.PopupWidget} Popup widget
5211 */
5212 OO.ui.PopupElement.prototype.getPopup = function () {
5213 return this.popup;
5214 };
5215
5216 /**
5217 * Element with named flags that can be added, removed, listed and checked.
5218 *
5219 * A flag, when set, adds a CSS class on the `$element` by combining `oo-ui-flaggedElement-` with
5220 * the flag name. Flags are primarily useful for styling.
5221 *
5222 * @abstract
5223 * @class
5224 *
5225 * @constructor
5226 * @param {Object} [config] Configuration options
5227 * @cfg {string|string[]} [flags] Flags describing importance and functionality, e.g. 'primary',
5228 * 'safe', 'progressive', 'destructive' or 'constructive'
5229 * @cfg {jQuery} [$flagged] Flagged node, assigned to #$flagged, omit to use #$element
5230 */
5231 OO.ui.FlaggedElement = function OoUiFlaggedElement( config ) {
5232 // Configuration initialization
5233 config = config || {};
5234
5235 // Properties
5236 this.flags = {};
5237 this.$flagged = null;
5238
5239 // Initialization
5240 this.setFlags( config.flags );
5241 this.setFlaggedElement( config.$flagged || this.$element );
5242 };
5243
5244 /* Events */
5245
5246 /**
5247 * @event flag
5248 * @param {Object.<string,boolean>} changes Object keyed by flag name containing boolean
5249 * added/removed properties
5250 */
5251
5252 /* Methods */
5253
5254 /**
5255 * Set the flagged element.
5256 *
5257 * If an element is already set, it will be cleaned up before setting up the new element.
5258 *
5259 * @param {jQuery} $flagged Element to add flags to
5260 */
5261 OO.ui.FlaggedElement.prototype.setFlaggedElement = function ( $flagged ) {
5262 var classNames = Object.keys( this.flags ).map( function ( flag ) {
5263 return 'oo-ui-flaggedElement-' + flag;
5264 } ).join( ' ' );
5265
5266 if ( this.$flagged ) {
5267 this.$flagged.removeClass( classNames );
5268 }
5269
5270 this.$flagged = $flagged.addClass( classNames );
5271 };
5272
5273 /**
5274 * Check if a flag is set.
5275 *
5276 * @param {string} flag Name of flag
5277 * @return {boolean} Has flag
5278 */
5279 OO.ui.FlaggedElement.prototype.hasFlag = function ( flag ) {
5280 return flag in this.flags;
5281 };
5282
5283 /**
5284 * Get the names of all flags set.
5285 *
5286 * @return {string[]} Flag names
5287 */
5288 OO.ui.FlaggedElement.prototype.getFlags = function () {
5289 return Object.keys( this.flags );
5290 };
5291
5292 /**
5293 * Clear all flags.
5294 *
5295 * @chainable
5296 * @fires flag
5297 */
5298 OO.ui.FlaggedElement.prototype.clearFlags = function () {
5299 var flag, className,
5300 changes = {},
5301 remove = [],
5302 classPrefix = 'oo-ui-flaggedElement-';
5303
5304 for ( flag in this.flags ) {
5305 className = classPrefix + flag;
5306 changes[ flag ] = false;
5307 delete this.flags[ flag ];
5308 remove.push( className );
5309 }
5310
5311 if ( this.$flagged ) {
5312 this.$flagged.removeClass( remove.join( ' ' ) );
5313 }
5314
5315 this.updateThemeClasses();
5316 this.emit( 'flag', changes );
5317
5318 return this;
5319 };
5320
5321 /**
5322 * Add one or more flags.
5323 *
5324 * @param {string|string[]|Object.<string, boolean>} flags One or more flags to add, or an object
5325 * keyed by flag name containing boolean set/remove instructions.
5326 * @chainable
5327 * @fires flag
5328 */
5329 OO.ui.FlaggedElement.prototype.setFlags = function ( flags ) {
5330 var i, len, flag, className,
5331 changes = {},
5332 add = [],
5333 remove = [],
5334 classPrefix = 'oo-ui-flaggedElement-';
5335
5336 if ( typeof flags === 'string' ) {
5337 className = classPrefix + flags;
5338 // Set
5339 if ( !this.flags[ flags ] ) {
5340 this.flags[ flags ] = true;
5341 add.push( className );
5342 }
5343 } else if ( $.isArray( flags ) ) {
5344 for ( i = 0, len = flags.length; i < len; i++ ) {
5345 flag = flags[ i ];
5346 className = classPrefix + flag;
5347 // Set
5348 if ( !this.flags[ flag ] ) {
5349 changes[ flag ] = true;
5350 this.flags[ flag ] = true;
5351 add.push( className );
5352 }
5353 }
5354 } else if ( OO.isPlainObject( flags ) ) {
5355 for ( flag in flags ) {
5356 className = classPrefix + flag;
5357 if ( flags[ flag ] ) {
5358 // Set
5359 if ( !this.flags[ flag ] ) {
5360 changes[ flag ] = true;
5361 this.flags[ flag ] = true;
5362 add.push( className );
5363 }
5364 } else {
5365 // Remove
5366 if ( this.flags[ flag ] ) {
5367 changes[ flag ] = false;
5368 delete this.flags[ flag ];
5369 remove.push( className );
5370 }
5371 }
5372 }
5373 }
5374
5375 if ( this.$flagged ) {
5376 this.$flagged
5377 .addClass( add.join( ' ' ) )
5378 .removeClass( remove.join( ' ' ) );
5379 }
5380
5381 this.updateThemeClasses();
5382 this.emit( 'flag', changes );
5383
5384 return this;
5385 };
5386
5387 /**
5388 * Element with a title.
5389 *
5390 * Titles are rendered by the browser and are made visible when hovering the element. Titles are
5391 * not visible on touch devices.
5392 *
5393 * @abstract
5394 * @class
5395 *
5396 * @constructor
5397 * @param {Object} [config] Configuration options
5398 * @cfg {jQuery} [$titled] Titled node, assigned to #$titled, omit to use #$element
5399 * @cfg {string|Function} [title] Title text or a function that returns text. If not provided, the
5400 * static property 'title' is used.
5401 */
5402 OO.ui.TitledElement = function OoUiTitledElement( config ) {
5403 // Configuration initialization
5404 config = config || {};
5405
5406 // Properties
5407 this.$titled = null;
5408 this.title = null;
5409
5410 // Initialization
5411 this.setTitle( config.title || this.constructor.static.title );
5412 this.setTitledElement( config.$titled || this.$element );
5413 };
5414
5415 /* Setup */
5416
5417 OO.initClass( OO.ui.TitledElement );
5418
5419 /* Static Properties */
5420
5421 /**
5422 * Title.
5423 *
5424 * @static
5425 * @inheritable
5426 * @property {string|Function} Title text or a function that returns text
5427 */
5428 OO.ui.TitledElement.static.title = null;
5429
5430 /* Methods */
5431
5432 /**
5433 * Set the titled element.
5434 *
5435 * If an element is already set, it will be cleaned up before setting up the new element.
5436 *
5437 * @param {jQuery} $titled Element to set title on
5438 */
5439 OO.ui.TitledElement.prototype.setTitledElement = function ( $titled ) {
5440 if ( this.$titled ) {
5441 this.$titled.removeAttr( 'title' );
5442 }
5443
5444 this.$titled = $titled;
5445 if ( this.title ) {
5446 this.$titled.attr( 'title', this.title );
5447 }
5448 };
5449
5450 /**
5451 * Set title.
5452 *
5453 * @param {string|Function|null} title Title text, a function that returns text or null for no title
5454 * @chainable
5455 */
5456 OO.ui.TitledElement.prototype.setTitle = function ( title ) {
5457 title = typeof title === 'string' ? OO.ui.resolveMsg( title ) : null;
5458
5459 if ( this.title !== title ) {
5460 if ( this.$titled ) {
5461 if ( title !== null ) {
5462 this.$titled.attr( 'title', title );
5463 } else {
5464 this.$titled.removeAttr( 'title' );
5465 }
5466 }
5467 this.title = title;
5468 }
5469
5470 return this;
5471 };
5472
5473 /**
5474 * Get title.
5475 *
5476 * @return {string} Title string
5477 */
5478 OO.ui.TitledElement.prototype.getTitle = function () {
5479 return this.title;
5480 };
5481
5482 /**
5483 * Element that can be automatically clipped to visible boundaries.
5484 *
5485 * Whenever the element's natural height changes, you have to call
5486 * #clip to make sure it's still clipping correctly.
5487 *
5488 * @abstract
5489 * @class
5490 *
5491 * @constructor
5492 * @param {Object} [config] Configuration options
5493 * @cfg {jQuery} [$clippable] Nodes to clip, assigned to #$clippable, omit to use #$element
5494 */
5495 OO.ui.ClippableElement = function OoUiClippableElement( config ) {
5496 // Configuration initialization
5497 config = config || {};
5498
5499 // Properties
5500 this.$clippable = null;
5501 this.clipping = false;
5502 this.clippedHorizontally = false;
5503 this.clippedVertically = false;
5504 this.$clippableContainer = null;
5505 this.$clippableScroller = null;
5506 this.$clippableWindow = null;
5507 this.idealWidth = null;
5508 this.idealHeight = null;
5509 this.onClippableContainerScrollHandler = this.clip.bind( this );
5510 this.onClippableWindowResizeHandler = this.clip.bind( this );
5511
5512 // Initialization
5513 this.setClippableElement( config.$clippable || this.$element );
5514 };
5515
5516 /* Methods */
5517
5518 /**
5519 * Set clippable element.
5520 *
5521 * If an element is already set, it will be cleaned up before setting up the new element.
5522 *
5523 * @param {jQuery} $clippable Element to make clippable
5524 */
5525 OO.ui.ClippableElement.prototype.setClippableElement = function ( $clippable ) {
5526 if ( this.$clippable ) {
5527 this.$clippable.removeClass( 'oo-ui-clippableElement-clippable' );
5528 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
5529 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
5530 }
5531
5532 this.$clippable = $clippable.addClass( 'oo-ui-clippableElement-clippable' );
5533 this.clip();
5534 };
5535
5536 /**
5537 * Toggle clipping.
5538 *
5539 * Do not turn clipping on until after the element is attached to the DOM and visible.
5540 *
5541 * @param {boolean} [clipping] Enable clipping, omit to toggle
5542 * @chainable
5543 */
5544 OO.ui.ClippableElement.prototype.toggleClipping = function ( clipping ) {
5545 clipping = clipping === undefined ? !this.clipping : !!clipping;
5546
5547 if ( this.clipping !== clipping ) {
5548 this.clipping = clipping;
5549 if ( clipping ) {
5550 this.$clippableContainer = $( this.getClosestScrollableElementContainer() );
5551 // If the clippable container is the root, we have to listen to scroll events and check
5552 // jQuery.scrollTop on the window because of browser inconsistencies
5553 this.$clippableScroller = this.$clippableContainer.is( 'html, body' ) ?
5554 $( OO.ui.Element.static.getWindow( this.$clippableContainer ) ) :
5555 this.$clippableContainer;
5556 this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler );
5557 this.$clippableWindow = $( this.getElementWindow() )
5558 .on( 'resize', this.onClippableWindowResizeHandler );
5559 // Initial clip after visible
5560 this.clip();
5561 } else {
5562 this.$clippable.css( { width: '', height: '', overflowX: '', overflowY: '' } );
5563 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
5564
5565 this.$clippableContainer = null;
5566 this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler );
5567 this.$clippableScroller = null;
5568 this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
5569 this.$clippableWindow = null;
5570 }
5571 }
5572
5573 return this;
5574 };
5575
5576 /**
5577 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
5578 *
5579 * @return {boolean} Element will be clipped to the visible area
5580 */
5581 OO.ui.ClippableElement.prototype.isClipping = function () {
5582 return this.clipping;
5583 };
5584
5585 /**
5586 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
5587 *
5588 * @return {boolean} Part of the element is being clipped
5589 */
5590 OO.ui.ClippableElement.prototype.isClipped = function () {
5591 return this.clippedHorizontally || this.clippedVertically;
5592 };
5593
5594 /**
5595 * Check if the right of the element is being clipped by the nearest scrollable container.
5596 *
5597 * @return {boolean} Part of the element is being clipped
5598 */
5599 OO.ui.ClippableElement.prototype.isClippedHorizontally = function () {
5600 return this.clippedHorizontally;
5601 };
5602
5603 /**
5604 * Check if the bottom of the element is being clipped by the nearest scrollable container.
5605 *
5606 * @return {boolean} Part of the element is being clipped
5607 */
5608 OO.ui.ClippableElement.prototype.isClippedVertically = function () {
5609 return this.clippedVertically;
5610 };
5611
5612 /**
5613 * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
5614 *
5615 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
5616 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
5617 */
5618 OO.ui.ClippableElement.prototype.setIdealSize = function ( width, height ) {
5619 this.idealWidth = width;
5620 this.idealHeight = height;
5621
5622 if ( !this.clipping ) {
5623 // Update dimensions
5624 this.$clippable.css( { width: width, height: height } );
5625 }
5626 // While clipping, idealWidth and idealHeight are not considered
5627 };
5628
5629 /**
5630 * Clip element to visible boundaries and allow scrolling when needed. Call this method when
5631 * the element's natural height changes.
5632 *
5633 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
5634 * overlapped by, the visible area of the nearest scrollable container.
5635 *
5636 * @chainable
5637 */
5638 OO.ui.ClippableElement.prototype.clip = function () {
5639 if ( !this.clipping ) {
5640 // this.$clippableContainer and this.$clippableWindow are null, so the below will fail
5641 return this;
5642 }
5643
5644 var buffer = 7, // Chosen by fair dice roll
5645 cOffset = this.$clippable.offset(),
5646 $container = this.$clippableContainer.is( 'html, body' ) ?
5647 this.$clippableWindow : this.$clippableContainer,
5648 ccOffset = $container.offset() || { top: 0, left: 0 },
5649 ccHeight = $container.innerHeight() - buffer,
5650 ccWidth = $container.innerWidth() - buffer,
5651 cHeight = this.$clippable.outerHeight() + buffer,
5652 cWidth = this.$clippable.outerWidth() + buffer,
5653 scrollTop = this.$clippableScroller.scrollTop(),
5654 scrollLeft = this.$clippableScroller.scrollLeft(),
5655 desiredWidth = cOffset.left < 0 ?
5656 cWidth + cOffset.left :
5657 ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left,
5658 desiredHeight = cOffset.top < 0 ?
5659 cHeight + cOffset.top :
5660 ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top,
5661 naturalWidth = this.$clippable.prop( 'scrollWidth' ),
5662 naturalHeight = this.$clippable.prop( 'scrollHeight' ),
5663 clipWidth = desiredWidth < naturalWidth,
5664 clipHeight = desiredHeight < naturalHeight;
5665
5666 if ( clipWidth ) {
5667 this.$clippable.css( { overflowX: 'scroll', width: desiredWidth } );
5668 } else {
5669 this.$clippable.css( { width: this.idealWidth || '', overflowX: '' } );
5670 }
5671 if ( clipHeight ) {
5672 this.$clippable.css( { overflowY: 'scroll', height: desiredHeight } );
5673 } else {
5674 this.$clippable.css( { height: this.idealHeight || '', overflowY: '' } );
5675 }
5676
5677 // If we stopped clipping in at least one of the dimensions
5678 if ( !clipWidth || !clipHeight ) {
5679 OO.ui.Element.static.reconsiderScrollbars( this.$clippable[ 0 ] );
5680 }
5681
5682 this.clippedHorizontally = clipWidth;
5683 this.clippedVertically = clipHeight;
5684
5685 return this;
5686 };
5687
5688 /**
5689 * Generic toolbar tool.
5690 *
5691 * @abstract
5692 * @class
5693 * @extends OO.ui.Widget
5694 * @mixins OO.ui.IconElement
5695 * @mixins OO.ui.FlaggedElement
5696 *
5697 * @constructor
5698 * @param {OO.ui.ToolGroup} toolGroup
5699 * @param {Object} [config] Configuration options
5700 * @cfg {string|Function} [title] Title text or a function that returns text
5701 */
5702 OO.ui.Tool = function OoUiTool( toolGroup, config ) {
5703 // Configuration initialization
5704 config = config || {};
5705
5706 // Parent constructor
5707 OO.ui.Tool.super.call( this, config );
5708
5709 // Mixin constructors
5710 OO.ui.IconElement.call( this, config );
5711 OO.ui.FlaggedElement.call( this, config );
5712
5713 // Properties
5714 this.toolGroup = toolGroup;
5715 this.toolbar = this.toolGroup.getToolbar();
5716 this.active = false;
5717 this.$title = $( '<span>' );
5718 this.$accel = $( '<span>' );
5719 this.$link = $( '<a>' );
5720 this.title = null;
5721
5722 // Events
5723 this.toolbar.connect( this, { updateState: 'onUpdateState' } );
5724
5725 // Initialization
5726 this.$title.addClass( 'oo-ui-tool-title' );
5727 this.$accel
5728 .addClass( 'oo-ui-tool-accel' )
5729 .prop( {
5730 // This may need to be changed if the key names are ever localized,
5731 // but for now they are essentially written in English
5732 dir: 'ltr',
5733 lang: 'en'
5734 } );
5735 this.$link
5736 .addClass( 'oo-ui-tool-link' )
5737 .append( this.$icon, this.$title, this.$accel )
5738 .prop( 'tabIndex', 0 )
5739 .attr( 'role', 'button' );
5740 this.$element
5741 .data( 'oo-ui-tool', this )
5742 .addClass(
5743 'oo-ui-tool ' + 'oo-ui-tool-name-' +
5744 this.constructor.static.name.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' )
5745 )
5746 .append( this.$link );
5747 this.setTitle( config.title || this.constructor.static.title );
5748 };
5749
5750 /* Setup */
5751
5752 OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
5753 OO.mixinClass( OO.ui.Tool, OO.ui.IconElement );
5754 OO.mixinClass( OO.ui.Tool, OO.ui.FlaggedElement );
5755
5756 /* Events */
5757
5758 /**
5759 * @event select
5760 */
5761
5762 /* Static Properties */
5763
5764 /**
5765 * @static
5766 * @inheritdoc
5767 */
5768 OO.ui.Tool.static.tagName = 'span';
5769
5770 /**
5771 * Symbolic name of tool.
5772 *
5773 * @abstract
5774 * @static
5775 * @inheritable
5776 * @property {string}
5777 */
5778 OO.ui.Tool.static.name = '';
5779
5780 /**
5781 * Tool group.
5782 *
5783 * @abstract
5784 * @static
5785 * @inheritable
5786 * @property {string}
5787 */
5788 OO.ui.Tool.static.group = '';
5789
5790 /**
5791 * Tool title.
5792 *
5793 * Title is used as a tooltip when the tool is part of a bar tool group, or a label when the tool
5794 * is part of a list or menu tool group. If a trigger is associated with an action by the same name
5795 * as the tool, a description of its keyboard shortcut for the appropriate platform will be
5796 * appended to the title if the tool is part of a bar tool group.
5797 *
5798 * @abstract
5799 * @static
5800 * @inheritable
5801 * @property {string|Function} Title text or a function that returns text
5802 */
5803 OO.ui.Tool.static.title = '';
5804
5805 /**
5806 * Tool can be automatically added to catch-all groups.
5807 *
5808 * @static
5809 * @inheritable
5810 * @property {boolean}
5811 */
5812 OO.ui.Tool.static.autoAddToCatchall = true;
5813
5814 /**
5815 * Tool can be automatically added to named groups.
5816 *
5817 * @static
5818 * @property {boolean}
5819 * @inheritable
5820 */
5821 OO.ui.Tool.static.autoAddToGroup = true;
5822
5823 /**
5824 * Check if this tool is compatible with given data.
5825 *
5826 * @static
5827 * @inheritable
5828 * @param {Mixed} data Data to check
5829 * @return {boolean} Tool can be used with data
5830 */
5831 OO.ui.Tool.static.isCompatibleWith = function () {
5832 return false;
5833 };
5834
5835 /* Methods */
5836
5837 /**
5838 * Handle the toolbar state being updated.
5839 *
5840 * This is an abstract method that must be overridden in a concrete subclass.
5841 *
5842 * @abstract
5843 */
5844 OO.ui.Tool.prototype.onUpdateState = function () {
5845 throw new Error(
5846 'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor
5847 );
5848 };
5849
5850 /**
5851 * Handle the tool being selected.
5852 *
5853 * This is an abstract method that must be overridden in a concrete subclass.
5854 *
5855 * @abstract
5856 */
5857 OO.ui.Tool.prototype.onSelect = function () {
5858 throw new Error(
5859 'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor
5860 );
5861 };
5862
5863 /**
5864 * Check if the button is active.
5865 *
5866 * @return {boolean} Button is active
5867 */
5868 OO.ui.Tool.prototype.isActive = function () {
5869 return this.active;
5870 };
5871
5872 /**
5873 * Make the button appear active or inactive.
5874 *
5875 * @param {boolean} state Make button appear active
5876 */
5877 OO.ui.Tool.prototype.setActive = function ( state ) {
5878 this.active = !!state;
5879 if ( this.active ) {
5880 this.$element.addClass( 'oo-ui-tool-active' );
5881 } else {
5882 this.$element.removeClass( 'oo-ui-tool-active' );
5883 }
5884 };
5885
5886 /**
5887 * Get the tool title.
5888 *
5889 * @param {string|Function} title Title text or a function that returns text
5890 * @chainable
5891 */
5892 OO.ui.Tool.prototype.setTitle = function ( title ) {
5893 this.title = OO.ui.resolveMsg( title );
5894 this.updateTitle();
5895 return this;
5896 };
5897
5898 /**
5899 * Get the tool title.
5900 *
5901 * @return {string} Title text
5902 */
5903 OO.ui.Tool.prototype.getTitle = function () {
5904 return this.title;
5905 };
5906
5907 /**
5908 * Get the tool's symbolic name.
5909 *
5910 * @return {string} Symbolic name of tool
5911 */
5912 OO.ui.Tool.prototype.getName = function () {
5913 return this.constructor.static.name;
5914 };
5915
5916 /**
5917 * Update the title.
5918 */
5919 OO.ui.Tool.prototype.updateTitle = function () {
5920 var titleTooltips = this.toolGroup.constructor.static.titleTooltips,
5921 accelTooltips = this.toolGroup.constructor.static.accelTooltips,
5922 accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
5923 tooltipParts = [];
5924
5925 this.$title.text( this.title );
5926 this.$accel.text( accel );
5927
5928 if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
5929 tooltipParts.push( this.title );
5930 }
5931 if ( accelTooltips && typeof accel === 'string' && accel.length ) {
5932 tooltipParts.push( accel );
5933 }
5934 if ( tooltipParts.length ) {
5935 this.$link.attr( 'title', tooltipParts.join( ' ' ) );
5936 } else {
5937 this.$link.removeAttr( 'title' );
5938 }
5939 };
5940
5941 /**
5942 * Destroy tool.
5943 */
5944 OO.ui.Tool.prototype.destroy = function () {
5945 this.toolbar.disconnect( this );
5946 this.$element.remove();
5947 };
5948
5949 /**
5950 * Collection of tool groups.
5951 *
5952 * @class
5953 * @extends OO.ui.Element
5954 * @mixins OO.EventEmitter
5955 * @mixins OO.ui.GroupElement
5956 *
5957 * @constructor
5958 * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
5959 * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating tool groups
5960 * @param {Object} [config] Configuration options
5961 * @cfg {boolean} [actions] Add an actions section opposite to the tools
5962 * @cfg {boolean} [shadow] Add a shadow below the toolbar
5963 */
5964 OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
5965 // Configuration initialization
5966 config = config || {};
5967
5968 // Parent constructor
5969 OO.ui.Toolbar.super.call( this, config );
5970
5971 // Mixin constructors
5972 OO.EventEmitter.call( this );
5973 OO.ui.GroupElement.call( this, config );
5974
5975 // Properties
5976 this.toolFactory = toolFactory;
5977 this.toolGroupFactory = toolGroupFactory;
5978 this.groups = [];
5979 this.tools = {};
5980 this.$bar = $( '<div>' );
5981 this.$actions = $( '<div>' );
5982 this.initialized = false;
5983
5984 // Events
5985 this.$element
5986 .add( this.$bar ).add( this.$group ).add( this.$actions )
5987 .on( 'mousedown touchstart', this.onPointerDown.bind( this ) );
5988
5989 // Initialization
5990 this.$group.addClass( 'oo-ui-toolbar-tools' );
5991 if ( config.actions ) {
5992 this.$bar.append( this.$actions.addClass( 'oo-ui-toolbar-actions' ) );
5993 }
5994 this.$bar
5995 .addClass( 'oo-ui-toolbar-bar' )
5996 .append( this.$group, '<div style="clear:both"></div>' );
5997 if ( config.shadow ) {
5998 this.$bar.append( '<div class="oo-ui-toolbar-shadow"></div>' );
5999 }
6000 this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar );
6001 };
6002
6003 /* Setup */
6004
6005 OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
6006 OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
6007 OO.mixinClass( OO.ui.Toolbar, OO.ui.GroupElement );
6008
6009 /* Methods */
6010
6011 /**
6012 * Get the tool factory.
6013 *
6014 * @return {OO.ui.ToolFactory} Tool factory
6015 */
6016 OO.ui.Toolbar.prototype.getToolFactory = function () {
6017 return this.toolFactory;
6018 };
6019
6020 /**
6021 * Get the tool group factory.
6022 *
6023 * @return {OO.Factory} Tool group factory
6024 */
6025 OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
6026 return this.toolGroupFactory;
6027 };
6028
6029 /**
6030 * Handles mouse down events.
6031 *
6032 * @param {jQuery.Event} e Mouse down event
6033 */
6034 OO.ui.Toolbar.prototype.onPointerDown = function ( e ) {
6035 var $closestWidgetToEvent = $( e.target ).closest( '.oo-ui-widget' ),
6036 $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
6037 if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[ 0 ] === $closestWidgetToToolbar[ 0 ] ) {
6038 return false;
6039 }
6040 };
6041
6042 /**
6043 * Sets up handles and preloads required information for the toolbar to work.
6044 * This must be called after it is attached to a visible document and before doing anything else.
6045 */
6046 OO.ui.Toolbar.prototype.initialize = function () {
6047 this.initialized = true;
6048 };
6049
6050 /**
6051 * Setup toolbar.
6052 *
6053 * Tools can be specified in the following ways:
6054 *
6055 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
6056 * - All tools in a group: `{ group: 'group-name' }`
6057 * - All tools: `'*'` - Using this will make the group a list with a "More" label by default
6058 *
6059 * @param {Object.<string,Array>} groups List of tool group configurations
6060 * @param {Array|string} [groups.include] Tools to include
6061 * @param {Array|string} [groups.exclude] Tools to exclude
6062 * @param {Array|string} [groups.promote] Tools to promote to the beginning
6063 * @param {Array|string} [groups.demote] Tools to demote to the end
6064 */
6065 OO.ui.Toolbar.prototype.setup = function ( groups ) {
6066 var i, len, type, group,
6067 items = [],
6068 defaultType = 'bar';
6069
6070 // Cleanup previous groups
6071 this.reset();
6072
6073 // Build out new groups
6074 for ( i = 0, len = groups.length; i < len; i++ ) {
6075 group = groups[ i ];
6076 if ( group.include === '*' ) {
6077 // Apply defaults to catch-all groups
6078 if ( group.type === undefined ) {
6079 group.type = 'list';
6080 }
6081 if ( group.label === undefined ) {
6082 group.label = OO.ui.msg( 'ooui-toolbar-more' );
6083 }
6084 }
6085 // Check type has been registered
6086 type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType;
6087 items.push(
6088 this.getToolGroupFactory().create( type, this, group )
6089 );
6090 }
6091 this.addItems( items );
6092 };
6093
6094 /**
6095 * Remove all tools and groups from the toolbar.
6096 */
6097 OO.ui.Toolbar.prototype.reset = function () {
6098 var i, len;
6099
6100 this.groups = [];
6101 this.tools = {};
6102 for ( i = 0, len = this.items.length; i < len; i++ ) {
6103 this.items[ i ].destroy();
6104 }
6105 this.clearItems();
6106 };
6107
6108 /**
6109 * Destroys toolbar, removing event handlers and DOM elements.
6110 *
6111 * Call this whenever you are done using a toolbar.
6112 */
6113 OO.ui.Toolbar.prototype.destroy = function () {
6114 this.reset();
6115 this.$element.remove();
6116 };
6117
6118 /**
6119 * Check if tool has not been used yet.
6120 *
6121 * @param {string} name Symbolic name of tool
6122 * @return {boolean} Tool is available
6123 */
6124 OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
6125 return !this.tools[ name ];
6126 };
6127
6128 /**
6129 * Prevent tool from being used again.
6130 *
6131 * @param {OO.ui.Tool} tool Tool to reserve
6132 */
6133 OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
6134 this.tools[ tool.getName() ] = tool;
6135 };
6136
6137 /**
6138 * Allow tool to be used again.
6139 *
6140 * @param {OO.ui.Tool} tool Tool to release
6141 */
6142 OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
6143 delete this.tools[ tool.getName() ];
6144 };
6145
6146 /**
6147 * Get accelerator label for tool.
6148 *
6149 * This is a stub that should be overridden to provide access to accelerator information.
6150 *
6151 * @param {string} name Symbolic name of tool
6152 * @return {string|undefined} Tool accelerator label if available
6153 */
6154 OO.ui.Toolbar.prototype.getToolAccelerator = function () {
6155 return undefined;
6156 };
6157
6158 /**
6159 * Collection of tools.
6160 *
6161 * Tools can be specified in the following ways:
6162 *
6163 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
6164 * - All tools in a group: `{ group: 'group-name' }`
6165 * - All tools: `'*'`
6166 *
6167 * @abstract
6168 * @class
6169 * @extends OO.ui.Widget
6170 * @mixins OO.ui.GroupElement
6171 *
6172 * @constructor
6173 * @param {OO.ui.Toolbar} toolbar
6174 * @param {Object} [config] Configuration options
6175 * @cfg {Array|string} [include=[]] List of tools to include
6176 * @cfg {Array|string} [exclude=[]] List of tools to exclude
6177 * @cfg {Array|string} [promote=[]] List of tools to promote to the beginning
6178 * @cfg {Array|string} [demote=[]] List of tools to demote to the end
6179 */
6180 OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
6181 // Configuration initialization
6182 config = config || {};
6183
6184 // Parent constructor
6185 OO.ui.ToolGroup.super.call( this, config );
6186
6187 // Mixin constructors
6188 OO.ui.GroupElement.call( this, config );
6189
6190 // Properties
6191 this.toolbar = toolbar;
6192 this.tools = {};
6193 this.pressed = null;
6194 this.autoDisabled = false;
6195 this.include = config.include || [];
6196 this.exclude = config.exclude || [];
6197 this.promote = config.promote || [];
6198 this.demote = config.demote || [];
6199 this.onCapturedMouseUpHandler = this.onCapturedMouseUp.bind( this );
6200
6201 // Events
6202 this.$element.on( {
6203 'mousedown touchstart': this.onPointerDown.bind( this ),
6204 'mouseup touchend': this.onPointerUp.bind( this ),
6205 mouseover: this.onMouseOver.bind( this ),
6206 mouseout: this.onMouseOut.bind( this )
6207 } );
6208 this.toolbar.getToolFactory().connect( this, { register: 'onToolFactoryRegister' } );
6209 this.aggregate( { disable: 'itemDisable' } );
6210 this.connect( this, { itemDisable: 'updateDisabled' } );
6211
6212 // Initialization
6213 this.$group.addClass( 'oo-ui-toolGroup-tools' );
6214 this.$element
6215 .addClass( 'oo-ui-toolGroup' )
6216 .append( this.$group );
6217 this.populate();
6218 };
6219
6220 /* Setup */
6221
6222 OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget );
6223 OO.mixinClass( OO.ui.ToolGroup, OO.ui.GroupElement );
6224
6225 /* Events */
6226
6227 /**
6228 * @event update
6229 */
6230
6231 /* Static Properties */
6232
6233 /**
6234 * Show labels in tooltips.
6235 *
6236 * @static
6237 * @inheritable
6238 * @property {boolean}
6239 */
6240 OO.ui.ToolGroup.static.titleTooltips = false;
6241
6242 /**
6243 * Show acceleration labels in tooltips.
6244 *
6245 * @static
6246 * @inheritable
6247 * @property {boolean}
6248 */
6249 OO.ui.ToolGroup.static.accelTooltips = false;
6250
6251 /**
6252 * Automatically disable the toolgroup when all tools are disabled
6253 *
6254 * @static
6255 * @inheritable
6256 * @property {boolean}
6257 */
6258 OO.ui.ToolGroup.static.autoDisable = true;
6259
6260 /* Methods */
6261
6262 /**
6263 * @inheritdoc
6264 */
6265 OO.ui.ToolGroup.prototype.isDisabled = function () {
6266 return this.autoDisabled || OO.ui.ToolGroup.super.prototype.isDisabled.apply( this, arguments );
6267 };
6268
6269 /**
6270 * @inheritdoc
6271 */
6272 OO.ui.ToolGroup.prototype.updateDisabled = function () {
6273 var i, item, allDisabled = true;
6274
6275 if ( this.constructor.static.autoDisable ) {
6276 for ( i = this.items.length - 1; i >= 0; i-- ) {
6277 item = this.items[ i ];
6278 if ( !item.isDisabled() ) {
6279 allDisabled = false;
6280 break;
6281 }
6282 }
6283 this.autoDisabled = allDisabled;
6284 }
6285 OO.ui.ToolGroup.super.prototype.updateDisabled.apply( this, arguments );
6286 };
6287
6288 /**
6289 * Handle mouse down events.
6290 *
6291 * @param {jQuery.Event} e Mouse down event
6292 */
6293 OO.ui.ToolGroup.prototype.onPointerDown = function ( e ) {
6294 // e.which is 0 for touch events, 1 for left mouse button
6295 if ( !this.isDisabled() && e.which <= 1 ) {
6296 this.pressed = this.getTargetTool( e );
6297 if ( this.pressed ) {
6298 this.pressed.setActive( true );
6299 this.getElementDocument().addEventListener(
6300 'mouseup', this.onCapturedMouseUpHandler, true
6301 );
6302 }
6303 }
6304 return false;
6305 };
6306
6307 /**
6308 * Handle captured mouse up events.
6309 *
6310 * @param {Event} e Mouse up event
6311 */
6312 OO.ui.ToolGroup.prototype.onCapturedMouseUp = function ( e ) {
6313 this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseUpHandler, true );
6314 // onPointerUp may be called a second time, depending on where the mouse is when the button is
6315 // released, but since `this.pressed` will no longer be true, the second call will be ignored.
6316 this.onPointerUp( e );
6317 };
6318
6319 /**
6320 * Handle mouse up events.
6321 *
6322 * @param {jQuery.Event} e Mouse up event
6323 */
6324 OO.ui.ToolGroup.prototype.onPointerUp = function ( e ) {
6325 var tool = this.getTargetTool( e );
6326
6327 // e.which is 0 for touch events, 1 for left mouse button
6328 if ( !this.isDisabled() && e.which <= 1 && this.pressed && this.pressed === tool ) {
6329 this.pressed.onSelect();
6330 }
6331
6332 this.pressed = null;
6333 return false;
6334 };
6335
6336 /**
6337 * Handle mouse over events.
6338 *
6339 * @param {jQuery.Event} e Mouse over event
6340 */
6341 OO.ui.ToolGroup.prototype.onMouseOver = function ( e ) {
6342 var tool = this.getTargetTool( e );
6343
6344 if ( this.pressed && this.pressed === tool ) {
6345 this.pressed.setActive( true );
6346 }
6347 };
6348
6349 /**
6350 * Handle mouse out events.
6351 *
6352 * @param {jQuery.Event} e Mouse out event
6353 */
6354 OO.ui.ToolGroup.prototype.onMouseOut = function ( e ) {
6355 var tool = this.getTargetTool( e );
6356
6357 if ( this.pressed && this.pressed === tool ) {
6358 this.pressed.setActive( false );
6359 }
6360 };
6361
6362 /**
6363 * Get the closest tool to a jQuery.Event.
6364 *
6365 * Only tool links are considered, which prevents other elements in the tool such as popups from
6366 * triggering tool group interactions.
6367 *
6368 * @private
6369 * @param {jQuery.Event} e
6370 * @return {OO.ui.Tool|null} Tool, `null` if none was found
6371 */
6372 OO.ui.ToolGroup.prototype.getTargetTool = function ( e ) {
6373 var tool,
6374 $item = $( e.target ).closest( '.oo-ui-tool-link' );
6375
6376 if ( $item.length ) {
6377 tool = $item.parent().data( 'oo-ui-tool' );
6378 }
6379
6380 return tool && !tool.isDisabled() ? tool : null;
6381 };
6382
6383 /**
6384 * Handle tool registry register events.
6385 *
6386 * If a tool is registered after the group is created, we must repopulate the list to account for:
6387 *
6388 * - a tool being added that may be included
6389 * - a tool already included being overridden
6390 *
6391 * @param {string} name Symbolic name of tool
6392 */
6393 OO.ui.ToolGroup.prototype.onToolFactoryRegister = function () {
6394 this.populate();
6395 };
6396
6397 /**
6398 * Get the toolbar this group is in.
6399 *
6400 * @return {OO.ui.Toolbar} Toolbar of group
6401 */
6402 OO.ui.ToolGroup.prototype.getToolbar = function () {
6403 return this.toolbar;
6404 };
6405
6406 /**
6407 * Add and remove tools based on configuration.
6408 */
6409 OO.ui.ToolGroup.prototype.populate = function () {
6410 var i, len, name, tool,
6411 toolFactory = this.toolbar.getToolFactory(),
6412 names = {},
6413 add = [],
6414 remove = [],
6415 list = this.toolbar.getToolFactory().getTools(
6416 this.include, this.exclude, this.promote, this.demote
6417 );
6418
6419 // Build a list of needed tools
6420 for ( i = 0, len = list.length; i < len; i++ ) {
6421 name = list[ i ];
6422 if (
6423 // Tool exists
6424 toolFactory.lookup( name ) &&
6425 // Tool is available or is already in this group
6426 ( this.toolbar.isToolAvailable( name ) || this.tools[ name ] )
6427 ) {
6428 tool = this.tools[ name ];
6429 if ( !tool ) {
6430 // Auto-initialize tools on first use
6431 this.tools[ name ] = tool = toolFactory.create( name, this );
6432 tool.updateTitle();
6433 }
6434 this.toolbar.reserveTool( tool );
6435 add.push( tool );
6436 names[ name ] = true;
6437 }
6438 }
6439 // Remove tools that are no longer needed
6440 for ( name in this.tools ) {
6441 if ( !names[ name ] ) {
6442 this.tools[ name ].destroy();
6443 this.toolbar.releaseTool( this.tools[ name ] );
6444 remove.push( this.tools[ name ] );
6445 delete this.tools[ name ];
6446 }
6447 }
6448 if ( remove.length ) {
6449 this.removeItems( remove );
6450 }
6451 // Update emptiness state
6452 if ( add.length ) {
6453 this.$element.removeClass( 'oo-ui-toolGroup-empty' );
6454 } else {
6455 this.$element.addClass( 'oo-ui-toolGroup-empty' );
6456 }
6457 // Re-add tools (moving existing ones to new locations)
6458 this.addItems( add );
6459 // Disabled state may depend on items
6460 this.updateDisabled();
6461 };
6462
6463 /**
6464 * Destroy tool group.
6465 */
6466 OO.ui.ToolGroup.prototype.destroy = function () {
6467 var name;
6468
6469 this.clearItems();
6470 this.toolbar.getToolFactory().disconnect( this );
6471 for ( name in this.tools ) {
6472 this.toolbar.releaseTool( this.tools[ name ] );
6473 this.tools[ name ].disconnect( this ).destroy();
6474 delete this.tools[ name ];
6475 }
6476 this.$element.remove();
6477 };
6478
6479 /**
6480 * Dialog for showing a message.
6481 *
6482 * User interface:
6483 * - Registers two actions by default (safe and primary).
6484 * - Renders action widgets in the footer.
6485 *
6486 * @class
6487 * @extends OO.ui.Dialog
6488 *
6489 * @constructor
6490 * @param {Object} [config] Configuration options
6491 */
6492 OO.ui.MessageDialog = function OoUiMessageDialog( config ) {
6493 // Parent constructor
6494 OO.ui.MessageDialog.super.call( this, config );
6495
6496 // Properties
6497 this.verticalActionLayout = null;
6498
6499 // Initialization
6500 this.$element.addClass( 'oo-ui-messageDialog' );
6501 };
6502
6503 /* Inheritance */
6504
6505 OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog );
6506
6507 /* Static Properties */
6508
6509 OO.ui.MessageDialog.static.name = 'message';
6510
6511 OO.ui.MessageDialog.static.size = 'small';
6512
6513 OO.ui.MessageDialog.static.verbose = false;
6514
6515 /**
6516 * Dialog title.
6517 *
6518 * A confirmation dialog's title should describe what the progressive action will do. An alert
6519 * dialog's title should describe what event occurred.
6520 *
6521 * @static
6522 * inheritable
6523 * @property {jQuery|string|Function|null}
6524 */
6525 OO.ui.MessageDialog.static.title = null;
6526
6527 /**
6528 * A confirmation dialog's message should describe the consequences of the progressive action. An
6529 * alert dialog's message should describe why the event occurred.
6530 *
6531 * @static
6532 * inheritable
6533 * @property {jQuery|string|Function|null}
6534 */
6535 OO.ui.MessageDialog.static.message = null;
6536
6537 OO.ui.MessageDialog.static.actions = [
6538 { action: 'accept', label: OO.ui.deferMsg( 'ooui-dialog-message-accept' ), flags: 'primary' },
6539 { action: 'reject', label: OO.ui.deferMsg( 'ooui-dialog-message-reject' ), flags: 'safe' }
6540 ];
6541
6542 /* Methods */
6543
6544 /**
6545 * @inheritdoc
6546 */
6547 OO.ui.MessageDialog.prototype.setManager = function ( manager ) {
6548 OO.ui.MessageDialog.super.prototype.setManager.call( this, manager );
6549
6550 // Events
6551 this.manager.connect( this, {
6552 resize: 'onResize'
6553 } );
6554
6555 return this;
6556 };
6557
6558 /**
6559 * @inheritdoc
6560 */
6561 OO.ui.MessageDialog.prototype.onActionResize = function ( action ) {
6562 this.fitActions();
6563 return OO.ui.MessageDialog.super.prototype.onActionResize.call( this, action );
6564 };
6565
6566 /**
6567 * Handle window resized events.
6568 */
6569 OO.ui.MessageDialog.prototype.onResize = function () {
6570 var dialog = this;
6571 dialog.fitActions();
6572 // Wait for CSS transition to finish and do it again :(
6573 setTimeout( function () {
6574 dialog.fitActions();
6575 }, 300 );
6576 };
6577
6578 /**
6579 * Toggle action layout between vertical and horizontal.
6580 *
6581 * @param {boolean} [value] Layout actions vertically, omit to toggle
6582 * @chainable
6583 */
6584 OO.ui.MessageDialog.prototype.toggleVerticalActionLayout = function ( value ) {
6585 value = value === undefined ? !this.verticalActionLayout : !!value;
6586
6587 if ( value !== this.verticalActionLayout ) {
6588 this.verticalActionLayout = value;
6589 this.$actions
6590 .toggleClass( 'oo-ui-messageDialog-actions-vertical', value )
6591 .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value );
6592 }
6593
6594 return this;
6595 };
6596
6597 /**
6598 * @inheritdoc
6599 */
6600 OO.ui.MessageDialog.prototype.getActionProcess = function ( action ) {
6601 if ( action ) {
6602 return new OO.ui.Process( function () {
6603 this.close( { action: action } );
6604 }, this );
6605 }
6606 return OO.ui.MessageDialog.super.prototype.getActionProcess.call( this, action );
6607 };
6608
6609 /**
6610 * @inheritdoc
6611 *
6612 * @param {Object} [data] Dialog opening data
6613 * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
6614 * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
6615 * @param {boolean} [data.verbose] Message is verbose and should be styled as a long message
6616 * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each
6617 * action item
6618 */
6619 OO.ui.MessageDialog.prototype.getSetupProcess = function ( data ) {
6620 data = data || {};
6621
6622 // Parent method
6623 return OO.ui.MessageDialog.super.prototype.getSetupProcess.call( this, data )
6624 .next( function () {
6625 this.title.setLabel(
6626 data.title !== undefined ? data.title : this.constructor.static.title
6627 );
6628 this.message.setLabel(
6629 data.message !== undefined ? data.message : this.constructor.static.message
6630 );
6631 this.message.$element.toggleClass(
6632 'oo-ui-messageDialog-message-verbose',
6633 data.verbose !== undefined ? data.verbose : this.constructor.static.verbose
6634 );
6635 }, this );
6636 };
6637
6638 /**
6639 * @inheritdoc
6640 */
6641 OO.ui.MessageDialog.prototype.getBodyHeight = function () {
6642 var bodyHeight, oldOverflow,
6643 $scrollable = this.container.$element;
6644
6645 oldOverflow = $scrollable[ 0 ].style.overflow;
6646 $scrollable[ 0 ].style.overflow = 'hidden';
6647
6648 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
6649
6650 bodyHeight = this.text.$element.outerHeight( true );
6651 $scrollable[ 0 ].style.overflow = oldOverflow;
6652
6653 return bodyHeight;
6654 };
6655
6656 /**
6657 * @inheritdoc
6658 */
6659 OO.ui.MessageDialog.prototype.setDimensions = function ( dim ) {
6660 var $scrollable = this.container.$element;
6661 OO.ui.MessageDialog.super.prototype.setDimensions.call( this, dim );
6662
6663 // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
6664 // Need to do it after transition completes (250ms), add 50ms just in case.
6665 setTimeout( function () {
6666 var oldOverflow = $scrollable[ 0 ].style.overflow;
6667 $scrollable[ 0 ].style.overflow = 'hidden';
6668
6669 OO.ui.Element.static.reconsiderScrollbars( $scrollable[ 0 ] );
6670
6671 $scrollable[ 0 ].style.overflow = oldOverflow;
6672 }, 300 );
6673
6674 return this;
6675 };
6676
6677 /**
6678 * @inheritdoc
6679 */
6680 OO.ui.MessageDialog.prototype.initialize = function () {
6681 // Parent method
6682 OO.ui.MessageDialog.super.prototype.initialize.call( this );
6683
6684 // Properties
6685 this.$actions = $( '<div>' );
6686 this.container = new OO.ui.PanelLayout( {
6687 scrollable: true, classes: [ 'oo-ui-messageDialog-container' ]
6688 } );
6689 this.text = new OO.ui.PanelLayout( {
6690 padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ]
6691 } );
6692 this.message = new OO.ui.LabelWidget( {
6693 classes: [ 'oo-ui-messageDialog-message' ]
6694 } );
6695
6696 // Initialization
6697 this.title.$element.addClass( 'oo-ui-messageDialog-title' );
6698 this.$content.addClass( 'oo-ui-messageDialog-content' );
6699 this.container.$element.append( this.text.$element );
6700 this.text.$element.append( this.title.$element, this.message.$element );
6701 this.$body.append( this.container.$element );
6702 this.$actions.addClass( 'oo-ui-messageDialog-actions' );
6703 this.$foot.append( this.$actions );
6704 };
6705
6706 /**
6707 * @inheritdoc
6708 */
6709 OO.ui.MessageDialog.prototype.attachActions = function () {
6710 var i, len, other, special, others;
6711
6712 // Parent method
6713 OO.ui.MessageDialog.super.prototype.attachActions.call( this );
6714
6715 special = this.actions.getSpecial();
6716 others = this.actions.getOthers();
6717 if ( special.safe ) {
6718 this.$actions.append( special.safe.$element );
6719 special.safe.toggleFramed( false );
6720 }
6721 if ( others.length ) {
6722 for ( i = 0, len = others.length; i < len; i++ ) {
6723 other = others[ i ];
6724 this.$actions.append( other.$element );
6725 other.toggleFramed( false );
6726 }
6727 }
6728 if ( special.primary ) {
6729 this.$actions.append( special.primary.$element );
6730 special.primary.toggleFramed( false );
6731 }
6732
6733 if ( !this.isOpening() ) {
6734 // If the dialog is currently opening, this will be called automatically soon.
6735 // This also calls #fitActions.
6736 this.updateSize();
6737 }
6738 };
6739
6740 /**
6741 * Fit action actions into columns or rows.
6742 *
6743 * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
6744 */
6745 OO.ui.MessageDialog.prototype.fitActions = function () {
6746 var i, len, action,
6747 previous = this.verticalActionLayout,
6748 actions = this.actions.get();
6749
6750 // Detect clipping
6751 this.toggleVerticalActionLayout( false );
6752 for ( i = 0, len = actions.length; i < len; i++ ) {
6753 action = actions[ i ];
6754 if ( action.$element.innerWidth() < action.$label.outerWidth( true ) ) {
6755 this.toggleVerticalActionLayout( true );
6756 break;
6757 }
6758 }
6759
6760 // Move the body out of the way of the foot
6761 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
6762
6763 if ( this.verticalActionLayout !== previous ) {
6764 // We changed the layout, window height might need to be updated.
6765 this.updateSize();
6766 }
6767 };
6768
6769 /**
6770 * Navigation dialog window.
6771 *
6772 * Logic:
6773 * - Show and hide errors.
6774 * - Retry an action.
6775 *
6776 * User interface:
6777 * - Renders header with dialog title and one action widget on either side
6778 * (a 'safe' button on the left, and a 'primary' button on the right, both of
6779 * which close the dialog).
6780 * - Displays any action widgets in the footer (none by default).
6781 * - Ability to dismiss errors.
6782 *
6783 * Subclass responsibilities:
6784 * - Register a 'safe' action.
6785 * - Register a 'primary' action.
6786 * - Add content to the dialog.
6787 *
6788 * @abstract
6789 * @class
6790 * @extends OO.ui.Dialog
6791 *
6792 * @constructor
6793 * @param {Object} [config] Configuration options
6794 */
6795 OO.ui.ProcessDialog = function OoUiProcessDialog( config ) {
6796 // Parent constructor
6797 OO.ui.ProcessDialog.super.call( this, config );
6798
6799 // Initialization
6800 this.$element.addClass( 'oo-ui-processDialog' );
6801 };
6802
6803 /* Setup */
6804
6805 OO.inheritClass( OO.ui.ProcessDialog, OO.ui.Dialog );
6806
6807 /* Methods */
6808
6809 /**
6810 * Handle dismiss button click events.
6811 *
6812 * Hides errors.
6813 */
6814 OO.ui.ProcessDialog.prototype.onDismissErrorButtonClick = function () {
6815 this.hideErrors();
6816 };
6817
6818 /**
6819 * Handle retry button click events.
6820 *
6821 * Hides errors and then tries again.
6822 */
6823 OO.ui.ProcessDialog.prototype.onRetryButtonClick = function () {
6824 this.hideErrors();
6825 this.executeAction( this.currentAction.getAction() );
6826 };
6827
6828 /**
6829 * @inheritdoc
6830 */
6831 OO.ui.ProcessDialog.prototype.onActionResize = function ( action ) {
6832 if ( this.actions.isSpecial( action ) ) {
6833 this.fitLabel();
6834 }
6835 return OO.ui.ProcessDialog.super.prototype.onActionResize.call( this, action );
6836 };
6837
6838 /**
6839 * @inheritdoc
6840 */
6841 OO.ui.ProcessDialog.prototype.initialize = function () {
6842 // Parent method
6843 OO.ui.ProcessDialog.super.prototype.initialize.call( this );
6844
6845 // Properties
6846 this.$navigation = $( '<div>' );
6847 this.$location = $( '<div>' );
6848 this.$safeActions = $( '<div>' );
6849 this.$primaryActions = $( '<div>' );
6850 this.$otherActions = $( '<div>' );
6851 this.dismissButton = new OO.ui.ButtonWidget( {
6852 label: OO.ui.msg( 'ooui-dialog-process-dismiss' )
6853 } );
6854 this.retryButton = new OO.ui.ButtonWidget();
6855 this.$errors = $( '<div>' );
6856 this.$errorsTitle = $( '<div>' );
6857
6858 // Events
6859 this.dismissButton.connect( this, { click: 'onDismissErrorButtonClick' } );
6860 this.retryButton.connect( this, { click: 'onRetryButtonClick' } );
6861
6862 // Initialization
6863 this.title.$element.addClass( 'oo-ui-processDialog-title' );
6864 this.$location
6865 .append( this.title.$element )
6866 .addClass( 'oo-ui-processDialog-location' );
6867 this.$safeActions.addClass( 'oo-ui-processDialog-actions-safe' );
6868 this.$primaryActions.addClass( 'oo-ui-processDialog-actions-primary' );
6869 this.$otherActions.addClass( 'oo-ui-processDialog-actions-other' );
6870 this.$errorsTitle
6871 .addClass( 'oo-ui-processDialog-errors-title' )
6872 .text( OO.ui.msg( 'ooui-dialog-process-error' ) );
6873 this.$errors
6874 .addClass( 'oo-ui-processDialog-errors' )
6875 .append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element );
6876 this.$content
6877 .addClass( 'oo-ui-processDialog-content' )
6878 .append( this.$errors );
6879 this.$navigation
6880 .addClass( 'oo-ui-processDialog-navigation' )
6881 .append( this.$safeActions, this.$location, this.$primaryActions );
6882 this.$head.append( this.$navigation );
6883 this.$foot.append( this.$otherActions );
6884 };
6885
6886 /**
6887 * @inheritdoc
6888 */
6889 OO.ui.ProcessDialog.prototype.attachActions = function () {
6890 var i, len, other, special, others;
6891
6892 // Parent method
6893 OO.ui.ProcessDialog.super.prototype.attachActions.call( this );
6894
6895 special = this.actions.getSpecial();
6896 others = this.actions.getOthers();
6897 if ( special.primary ) {
6898 this.$primaryActions.append( special.primary.$element );
6899 special.primary.toggleFramed( true );
6900 }
6901 if ( others.length ) {
6902 for ( i = 0, len = others.length; i < len; i++ ) {
6903 other = others[ i ];
6904 this.$otherActions.append( other.$element );
6905 other.toggleFramed( true );
6906 }
6907 }
6908 if ( special.safe ) {
6909 this.$safeActions.append( special.safe.$element );
6910 special.safe.toggleFramed( true );
6911 }
6912
6913 this.fitLabel();
6914 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
6915 };
6916
6917 /**
6918 * @inheritdoc
6919 */
6920 OO.ui.ProcessDialog.prototype.executeAction = function ( action ) {
6921 OO.ui.ProcessDialog.super.prototype.executeAction.call( this, action )
6922 .fail( this.showErrors.bind( this ) );
6923 };
6924
6925 /**
6926 * Fit label between actions.
6927 *
6928 * @chainable
6929 */
6930 OO.ui.ProcessDialog.prototype.fitLabel = function () {
6931 var width = Math.max(
6932 this.$safeActions.is( ':visible' ) ? this.$safeActions.width() : 0,
6933 this.$primaryActions.is( ':visible' ) ? this.$primaryActions.width() : 0
6934 );
6935 this.$location.css( { paddingLeft: width, paddingRight: width } );
6936
6937 return this;
6938 };
6939
6940 /**
6941 * Handle errors that occurred during accept or reject processes.
6942 *
6943 * @param {OO.ui.Error[]} errors Errors to be handled
6944 */
6945 OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
6946 var i, len, $item,
6947 items = [],
6948 recoverable = true,
6949 warning = false;
6950
6951 for ( i = 0, len = errors.length; i < len; i++ ) {
6952 if ( !errors[ i ].isRecoverable() ) {
6953 recoverable = false;
6954 }
6955 if ( errors[ i ].isWarning() ) {
6956 warning = true;
6957 }
6958 $item = $( '<div>' )
6959 .addClass( 'oo-ui-processDialog-error' )
6960 .append( errors[ i ].getMessage() );
6961 items.push( $item[ 0 ] );
6962 }
6963 this.$errorItems = $( items );
6964 if ( recoverable ) {
6965 this.retryButton.clearFlags().setFlags( this.currentAction.getFlags() );
6966 } else {
6967 this.currentAction.setDisabled( true );
6968 }
6969 if ( warning ) {
6970 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-continue' ) );
6971 } else {
6972 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-retry' ) );
6973 }
6974 this.retryButton.toggle( recoverable );
6975 this.$errorsTitle.after( this.$errorItems );
6976 this.$errors.removeClass( 'oo-ui-widget-hidden' ).scrollTop( 0 );
6977 };
6978
6979 /**
6980 * Hide errors.
6981 */
6982 OO.ui.ProcessDialog.prototype.hideErrors = function () {
6983 this.$errors.addClass( 'oo-ui-widget-hidden' );
6984 this.$errorItems.remove();
6985 this.$errorItems = null;
6986 };
6987
6988 /**
6989 * Layout made of a field and optional label.
6990 *
6991 * Available label alignment modes include:
6992 * - left: Label is before the field and aligned away from it, best for when the user will be
6993 * scanning for a specific label in a form with many fields
6994 * - right: Label is before the field and aligned toward it, best for forms the user is very
6995 * familiar with and will tab through field checking quickly to verify which field they are in
6996 * - top: Label is before the field and above it, best for when the user will need to fill out all
6997 * fields from top to bottom in a form with few fields
6998 * - inline: Label is after the field and aligned toward it, best for small boolean fields like
6999 * checkboxes or radio buttons
7000 *
7001 * @class
7002 * @extends OO.ui.Layout
7003 * @mixins OO.ui.LabelElement
7004 *
7005 * @constructor
7006 * @param {OO.ui.Widget} fieldWidget Field widget
7007 * @param {Object} [config] Configuration options
7008 * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline'
7009 * @cfg {string} [help] Explanatory text shown as a '?' icon.
7010 */
7011 OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
7012 var hasInputWidget = fieldWidget instanceof OO.ui.InputWidget;
7013
7014 // Configuration initialization
7015 config = $.extend( { align: 'left' }, config );
7016
7017 // Properties (must be set before parent constructor, which calls #getTagName)
7018 this.fieldWidget = fieldWidget;
7019
7020 // Parent constructor
7021 OO.ui.FieldLayout.super.call( this, config );
7022
7023 // Mixin constructors
7024 OO.ui.LabelElement.call( this, config );
7025
7026 // Properties
7027 this.$field = $( '<div>' );
7028 this.$body = $( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
7029 this.align = null;
7030 if ( config.help ) {
7031 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
7032 classes: [ 'oo-ui-fieldLayout-help' ],
7033 framed: false,
7034 icon: 'info'
7035 } );
7036
7037 this.popupButtonWidget.getPopup().$body.append(
7038 $( '<div>' )
7039 .text( config.help )
7040 .addClass( 'oo-ui-fieldLayout-help-content' )
7041 );
7042 this.$help = this.popupButtonWidget.$element;
7043 } else {
7044 this.$help = $( [] );
7045 }
7046
7047 // Events
7048 if ( hasInputWidget ) {
7049 this.$label.on( 'click', this.onLabelClick.bind( this ) );
7050 }
7051 this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
7052
7053 // Initialization
7054 this.$element
7055 .addClass( 'oo-ui-fieldLayout' )
7056 .append( this.$help, this.$body );
7057 this.$body.addClass( 'oo-ui-fieldLayout-body' );
7058 this.$field
7059 .addClass( 'oo-ui-fieldLayout-field' )
7060 .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget.isDisabled() )
7061 .append( this.fieldWidget.$element );
7062
7063 this.setAlignment( config.align );
7064 };
7065
7066 /* Setup */
7067
7068 OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
7069 OO.mixinClass( OO.ui.FieldLayout, OO.ui.LabelElement );
7070
7071 /* Methods */
7072
7073 /**
7074 * Handle field disable events.
7075 *
7076 * @param {boolean} value Field is disabled
7077 */
7078 OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
7079 this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
7080 };
7081
7082 /**
7083 * Handle label mouse click events.
7084 *
7085 * @param {jQuery.Event} e Mouse click event
7086 */
7087 OO.ui.FieldLayout.prototype.onLabelClick = function () {
7088 this.fieldWidget.simulateLabelClick();
7089 return false;
7090 };
7091
7092 /**
7093 * Get the field.
7094 *
7095 * @return {OO.ui.Widget} Field widget
7096 */
7097 OO.ui.FieldLayout.prototype.getField = function () {
7098 return this.fieldWidget;
7099 };
7100
7101 /**
7102 * Set the field alignment mode.
7103 *
7104 * @private
7105 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
7106 * @chainable
7107 */
7108 OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
7109 if ( value !== this.align ) {
7110 // Default to 'left'
7111 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) {
7112 value = 'left';
7113 }
7114 // Reorder elements
7115 if ( value === 'inline' ) {
7116 this.$body.append( this.$field, this.$label );
7117 } else {
7118 this.$body.append( this.$label, this.$field );
7119 }
7120 // Set classes. The following classes can be used here:
7121 // * oo-ui-fieldLayout-align-left
7122 // * oo-ui-fieldLayout-align-right
7123 // * oo-ui-fieldLayout-align-top
7124 // * oo-ui-fieldLayout-align-inline
7125 if ( this.align ) {
7126 this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
7127 }
7128 this.$element.addClass( 'oo-ui-fieldLayout-align-' + value );
7129 this.align = value;
7130 }
7131
7132 return this;
7133 };
7134
7135 /**
7136 * Layout made of a field, a button, and an optional label.
7137 *
7138 * @class
7139 * @extends OO.ui.FieldLayout
7140 *
7141 * @constructor
7142 * @param {OO.ui.Widget} fieldWidget Field widget
7143 * @param {OO.ui.ButtonWidget} buttonWidget Button widget
7144 * @param {Object} [config] Configuration options
7145 * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline'
7146 * @cfg {string} [help] Explanatory text shown as a '?' icon.
7147 */
7148 OO.ui.ActionFieldLayout = function OoUiActionFieldLayout( fieldWidget, buttonWidget, config ) {
7149 // Configuration initialization
7150 config = $.extend( { align: 'left' }, config );
7151
7152 // Properties (must be set before parent constructor, which calls #getTagName)
7153 this.fieldWidget = fieldWidget;
7154 this.buttonWidget = buttonWidget;
7155
7156 // Parent constructor
7157 OO.ui.ActionFieldLayout.super.call( this, fieldWidget, config );
7158
7159 // Mixin constructors
7160 OO.ui.LabelElement.call( this, config );
7161
7162 // Properties
7163 this.$button = $( '<div>' )
7164 .addClass( 'oo-ui-actionFieldLayout-button' )
7165 .append( this.buttonWidget.$element );
7166
7167 this.$input = $( '<div>' )
7168 .addClass( 'oo-ui-actionFieldLayout-input' )
7169 .append( this.fieldWidget.$element );
7170
7171 this.$field
7172 .addClass( 'oo-ui-actionFieldLayout' )
7173 .append( this.$input, this.$button );
7174 };
7175
7176 /* Setup */
7177
7178 OO.inheritClass( OO.ui.ActionFieldLayout, OO.ui.FieldLayout );
7179
7180 /**
7181 * Layout made of a fieldset and optional legend.
7182 *
7183 * Just add OO.ui.FieldLayout items.
7184 *
7185 * @class
7186 * @extends OO.ui.Layout
7187 * @mixins OO.ui.IconElement
7188 * @mixins OO.ui.LabelElement
7189 * @mixins OO.ui.GroupElement
7190 *
7191 * @constructor
7192 * @param {Object} [config] Configuration options
7193 * @cfg {OO.ui.FieldLayout[]} [items] Items to add
7194 */
7195 OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
7196 // Configuration initialization
7197 config = config || {};
7198
7199 // Parent constructor
7200 OO.ui.FieldsetLayout.super.call( this, config );
7201
7202 // Mixin constructors
7203 OO.ui.IconElement.call( this, config );
7204 OO.ui.LabelElement.call( this, config );
7205 OO.ui.GroupElement.call( this, config );
7206
7207 if ( config.help ) {
7208 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
7209 classes: [ 'oo-ui-fieldsetLayout-help' ],
7210 framed: false,
7211 icon: 'info'
7212 } );
7213
7214 this.popupButtonWidget.getPopup().$body.append(
7215 $( '<div>' )
7216 .text( config.help )
7217 .addClass( 'oo-ui-fieldsetLayout-help-content' )
7218 );
7219 this.$help = this.popupButtonWidget.$element;
7220 } else {
7221 this.$help = $( [] );
7222 }
7223
7224 // Initialization
7225 this.$element
7226 .addClass( 'oo-ui-fieldsetLayout' )
7227 .prepend( this.$help, this.$icon, this.$label, this.$group );
7228 if ( $.isArray( config.items ) ) {
7229 this.addItems( config.items );
7230 }
7231 };
7232
7233 /* Setup */
7234
7235 OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
7236 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.IconElement );
7237 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.LabelElement );
7238 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.GroupElement );
7239
7240 /**
7241 * Layout with an HTML form.
7242 *
7243 * @class
7244 * @extends OO.ui.Layout
7245 *
7246 * @constructor
7247 * @param {Object} [config] Configuration options
7248 * @cfg {string} [method] HTML form `method` attribute
7249 * @cfg {string} [action] HTML form `action` attribute
7250 * @cfg {string} [enctype] HTML form `enctype` attribute
7251 */
7252 OO.ui.FormLayout = function OoUiFormLayout( config ) {
7253 // Configuration initialization
7254 config = config || {};
7255
7256 // Parent constructor
7257 OO.ui.FormLayout.super.call( this, config );
7258
7259 // Events
7260 this.$element.on( 'submit', this.onFormSubmit.bind( this ) );
7261
7262 // Initialization
7263 this.$element
7264 .addClass( 'oo-ui-formLayout' )
7265 .attr( {
7266 method: config.method,
7267 action: config.action,
7268 enctype: config.enctype
7269 } );
7270 };
7271
7272 /* Setup */
7273
7274 OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout );
7275
7276 /* Events */
7277
7278 /**
7279 * @event submit
7280 */
7281
7282 /* Static Properties */
7283
7284 OO.ui.FormLayout.static.tagName = 'form';
7285
7286 /* Methods */
7287
7288 /**
7289 * Handle form submit events.
7290 *
7291 * @param {jQuery.Event} e Submit event
7292 * @fires submit
7293 */
7294 OO.ui.FormLayout.prototype.onFormSubmit = function () {
7295 this.emit( 'submit' );
7296 return false;
7297 };
7298
7299 /**
7300 * Layout made of proportionally sized columns and rows.
7301 *
7302 * @class
7303 * @extends OO.ui.Layout
7304 * @deprecated Use OO.ui.MenuLayout or plain CSS instead.
7305 *
7306 * @constructor
7307 * @param {OO.ui.PanelLayout[]} panels Panels in the grid
7308 * @param {Object} [config] Configuration options
7309 * @cfg {number[]} [widths] Widths of columns as ratios
7310 * @cfg {number[]} [heights] Heights of rows as ratios
7311 */
7312 OO.ui.GridLayout = function OoUiGridLayout( panels, config ) {
7313 var i, len, widths;
7314
7315 // Configuration initialization
7316 config = config || {};
7317
7318 // Parent constructor
7319 OO.ui.GridLayout.super.call( this, config );
7320
7321 // Properties
7322 this.panels = [];
7323 this.widths = [];
7324 this.heights = [];
7325
7326 // Initialization
7327 this.$element.addClass( 'oo-ui-gridLayout' );
7328 for ( i = 0, len = panels.length; i < len; i++ ) {
7329 this.panels.push( panels[ i ] );
7330 this.$element.append( panels[ i ].$element );
7331 }
7332 if ( config.widths || config.heights ) {
7333 this.layout( config.widths || [ 1 ], config.heights || [ 1 ] );
7334 } else {
7335 // Arrange in columns by default
7336 widths = this.panels.map( function () { return 1; } );
7337 this.layout( widths, [ 1 ] );
7338 }
7339 };
7340
7341 /* Setup */
7342
7343 OO.inheritClass( OO.ui.GridLayout, OO.ui.Layout );
7344
7345 /* Events */
7346
7347 /**
7348 * @event layout
7349 */
7350
7351 /**
7352 * @event update
7353 */
7354
7355 /* Methods */
7356
7357 /**
7358 * Set grid dimensions.
7359 *
7360 * @param {number[]} widths Widths of columns as ratios
7361 * @param {number[]} heights Heights of rows as ratios
7362 * @fires layout
7363 * @throws {Error} If grid is not large enough to fit all panels
7364 */
7365 OO.ui.GridLayout.prototype.layout = function ( widths, heights ) {
7366 var x, y,
7367 xd = 0,
7368 yd = 0,
7369 cols = widths.length,
7370 rows = heights.length;
7371
7372 // Verify grid is big enough to fit panels
7373 if ( cols * rows < this.panels.length ) {
7374 throw new Error( 'Grid is not large enough to fit ' + this.panels.length + 'panels' );
7375 }
7376
7377 // Sum up denominators
7378 for ( x = 0; x < cols; x++ ) {
7379 xd += widths[ x ];
7380 }
7381 for ( y = 0; y < rows; y++ ) {
7382 yd += heights[ y ];
7383 }
7384 // Store factors
7385 this.widths = [];
7386 this.heights = [];
7387 for ( x = 0; x < cols; x++ ) {
7388 this.widths[ x ] = widths[ x ] / xd;
7389 }
7390 for ( y = 0; y < rows; y++ ) {
7391 this.heights[ y ] = heights[ y ] / yd;
7392 }
7393 // Synchronize view
7394 this.update();
7395 this.emit( 'layout' );
7396 };
7397
7398 /**
7399 * Update panel positions and sizes.
7400 *
7401 * @fires update
7402 */
7403 OO.ui.GridLayout.prototype.update = function () {
7404 var x, y, panel, width, height, dimensions,
7405 i = 0,
7406 top = 0,
7407 left = 0,
7408 cols = this.widths.length,
7409 rows = this.heights.length;
7410
7411 for ( y = 0; y < rows; y++ ) {
7412 height = this.heights[ y ];
7413 for ( x = 0; x < cols; x++ ) {
7414 width = this.widths[ x ];
7415 panel = this.panels[ i ];
7416 dimensions = {
7417 width: ( width * 100 ) + '%',
7418 height: ( height * 100 ) + '%',
7419 top: ( top * 100 ) + '%'
7420 };
7421 // If RTL, reverse:
7422 if ( OO.ui.Element.static.getDir( document ) === 'rtl' ) {
7423 dimensions.right = ( left * 100 ) + '%';
7424 } else {
7425 dimensions.left = ( left * 100 ) + '%';
7426 }
7427 // HACK: Work around IE bug by setting visibility: hidden; if width or height is zero
7428 if ( width === 0 || height === 0 ) {
7429 dimensions.visibility = 'hidden';
7430 } else {
7431 dimensions.visibility = '';
7432 }
7433 panel.$element.css( dimensions );
7434 i++;
7435 left += width;
7436 }
7437 top += height;
7438 left = 0;
7439 }
7440
7441 this.emit( 'update' );
7442 };
7443
7444 /**
7445 * Get a panel at a given position.
7446 *
7447 * The x and y position is affected by the current grid layout.
7448 *
7449 * @param {number} x Horizontal position
7450 * @param {number} y Vertical position
7451 * @return {OO.ui.PanelLayout} The panel at the given position
7452 */
7453 OO.ui.GridLayout.prototype.getPanel = function ( x, y ) {
7454 return this.panels[ ( x * this.widths.length ) + y ];
7455 };
7456
7457 /**
7458 * Layout with a content and menu area.
7459 *
7460 * The menu area can be positioned at the top, after, bottom or before. The content area will fill
7461 * all remaining space.
7462 *
7463 * @class
7464 * @extends OO.ui.Layout
7465 *
7466 * @constructor
7467 * @param {Object} [config] Configuration options
7468 * @cfg {number|string} [menuSize='18em'] Size of menu in pixels or any CSS unit
7469 * @cfg {boolean} [showMenu=true] Show menu
7470 * @cfg {string} [position='before'] Position of menu, either `top`, `after`, `bottom` or `before`
7471 * @cfg {boolean} [collapse] Collapse the menu out of view
7472 */
7473 OO.ui.MenuLayout = function OoUiMenuLayout( config ) {
7474 var positions = this.constructor.static.menuPositions;
7475
7476 // Configuration initialization
7477 config = config || {};
7478
7479 // Parent constructor
7480 OO.ui.MenuLayout.super.call( this, config );
7481
7482 // Properties
7483 this.showMenu = config.showMenu !== false;
7484 this.menuSize = config.menuSize || '18em';
7485 this.menuPosition = positions[ config.menuPosition ] || positions.before;
7486
7487 /**
7488 * Menu DOM node
7489 *
7490 * @property {jQuery}
7491 */
7492 this.$menu = $( '<div>' );
7493 /**
7494 * Content DOM node
7495 *
7496 * @property {jQuery}
7497 */
7498 this.$content = $( '<div>' );
7499
7500 // Initialization
7501 this.toggleMenu( this.showMenu );
7502 this.updateSizes();
7503 this.$menu
7504 .addClass( 'oo-ui-menuLayout-menu' )
7505 .css( this.menuPosition.sizeProperty, this.menuSize );
7506 this.$content.addClass( 'oo-ui-menuLayout-content' );
7507 this.$element
7508 .addClass( 'oo-ui-menuLayout ' + this.menuPosition.className )
7509 .append( this.$content, this.$menu );
7510 };
7511
7512 /* Setup */
7513
7514 OO.inheritClass( OO.ui.MenuLayout, OO.ui.Layout );
7515
7516 /* Static Properties */
7517
7518 OO.ui.MenuLayout.static.menuPositions = {
7519 top: {
7520 sizeProperty: 'height',
7521 className: 'oo-ui-menuLayout-top'
7522 },
7523 after: {
7524 sizeProperty: 'width',
7525 className: 'oo-ui-menuLayout-after'
7526 },
7527 bottom: {
7528 sizeProperty: 'height',
7529 className: 'oo-ui-menuLayout-bottom'
7530 },
7531 before: {
7532 sizeProperty: 'width',
7533 className: 'oo-ui-menuLayout-before'
7534 }
7535 };
7536
7537 /* Methods */
7538
7539 /**
7540 * Toggle menu.
7541 *
7542 * @param {boolean} showMenu Show menu, omit to toggle
7543 * @chainable
7544 */
7545 OO.ui.MenuLayout.prototype.toggleMenu = function ( showMenu ) {
7546 showMenu = showMenu === undefined ? !this.showMenu : !!showMenu;
7547
7548 if ( this.showMenu !== showMenu ) {
7549 this.showMenu = showMenu;
7550 this.updateSizes();
7551 }
7552
7553 return this;
7554 };
7555
7556 /**
7557 * Check if menu is visible
7558 *
7559 * @return {boolean} Menu is visible
7560 */
7561 OO.ui.MenuLayout.prototype.isMenuVisible = function () {
7562 return this.showMenu;
7563 };
7564
7565 /**
7566 * Set menu size.
7567 *
7568 * @param {number|string} size Size of menu in pixels or any CSS unit
7569 * @chainable
7570 */
7571 OO.ui.MenuLayout.prototype.setMenuSize = function ( size ) {
7572 this.menuSize = size;
7573 this.updateSizes();
7574
7575 return this;
7576 };
7577
7578 /**
7579 * Update menu and content CSS based on current menu size and visibility
7580 *
7581 * This method is called internally when size or position is changed.
7582 */
7583 OO.ui.MenuLayout.prototype.updateSizes = function () {
7584 if ( this.showMenu ) {
7585 this.$menu
7586 .css( this.menuPosition.sizeProperty, this.menuSize )
7587 .css( 'overflow', '' );
7588 // Set offsets on all sides. CSS resets all but one with
7589 // 'important' rules so directionality flips are supported
7590 this.$content.css( {
7591 top: this.menuSize,
7592 right: this.menuSize,
7593 bottom: this.menuSize,
7594 left: this.menuSize
7595 } );
7596 } else {
7597 this.$menu
7598 .css( this.menuPosition.sizeProperty, 0 )
7599 .css( 'overflow', 'hidden' );
7600 this.$content.css( {
7601 top: 0,
7602 right: 0,
7603 bottom: 0,
7604 left: 0
7605 } );
7606 }
7607 };
7608
7609 /**
7610 * Get menu size.
7611 *
7612 * @return {number|string} Menu size
7613 */
7614 OO.ui.MenuLayout.prototype.getMenuSize = function () {
7615 return this.menuSize;
7616 };
7617
7618 /**
7619 * Set menu position.
7620 *
7621 * @param {string} position Position of menu, either `top`, `after`, `bottom` or `before`
7622 * @throws {Error} If position value is not supported
7623 * @chainable
7624 */
7625 OO.ui.MenuLayout.prototype.setMenuPosition = function ( position ) {
7626 var positions = this.constructor.static.menuPositions;
7627
7628 if ( !positions[ position ] ) {
7629 throw new Error( 'Cannot set position; unsupported position value: ' + position );
7630 }
7631
7632 this.$menu.css( this.menuPosition.sizeProperty, '' );
7633 this.$element.removeClass( this.menuPosition.className );
7634
7635 this.menuPosition = positions[ position ];
7636
7637 this.updateSizes();
7638 this.$element.addClass( this.menuPosition.className );
7639
7640 return this;
7641 };
7642
7643 /**
7644 * Get menu position.
7645 *
7646 * @return {string} Menu position
7647 */
7648 OO.ui.MenuLayout.prototype.getMenuPosition = function () {
7649 return this.menuPosition;
7650 };
7651
7652 /**
7653 * Layout containing a series of pages.
7654 *
7655 * @class
7656 * @extends OO.ui.MenuLayout
7657 *
7658 * @constructor
7659 * @param {Object} [config] Configuration options
7660 * @cfg {boolean} [continuous=false] Show all pages, one after another
7661 * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when changing to a page
7662 * @cfg {boolean} [outlined=false] Show an outline
7663 * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
7664 */
7665 OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
7666 // Configuration initialization
7667 config = config || {};
7668
7669 // Parent constructor
7670 OO.ui.BookletLayout.super.call( this, config );
7671
7672 // Properties
7673 this.currentPageName = null;
7674 this.pages = {};
7675 this.ignoreFocus = false;
7676 this.stackLayout = new OO.ui.StackLayout( { continuous: !!config.continuous } );
7677 this.$content.append( this.stackLayout.$element );
7678 this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
7679 this.outlineVisible = false;
7680 this.outlined = !!config.outlined;
7681 if ( this.outlined ) {
7682 this.editable = !!config.editable;
7683 this.outlineControlsWidget = null;
7684 this.outlineSelectWidget = new OO.ui.OutlineSelectWidget();
7685 this.outlinePanel = new OO.ui.PanelLayout( { scrollable: true } );
7686 this.$menu.append( this.outlinePanel.$element );
7687 this.outlineVisible = true;
7688 if ( this.editable ) {
7689 this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
7690 this.outlineSelectWidget
7691 );
7692 }
7693 }
7694 this.toggleMenu( this.outlined );
7695
7696 // Events
7697 this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
7698 if ( this.outlined ) {
7699 this.outlineSelectWidget.connect( this, { select: 'onOutlineSelectWidgetSelect' } );
7700 }
7701 if ( this.autoFocus ) {
7702 // Event 'focus' does not bubble, but 'focusin' does
7703 this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
7704 }
7705
7706 // Initialization
7707 this.$element.addClass( 'oo-ui-bookletLayout' );
7708 this.stackLayout.$element.addClass( 'oo-ui-bookletLayout-stackLayout' );
7709 if ( this.outlined ) {
7710 this.outlinePanel.$element
7711 .addClass( 'oo-ui-bookletLayout-outlinePanel' )
7712 .append( this.outlineSelectWidget.$element );
7713 if ( this.editable ) {
7714 this.outlinePanel.$element
7715 .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
7716 .append( this.outlineControlsWidget.$element );
7717 }
7718 }
7719 };
7720
7721 /* Setup */
7722
7723 OO.inheritClass( OO.ui.BookletLayout, OO.ui.MenuLayout );
7724
7725 /* Events */
7726
7727 /**
7728 * @event set
7729 * @param {OO.ui.PageLayout} page Current page
7730 */
7731
7732 /**
7733 * @event add
7734 * @param {OO.ui.PageLayout[]} page Added pages
7735 * @param {number} index Index pages were added at
7736 */
7737
7738 /**
7739 * @event remove
7740 * @param {OO.ui.PageLayout[]} pages Removed pages
7741 */
7742
7743 /* Methods */
7744
7745 /**
7746 * Handle stack layout focus.
7747 *
7748 * @param {jQuery.Event} e Focusin event
7749 */
7750 OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) {
7751 var name, $target;
7752
7753 // Find the page that an element was focused within
7754 $target = $( e.target ).closest( '.oo-ui-pageLayout' );
7755 for ( name in this.pages ) {
7756 // Check for page match, exclude current page to find only page changes
7757 if ( this.pages[ name ].$element[ 0 ] === $target[ 0 ] && name !== this.currentPageName ) {
7758 this.setPage( name );
7759 break;
7760 }
7761 }
7762 };
7763
7764 /**
7765 * Handle stack layout set events.
7766 *
7767 * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
7768 */
7769 OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
7770 var layout = this;
7771 if ( page ) {
7772 page.scrollElementIntoView( { complete: function () {
7773 if ( layout.autoFocus ) {
7774 layout.focus();
7775 }
7776 } } );
7777 }
7778 };
7779
7780 /**
7781 * Focus the first input in the current page.
7782 *
7783 * If no page is selected, the first selectable page will be selected.
7784 * If the focus is already in an element on the current page, nothing will happen.
7785 */
7786 OO.ui.BookletLayout.prototype.focus = function () {
7787 var $input, page = this.stackLayout.getCurrentItem();
7788 if ( !page && this.outlined ) {
7789 this.selectFirstSelectablePage();
7790 page = this.stackLayout.getCurrentItem();
7791 }
7792 if ( !page ) {
7793 return;
7794 }
7795 // Only change the focus if is not already in the current page
7796 if ( !page.$element.find( ':focus' ).length ) {
7797 $input = page.$element.find( ':input:first' );
7798 if ( $input.length ) {
7799 $input[ 0 ].focus();
7800 }
7801 }
7802 };
7803
7804 /**
7805 * Handle outline widget select events.
7806 *
7807 * @param {OO.ui.OptionWidget|null} item Selected item
7808 */
7809 OO.ui.BookletLayout.prototype.onOutlineSelectWidgetSelect = function ( item ) {
7810 if ( item ) {
7811 this.setPage( item.getData() );
7812 }
7813 };
7814
7815 /**
7816 * Check if booklet has an outline.
7817 *
7818 * @return {boolean}
7819 */
7820 OO.ui.BookletLayout.prototype.isOutlined = function () {
7821 return this.outlined;
7822 };
7823
7824 /**
7825 * Check if booklet has editing controls.
7826 *
7827 * @return {boolean}
7828 */
7829 OO.ui.BookletLayout.prototype.isEditable = function () {
7830 return this.editable;
7831 };
7832
7833 /**
7834 * Check if booklet has a visible outline.
7835 *
7836 * @return {boolean}
7837 */
7838 OO.ui.BookletLayout.prototype.isOutlineVisible = function () {
7839 return this.outlined && this.outlineVisible;
7840 };
7841
7842 /**
7843 * Hide or show the outline.
7844 *
7845 * @param {boolean} [show] Show outline, omit to invert current state
7846 * @chainable
7847 */
7848 OO.ui.BookletLayout.prototype.toggleOutline = function ( show ) {
7849 if ( this.outlined ) {
7850 show = show === undefined ? !this.outlineVisible : !!show;
7851 this.outlineVisible = show;
7852 this.toggleMenu( show );
7853 }
7854
7855 return this;
7856 };
7857
7858 /**
7859 * Get the outline widget.
7860 *
7861 * @param {OO.ui.PageLayout} page Page to be selected
7862 * @return {OO.ui.PageLayout|null} Closest page to another
7863 */
7864 OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) {
7865 var next, prev, level,
7866 pages = this.stackLayout.getItems(),
7867 index = $.inArray( page, pages );
7868
7869 if ( index !== -1 ) {
7870 next = pages[ index + 1 ];
7871 prev = pages[ index - 1 ];
7872 // Prefer adjacent pages at the same level
7873 if ( this.outlined ) {
7874 level = this.outlineSelectWidget.getItemFromData( page.getName() ).getLevel();
7875 if (
7876 prev &&
7877 level === this.outlineSelectWidget.getItemFromData( prev.getName() ).getLevel()
7878 ) {
7879 return prev;
7880 }
7881 if (
7882 next &&
7883 level === this.outlineSelectWidget.getItemFromData( next.getName() ).getLevel()
7884 ) {
7885 return next;
7886 }
7887 }
7888 }
7889 return prev || next || null;
7890 };
7891
7892 /**
7893 * Get the outline widget.
7894 *
7895 * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if booklet has no outline
7896 */
7897 OO.ui.BookletLayout.prototype.getOutline = function () {
7898 return this.outlineSelectWidget;
7899 };
7900
7901 /**
7902 * Get the outline controls widget. If the outline is not editable, null is returned.
7903 *
7904 * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
7905 */
7906 OO.ui.BookletLayout.prototype.getOutlineControls = function () {
7907 return this.outlineControlsWidget;
7908 };
7909
7910 /**
7911 * Get a page by name.
7912 *
7913 * @param {string} name Symbolic name of page
7914 * @return {OO.ui.PageLayout|undefined} Page, if found
7915 */
7916 OO.ui.BookletLayout.prototype.getPage = function ( name ) {
7917 return this.pages[ name ];
7918 };
7919
7920 /**
7921 * Get the current page
7922 *
7923 * @return {OO.ui.PageLayout|undefined} Current page, if found
7924 */
7925 OO.ui.BookletLayout.prototype.getCurrentPage = function () {
7926 var name = this.getCurrentPageName();
7927 return name ? this.getPage( name ) : undefined;
7928 };
7929
7930 /**
7931 * Get the current page name.
7932 *
7933 * @return {string|null} Current page name
7934 */
7935 OO.ui.BookletLayout.prototype.getCurrentPageName = function () {
7936 return this.currentPageName;
7937 };
7938
7939 /**
7940 * Add a page to the layout.
7941 *
7942 * When pages are added with the same names as existing pages, the existing pages will be
7943 * automatically removed before the new pages are added.
7944 *
7945 * @param {OO.ui.PageLayout[]} pages Pages to add
7946 * @param {number} index Index to insert pages after
7947 * @fires add
7948 * @chainable
7949 */
7950 OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) {
7951 var i, len, name, page, item, currentIndex,
7952 stackLayoutPages = this.stackLayout.getItems(),
7953 remove = [],
7954 items = [];
7955
7956 // Remove pages with same names
7957 for ( i = 0, len = pages.length; i < len; i++ ) {
7958 page = pages[ i ];
7959 name = page.getName();
7960
7961 if ( Object.prototype.hasOwnProperty.call( this.pages, name ) ) {
7962 // Correct the insertion index
7963 currentIndex = $.inArray( this.pages[ name ], stackLayoutPages );
7964 if ( currentIndex !== -1 && currentIndex + 1 < index ) {
7965 index--;
7966 }
7967 remove.push( this.pages[ name ] );
7968 }
7969 }
7970 if ( remove.length ) {
7971 this.removePages( remove );
7972 }
7973
7974 // Add new pages
7975 for ( i = 0, len = pages.length; i < len; i++ ) {
7976 page = pages[ i ];
7977 name = page.getName();
7978 this.pages[ page.getName() ] = page;
7979 if ( this.outlined ) {
7980 item = new OO.ui.OutlineOptionWidget( { data: name } );
7981 page.setOutlineItem( item );
7982 items.push( item );
7983 }
7984 }
7985
7986 if ( this.outlined && items.length ) {
7987 this.outlineSelectWidget.addItems( items, index );
7988 this.selectFirstSelectablePage();
7989 }
7990 this.stackLayout.addItems( pages, index );
7991 this.emit( 'add', pages, index );
7992
7993 return this;
7994 };
7995
7996 /**
7997 * Remove a page from the layout.
7998 *
7999 * @fires remove
8000 * @chainable
8001 */
8002 OO.ui.BookletLayout.prototype.removePages = function ( pages ) {
8003 var i, len, name, page,
8004 items = [];
8005
8006 for ( i = 0, len = pages.length; i < len; i++ ) {
8007 page = pages[ i ];
8008 name = page.getName();
8009 delete this.pages[ name ];
8010 if ( this.outlined ) {
8011 items.push( this.outlineSelectWidget.getItemFromData( name ) );
8012 page.setOutlineItem( null );
8013 }
8014 }
8015 if ( this.outlined && items.length ) {
8016 this.outlineSelectWidget.removeItems( items );
8017 this.selectFirstSelectablePage();
8018 }
8019 this.stackLayout.removeItems( pages );
8020 this.emit( 'remove', pages );
8021
8022 return this;
8023 };
8024
8025 /**
8026 * Clear all pages from the layout.
8027 *
8028 * @fires remove
8029 * @chainable
8030 */
8031 OO.ui.BookletLayout.prototype.clearPages = function () {
8032 var i, len,
8033 pages = this.stackLayout.getItems();
8034
8035 this.pages = {};
8036 this.currentPageName = null;
8037 if ( this.outlined ) {
8038 this.outlineSelectWidget.clearItems();
8039 for ( i = 0, len = pages.length; i < len; i++ ) {
8040 pages[ i ].setOutlineItem( null );
8041 }
8042 }
8043 this.stackLayout.clearItems();
8044
8045 this.emit( 'remove', pages );
8046
8047 return this;
8048 };
8049
8050 /**
8051 * Set the current page by name.
8052 *
8053 * @fires set
8054 * @param {string} name Symbolic name of page
8055 */
8056 OO.ui.BookletLayout.prototype.setPage = function ( name ) {
8057 var selectedItem,
8058 $focused,
8059 page = this.pages[ name ];
8060
8061 if ( name !== this.currentPageName ) {
8062 if ( this.outlined ) {
8063 selectedItem = this.outlineSelectWidget.getSelectedItem();
8064 if ( selectedItem && selectedItem.getData() !== name ) {
8065 this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getItemFromData( name ) );
8066 }
8067 }
8068 if ( page ) {
8069 if ( this.currentPageName && this.pages[ this.currentPageName ] ) {
8070 this.pages[ this.currentPageName ].setActive( false );
8071 // Blur anything focused if the next page doesn't have anything focusable - this
8072 // is not needed if the next page has something focusable because once it is focused
8073 // this blur happens automatically
8074 if ( this.autoFocus && !page.$element.find( ':input' ).length ) {
8075 $focused = this.pages[ this.currentPageName ].$element.find( ':focus' );
8076 if ( $focused.length ) {
8077 $focused[ 0 ].blur();
8078 }
8079 }
8080 }
8081 this.currentPageName = name;
8082 this.stackLayout.setItem( page );
8083 page.setActive( true );
8084 this.emit( 'set', page );
8085 }
8086 }
8087 };
8088
8089 /**
8090 * Select the first selectable page.
8091 *
8092 * @chainable
8093 */
8094 OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () {
8095 if ( !this.outlineSelectWidget.getSelectedItem() ) {
8096 this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getFirstSelectableItem() );
8097 }
8098
8099 return this;
8100 };
8101
8102 /**
8103 * Layout that expands to cover the entire area of its parent, with optional scrolling and padding.
8104 *
8105 * @class
8106 * @extends OO.ui.Layout
8107 *
8108 * @constructor
8109 * @param {Object} [config] Configuration options
8110 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
8111 * @cfg {boolean} [padded=false] Pad the content from the edges
8112 * @cfg {boolean} [expanded=true] Expand size to fill the entire parent element
8113 */
8114 OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
8115 // Configuration initialization
8116 config = $.extend( {
8117 scrollable: false,
8118 padded: false,
8119 expanded: true
8120 }, config );
8121
8122 // Parent constructor
8123 OO.ui.PanelLayout.super.call( this, config );
8124
8125 // Initialization
8126 this.$element.addClass( 'oo-ui-panelLayout' );
8127 if ( config.scrollable ) {
8128 this.$element.addClass( 'oo-ui-panelLayout-scrollable' );
8129 }
8130 if ( config.padded ) {
8131 this.$element.addClass( 'oo-ui-panelLayout-padded' );
8132 }
8133 if ( config.expanded ) {
8134 this.$element.addClass( 'oo-ui-panelLayout-expanded' );
8135 }
8136 };
8137
8138 /* Setup */
8139
8140 OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
8141
8142 /**
8143 * Page within an booklet layout.
8144 *
8145 * @class
8146 * @extends OO.ui.PanelLayout
8147 *
8148 * @constructor
8149 * @param {string} name Unique symbolic name of page
8150 * @param {Object} [config] Configuration options
8151 */
8152 OO.ui.PageLayout = function OoUiPageLayout( name, config ) {
8153 // Configuration initialization
8154 config = $.extend( { scrollable: true }, config );
8155
8156 // Parent constructor
8157 OO.ui.PageLayout.super.call( this, config );
8158
8159 // Properties
8160 this.name = name;
8161 this.outlineItem = null;
8162 this.active = false;
8163
8164 // Initialization
8165 this.$element.addClass( 'oo-ui-pageLayout' );
8166 };
8167
8168 /* Setup */
8169
8170 OO.inheritClass( OO.ui.PageLayout, OO.ui.PanelLayout );
8171
8172 /* Events */
8173
8174 /**
8175 * @event active
8176 * @param {boolean} active Page is active
8177 */
8178
8179 /* Methods */
8180
8181 /**
8182 * Get page name.
8183 *
8184 * @return {string} Symbolic name of page
8185 */
8186 OO.ui.PageLayout.prototype.getName = function () {
8187 return this.name;
8188 };
8189
8190 /**
8191 * Check if page is active.
8192 *
8193 * @return {boolean} Page is active
8194 */
8195 OO.ui.PageLayout.prototype.isActive = function () {
8196 return this.active;
8197 };
8198
8199 /**
8200 * Get outline item.
8201 *
8202 * @return {OO.ui.OutlineOptionWidget|null} Outline item widget
8203 */
8204 OO.ui.PageLayout.prototype.getOutlineItem = function () {
8205 return this.outlineItem;
8206 };
8207
8208 /**
8209 * Set outline item.
8210 *
8211 * @localdoc Subclasses should override #setupOutlineItem instead of this method to adjust the
8212 * outline item as desired; this method is called for setting (with an object) and unsetting
8213 * (with null) and overriding methods would have to check the value of `outlineItem` to avoid
8214 * operating on null instead of an OO.ui.OutlineOptionWidget object.
8215 *
8216 * @param {OO.ui.OutlineOptionWidget|null} outlineItem Outline item widget, null to clear
8217 * @chainable
8218 */
8219 OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) {
8220 this.outlineItem = outlineItem || null;
8221 if ( outlineItem ) {
8222 this.setupOutlineItem();
8223 }
8224 return this;
8225 };
8226
8227 /**
8228 * Setup outline item.
8229 *
8230 * @localdoc Subclasses should override this method to adjust the outline item as desired.
8231 *
8232 * @param {OO.ui.OutlineOptionWidget} outlineItem Outline item widget to setup
8233 * @chainable
8234 */
8235 OO.ui.PageLayout.prototype.setupOutlineItem = function () {
8236 return this;
8237 };
8238
8239 /**
8240 * Set page active state.
8241 *
8242 * @param {boolean} Page is active
8243 * @fires active
8244 */
8245 OO.ui.PageLayout.prototype.setActive = function ( active ) {
8246 active = !!active;
8247
8248 if ( active !== this.active ) {
8249 this.active = active;
8250 this.$element.toggleClass( 'oo-ui-pageLayout-active', active );
8251 this.emit( 'active', this.active );
8252 }
8253 };
8254
8255 /**
8256 * Layout containing a series of mutually exclusive pages.
8257 *
8258 * @class
8259 * @extends OO.ui.PanelLayout
8260 * @mixins OO.ui.GroupElement
8261 *
8262 * @constructor
8263 * @param {Object} [config] Configuration options
8264 * @cfg {boolean} [continuous=false] Show all pages, one after another
8265 * @cfg {OO.ui.Layout[]} [items] Layouts to add
8266 */
8267 OO.ui.StackLayout = function OoUiStackLayout( config ) {
8268 // Configuration initialization
8269 config = $.extend( { scrollable: true }, config );
8270
8271 // Parent constructor
8272 OO.ui.StackLayout.super.call( this, config );
8273
8274 // Mixin constructors
8275 OO.ui.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
8276
8277 // Properties
8278 this.currentItem = null;
8279 this.continuous = !!config.continuous;
8280
8281 // Initialization
8282 this.$element.addClass( 'oo-ui-stackLayout' );
8283 if ( this.continuous ) {
8284 this.$element.addClass( 'oo-ui-stackLayout-continuous' );
8285 }
8286 if ( $.isArray( config.items ) ) {
8287 this.addItems( config.items );
8288 }
8289 };
8290
8291 /* Setup */
8292
8293 OO.inheritClass( OO.ui.StackLayout, OO.ui.PanelLayout );
8294 OO.mixinClass( OO.ui.StackLayout, OO.ui.GroupElement );
8295
8296 /* Events */
8297
8298 /**
8299 * @event set
8300 * @param {OO.ui.Layout|null} item Current item or null if there is no longer a layout shown
8301 */
8302
8303 /* Methods */
8304
8305 /**
8306 * Get the current item.
8307 *
8308 * @return {OO.ui.Layout|null}
8309 */
8310 OO.ui.StackLayout.prototype.getCurrentItem = function () {
8311 return this.currentItem;
8312 };
8313
8314 /**
8315 * Unset the current item.
8316 *
8317 * @private
8318 * @param {OO.ui.StackLayout} layout
8319 * @fires set
8320 */
8321 OO.ui.StackLayout.prototype.unsetCurrentItem = function () {
8322 var prevItem = this.currentItem;
8323 if ( prevItem === null ) {
8324 return;
8325 }
8326
8327 this.currentItem = null;
8328 this.emit( 'set', null );
8329 };
8330
8331 /**
8332 * Add items.
8333 *
8334 * Adding an existing item (by value) will move it.
8335 *
8336 * @param {OO.ui.Layout[]} items Items to add
8337 * @param {number} [index] Index to insert items after
8338 * @chainable
8339 */
8340 OO.ui.StackLayout.prototype.addItems = function ( items, index ) {
8341 // Update the visibility
8342 this.updateHiddenState( items, this.currentItem );
8343
8344 // Mixin method
8345 OO.ui.GroupElement.prototype.addItems.call( this, items, index );
8346
8347 if ( !this.currentItem && items.length ) {
8348 this.setItem( items[ 0 ] );
8349 }
8350
8351 return this;
8352 };
8353
8354 /**
8355 * Remove items.
8356 *
8357 * Items will be detached, not removed, so they can be used later.
8358 *
8359 * @param {OO.ui.Layout[]} items Items to remove
8360 * @chainable
8361 * @fires set
8362 */
8363 OO.ui.StackLayout.prototype.removeItems = function ( items ) {
8364 // Mixin method
8365 OO.ui.GroupElement.prototype.removeItems.call( this, items );
8366
8367 if ( $.inArray( this.currentItem, items ) !== -1 ) {
8368 if ( this.items.length ) {
8369 this.setItem( this.items[ 0 ] );
8370 } else {
8371 this.unsetCurrentItem();
8372 }
8373 }
8374
8375 return this;
8376 };
8377
8378 /**
8379 * Clear all items.
8380 *
8381 * Items will be detached, not removed, so they can be used later.
8382 *
8383 * @chainable
8384 * @fires set
8385 */
8386 OO.ui.StackLayout.prototype.clearItems = function () {
8387 this.unsetCurrentItem();
8388 OO.ui.GroupElement.prototype.clearItems.call( this );
8389
8390 return this;
8391 };
8392
8393 /**
8394 * Show item.
8395 *
8396 * Any currently shown item will be hidden.
8397 *
8398 * FIXME: If the passed item to show has not been added in the items list, then
8399 * this method drops it and unsets the current item.
8400 *
8401 * @param {OO.ui.Layout} item Item to show
8402 * @chainable
8403 * @fires set
8404 */
8405 OO.ui.StackLayout.prototype.setItem = function ( item ) {
8406 if ( item !== this.currentItem ) {
8407 this.updateHiddenState( this.items, item );
8408
8409 if ( $.inArray( item, this.items ) !== -1 ) {
8410 this.currentItem = item;
8411 this.emit( 'set', item );
8412 } else {
8413 this.unsetCurrentItem();
8414 }
8415 }
8416
8417 return this;
8418 };
8419
8420 /**
8421 * Update the visibility of all items in case of non-continuous view.
8422 *
8423 * Ensure all items are hidden except for the selected one.
8424 * This method does nothing when the stack is continuous.
8425 *
8426 * @param {OO.ui.Layout[]} items Item list iterate over
8427 * @param {OO.ui.Layout} [selectedItem] Selected item to show
8428 */
8429 OO.ui.StackLayout.prototype.updateHiddenState = function ( items, selectedItem ) {
8430 var i, len;
8431
8432 if ( !this.continuous ) {
8433 for ( i = 0, len = items.length; i < len; i++ ) {
8434 if ( !selectedItem || selectedItem !== items[ i ] ) {
8435 items[ i ].$element.addClass( 'oo-ui-element-hidden' );
8436 }
8437 }
8438 if ( selectedItem ) {
8439 selectedItem.$element.removeClass( 'oo-ui-element-hidden' );
8440 }
8441 }
8442 };
8443
8444 /**
8445 * Horizontal bar layout of tools as icon buttons.
8446 *
8447 * @class
8448 * @extends OO.ui.ToolGroup
8449 *
8450 * @constructor
8451 * @param {OO.ui.Toolbar} toolbar
8452 * @param {Object} [config] Configuration options
8453 */
8454 OO.ui.BarToolGroup = function OoUiBarToolGroup( toolbar, config ) {
8455 // Parent constructor
8456 OO.ui.BarToolGroup.super.call( this, toolbar, config );
8457
8458 // Initialization
8459 this.$element.addClass( 'oo-ui-barToolGroup' );
8460 };
8461
8462 /* Setup */
8463
8464 OO.inheritClass( OO.ui.BarToolGroup, OO.ui.ToolGroup );
8465
8466 /* Static Properties */
8467
8468 OO.ui.BarToolGroup.static.titleTooltips = true;
8469
8470 OO.ui.BarToolGroup.static.accelTooltips = true;
8471
8472 OO.ui.BarToolGroup.static.name = 'bar';
8473
8474 /**
8475 * Popup list of tools with an icon and optional label.
8476 *
8477 * @abstract
8478 * @class
8479 * @extends OO.ui.ToolGroup
8480 * @mixins OO.ui.IconElement
8481 * @mixins OO.ui.IndicatorElement
8482 * @mixins OO.ui.LabelElement
8483 * @mixins OO.ui.TitledElement
8484 * @mixins OO.ui.ClippableElement
8485 *
8486 * @constructor
8487 * @param {OO.ui.Toolbar} toolbar
8488 * @param {Object} [config] Configuration options
8489 * @cfg {string} [header] Text to display at the top of the pop-up
8490 */
8491 OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
8492 // Configuration initialization
8493 config = config || {};
8494
8495 // Parent constructor
8496 OO.ui.PopupToolGroup.super.call( this, toolbar, config );
8497
8498 // Mixin constructors
8499 OO.ui.IconElement.call( this, config );
8500 OO.ui.IndicatorElement.call( this, config );
8501 OO.ui.LabelElement.call( this, config );
8502 OO.ui.TitledElement.call( this, config );
8503 OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
8504
8505 // Properties
8506 this.active = false;
8507 this.dragging = false;
8508 this.onBlurHandler = this.onBlur.bind( this );
8509 this.$handle = $( '<span>' );
8510
8511 // Events
8512 this.$handle.on( {
8513 'mousedown touchstart': this.onHandlePointerDown.bind( this ),
8514 'mouseup touchend': this.onHandlePointerUp.bind( this )
8515 } );
8516
8517 // Initialization
8518 this.$handle
8519 .addClass( 'oo-ui-popupToolGroup-handle' )
8520 .append( this.$icon, this.$label, this.$indicator );
8521 // If the pop-up should have a header, add it to the top of the toolGroup.
8522 // Note: If this feature is useful for other widgets, we could abstract it into an
8523 // OO.ui.HeaderedElement mixin constructor.
8524 if ( config.header !== undefined ) {
8525 this.$group
8526 .prepend( $( '<span>' )
8527 .addClass( 'oo-ui-popupToolGroup-header' )
8528 .text( config.header )
8529 );
8530 }
8531 this.$element
8532 .addClass( 'oo-ui-popupToolGroup' )
8533 .prepend( this.$handle );
8534 };
8535
8536 /* Setup */
8537
8538 OO.inheritClass( OO.ui.PopupToolGroup, OO.ui.ToolGroup );
8539 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IconElement );
8540 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IndicatorElement );
8541 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.LabelElement );
8542 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.TitledElement );
8543 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.ClippableElement );
8544
8545 /* Static Properties */
8546
8547 /* Methods */
8548
8549 /**
8550 * @inheritdoc
8551 */
8552 OO.ui.PopupToolGroup.prototype.setDisabled = function () {
8553 // Parent method
8554 OO.ui.PopupToolGroup.super.prototype.setDisabled.apply( this, arguments );
8555
8556 if ( this.isDisabled() && this.isElementAttached() ) {
8557 this.setActive( false );
8558 }
8559 };
8560
8561 /**
8562 * Handle focus being lost.
8563 *
8564 * The event is actually generated from a mouseup, so it is not a normal blur event object.
8565 *
8566 * @param {jQuery.Event} e Mouse up event
8567 */
8568 OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) {
8569 // Only deactivate when clicking outside the dropdown element
8570 if ( $( e.target ).closest( '.oo-ui-popupToolGroup' )[ 0 ] !== this.$element[ 0 ] ) {
8571 this.setActive( false );
8572 }
8573 };
8574
8575 /**
8576 * @inheritdoc
8577 */
8578 OO.ui.PopupToolGroup.prototype.onPointerUp = function ( e ) {
8579 // e.which is 0 for touch events, 1 for left mouse button
8580 // Only close toolgroup when a tool was actually selected
8581 // FIXME: this duplicates logic from the parent class
8582 if ( !this.isDisabled() && e.which <= 1 && this.pressed && this.pressed === this.getTargetTool( e ) ) {
8583 this.setActive( false );
8584 }
8585 return OO.ui.PopupToolGroup.super.prototype.onPointerUp.call( this, e );
8586 };
8587
8588 /**
8589 * Handle mouse up events.
8590 *
8591 * @param {jQuery.Event} e Mouse up event
8592 */
8593 OO.ui.PopupToolGroup.prototype.onHandlePointerUp = function () {
8594 return false;
8595 };
8596
8597 /**
8598 * Handle mouse down events.
8599 *
8600 * @param {jQuery.Event} e Mouse down event
8601 */
8602 OO.ui.PopupToolGroup.prototype.onHandlePointerDown = function ( e ) {
8603 // e.which is 0 for touch events, 1 for left mouse button
8604 if ( !this.isDisabled() && e.which <= 1 ) {
8605 this.setActive( !this.active );
8606 }
8607 return false;
8608 };
8609
8610 /**
8611 * Switch into active mode.
8612 *
8613 * When active, mouseup events anywhere in the document will trigger deactivation.
8614 */
8615 OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
8616 value = !!value;
8617 if ( this.active !== value ) {
8618 this.active = value;
8619 if ( value ) {
8620 this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler, true );
8621
8622 // Try anchoring the popup to the left first
8623 this.$element.addClass( 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left' );
8624 this.toggleClipping( true );
8625 if ( this.isClippedHorizontally() ) {
8626 // Anchoring to the left caused the popup to clip, so anchor it to the right instead
8627 this.toggleClipping( false );
8628 this.$element
8629 .removeClass( 'oo-ui-popupToolGroup-left' )
8630 .addClass( 'oo-ui-popupToolGroup-right' );
8631 this.toggleClipping( true );
8632 }
8633 } else {
8634 this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler, true );
8635 this.$element.removeClass(
8636 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left oo-ui-popupToolGroup-right'
8637 );
8638 this.toggleClipping( false );
8639 }
8640 }
8641 };
8642
8643 /**
8644 * Drop down list layout of tools as labeled icon buttons.
8645 *
8646 * This layout allows some tools to be collapsible, controlled by a "More" / "Fewer" option at the
8647 * bottom of the main list. These are not automatically positioned at the bottom of the list; you
8648 * may want to use the 'promote' and 'demote' configuration options to achieve this.
8649 *
8650 * @class
8651 * @extends OO.ui.PopupToolGroup
8652 *
8653 * @constructor
8654 * @param {OO.ui.Toolbar} toolbar
8655 * @param {Object} [config] Configuration options
8656 * @cfg {Array} [allowCollapse] List of tools that can be collapsed. Remaining tools will be always
8657 * shown.
8658 * @cfg {Array} [forceExpand] List of tools that *may not* be collapsed. All remaining tools will be
8659 * allowed to be collapsed.
8660 * @cfg {boolean} [expanded=false] Whether the collapsible tools are expanded by default
8661 */
8662 OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
8663 // Configuration initialization
8664 config = config || {};
8665
8666 // Properties (must be set before parent constructor, which calls #populate)
8667 this.allowCollapse = config.allowCollapse;
8668 this.forceExpand = config.forceExpand;
8669 this.expanded = config.expanded !== undefined ? config.expanded : false;
8670 this.collapsibleTools = [];
8671
8672 // Parent constructor
8673 OO.ui.ListToolGroup.super.call( this, toolbar, config );
8674
8675 // Initialization
8676 this.$element.addClass( 'oo-ui-listToolGroup' );
8677 };
8678
8679 /* Setup */
8680
8681 OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup );
8682
8683 /* Static Properties */
8684
8685 OO.ui.ListToolGroup.static.accelTooltips = true;
8686
8687 OO.ui.ListToolGroup.static.name = 'list';
8688
8689 /* Methods */
8690
8691 /**
8692 * @inheritdoc
8693 */
8694 OO.ui.ListToolGroup.prototype.populate = function () {
8695 var i, len, allowCollapse = [];
8696
8697 OO.ui.ListToolGroup.super.prototype.populate.call( this );
8698
8699 // Update the list of collapsible tools
8700 if ( this.allowCollapse !== undefined ) {
8701 allowCollapse = this.allowCollapse;
8702 } else if ( this.forceExpand !== undefined ) {
8703 allowCollapse = OO.simpleArrayDifference( Object.keys( this.tools ), this.forceExpand );
8704 }
8705
8706 this.collapsibleTools = [];
8707 for ( i = 0, len = allowCollapse.length; i < len; i++ ) {
8708 if ( this.tools[ allowCollapse[ i ] ] !== undefined ) {
8709 this.collapsibleTools.push( this.tools[ allowCollapse[ i ] ] );
8710 }
8711 }
8712
8713 // Keep at the end, even when tools are added
8714 this.$group.append( this.getExpandCollapseTool().$element );
8715
8716 this.getExpandCollapseTool().toggle( this.collapsibleTools.length !== 0 );
8717 this.updateCollapsibleState();
8718 };
8719
8720 OO.ui.ListToolGroup.prototype.getExpandCollapseTool = function () {
8721 if ( this.expandCollapseTool === undefined ) {
8722 var ExpandCollapseTool = function () {
8723 ExpandCollapseTool.super.apply( this, arguments );
8724 };
8725
8726 OO.inheritClass( ExpandCollapseTool, OO.ui.Tool );
8727
8728 ExpandCollapseTool.prototype.onSelect = function () {
8729 this.toolGroup.expanded = !this.toolGroup.expanded;
8730 this.toolGroup.updateCollapsibleState();
8731 this.setActive( false );
8732 };
8733 ExpandCollapseTool.prototype.onUpdateState = function () {
8734 // Do nothing. Tool interface requires an implementation of this function.
8735 };
8736
8737 ExpandCollapseTool.static.name = 'more-fewer';
8738
8739 this.expandCollapseTool = new ExpandCollapseTool( this );
8740 }
8741 return this.expandCollapseTool;
8742 };
8743
8744 /**
8745 * @inheritdoc
8746 */
8747 OO.ui.ListToolGroup.prototype.onPointerUp = function ( e ) {
8748 var ret = OO.ui.ListToolGroup.super.prototype.onPointerUp.call( this, e );
8749
8750 // Do not close the popup when the user wants to show more/fewer tools
8751 if ( $( e.target ).closest( '.oo-ui-tool-name-more-fewer' ).length ) {
8752 // Prevent the popup list from being hidden
8753 this.setActive( true );
8754 }
8755
8756 return ret;
8757 };
8758
8759 OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () {
8760 var i, len;
8761
8762 this.getExpandCollapseTool()
8763 .setIcon( this.expanded ? 'collapse' : 'expand' )
8764 .setTitle( OO.ui.msg( this.expanded ? 'ooui-toolgroup-collapse' : 'ooui-toolgroup-expand' ) );
8765
8766 for ( i = 0, len = this.collapsibleTools.length; i < len; i++ ) {
8767 this.collapsibleTools[ i ].toggle( this.expanded );
8768 }
8769 };
8770
8771 /**
8772 * Drop down menu layout of tools as selectable menu items.
8773 *
8774 * @class
8775 * @extends OO.ui.PopupToolGroup
8776 *
8777 * @constructor
8778 * @param {OO.ui.Toolbar} toolbar
8779 * @param {Object} [config] Configuration options
8780 */
8781 OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
8782 // Configuration initialization
8783 config = config || {};
8784
8785 // Parent constructor
8786 OO.ui.MenuToolGroup.super.call( this, toolbar, config );
8787
8788 // Events
8789 this.toolbar.connect( this, { updateState: 'onUpdateState' } );
8790
8791 // Initialization
8792 this.$element.addClass( 'oo-ui-menuToolGroup' );
8793 };
8794
8795 /* Setup */
8796
8797 OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup );
8798
8799 /* Static Properties */
8800
8801 OO.ui.MenuToolGroup.static.accelTooltips = true;
8802
8803 OO.ui.MenuToolGroup.static.name = 'menu';
8804
8805 /* Methods */
8806
8807 /**
8808 * Handle the toolbar state being updated.
8809 *
8810 * When the state changes, the title of each active item in the menu will be joined together and
8811 * used as a label for the group. The label will be empty if none of the items are active.
8812 */
8813 OO.ui.MenuToolGroup.prototype.onUpdateState = function () {
8814 var name,
8815 labelTexts = [];
8816
8817 for ( name in this.tools ) {
8818 if ( this.tools[ name ].isActive() ) {
8819 labelTexts.push( this.tools[ name ].getTitle() );
8820 }
8821 }
8822
8823 this.setLabel( labelTexts.join( ', ' ) || ' ' );
8824 };
8825
8826 /**
8827 * Tool that shows a popup when selected.
8828 *
8829 * @abstract
8830 * @class
8831 * @extends OO.ui.Tool
8832 * @mixins OO.ui.PopupElement
8833 *
8834 * @constructor
8835 * @param {OO.ui.Toolbar} toolbar
8836 * @param {Object} [config] Configuration options
8837 */
8838 OO.ui.PopupTool = function OoUiPopupTool( toolbar, config ) {
8839 // Parent constructor
8840 OO.ui.PopupTool.super.call( this, toolbar, config );
8841
8842 // Mixin constructors
8843 OO.ui.PopupElement.call( this, config );
8844
8845 // Initialization
8846 this.$element
8847 .addClass( 'oo-ui-popupTool' )
8848 .append( this.popup.$element );
8849 };
8850
8851 /* Setup */
8852
8853 OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool );
8854 OO.mixinClass( OO.ui.PopupTool, OO.ui.PopupElement );
8855
8856 /* Methods */
8857
8858 /**
8859 * Handle the tool being selected.
8860 *
8861 * @inheritdoc
8862 */
8863 OO.ui.PopupTool.prototype.onSelect = function () {
8864 if ( !this.isDisabled() ) {
8865 this.popup.toggle();
8866 }
8867 this.setActive( false );
8868 return false;
8869 };
8870
8871 /**
8872 * Handle the toolbar state being updated.
8873 *
8874 * @inheritdoc
8875 */
8876 OO.ui.PopupTool.prototype.onUpdateState = function () {
8877 this.setActive( false );
8878 };
8879
8880 /**
8881 * Mixin for OO.ui.Widget subclasses to provide OO.ui.GroupElement.
8882 *
8883 * Use together with OO.ui.ItemWidget to make disabled state inheritable.
8884 *
8885 * @abstract
8886 * @class
8887 * @extends OO.ui.GroupElement
8888 *
8889 * @constructor
8890 * @param {Object} [config] Configuration options
8891 */
8892 OO.ui.GroupWidget = function OoUiGroupWidget( config ) {
8893 // Parent constructor
8894 OO.ui.GroupWidget.super.call( this, config );
8895 };
8896
8897 /* Setup */
8898
8899 OO.inheritClass( OO.ui.GroupWidget, OO.ui.GroupElement );
8900
8901 /* Methods */
8902
8903 /**
8904 * Set the disabled state of the widget.
8905 *
8906 * This will also update the disabled state of child widgets.
8907 *
8908 * @param {boolean} disabled Disable widget
8909 * @chainable
8910 */
8911 OO.ui.GroupWidget.prototype.setDisabled = function ( disabled ) {
8912 var i, len;
8913
8914 // Parent method
8915 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
8916 OO.ui.Widget.prototype.setDisabled.call( this, disabled );
8917
8918 // During construction, #setDisabled is called before the OO.ui.GroupElement constructor
8919 if ( this.items ) {
8920 for ( i = 0, len = this.items.length; i < len; i++ ) {
8921 this.items[ i ].updateDisabled();
8922 }
8923 }
8924
8925 return this;
8926 };
8927
8928 /**
8929 * Mixin for widgets used as items in widgets that inherit OO.ui.GroupWidget.
8930 *
8931 * Item widgets have a reference to a OO.ui.GroupWidget while they are attached to the group. This
8932 * allows bidirectional communication.
8933 *
8934 * Use together with OO.ui.GroupWidget to make disabled state inheritable.
8935 *
8936 * @abstract
8937 * @class
8938 *
8939 * @constructor
8940 */
8941 OO.ui.ItemWidget = function OoUiItemWidget() {
8942 //
8943 };
8944
8945 /* Methods */
8946
8947 /**
8948 * Check if widget is disabled.
8949 *
8950 * Checks parent if present, making disabled state inheritable.
8951 *
8952 * @return {boolean} Widget is disabled
8953 */
8954 OO.ui.ItemWidget.prototype.isDisabled = function () {
8955 return this.disabled ||
8956 ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() );
8957 };
8958
8959 /**
8960 * Set group element is in.
8961 *
8962 * @param {OO.ui.GroupElement|null} group Group element, null if none
8963 * @chainable
8964 */
8965 OO.ui.ItemWidget.prototype.setElementGroup = function ( group ) {
8966 // Parent method
8967 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
8968 OO.ui.Element.prototype.setElementGroup.call( this, group );
8969
8970 // Initialize item disabled states
8971 this.updateDisabled();
8972
8973 return this;
8974 };
8975
8976 /**
8977 * Mixin that adds a menu showing suggested values for a text input.
8978 *
8979 * Subclasses must handle `select` and `choose` events on #lookupMenu to make use of selections.
8980 *
8981 * Subclasses that set the value of #lookupInput from their `choose` or `select` handler should
8982 * be aware that this will cause new suggestions to be looked up for the new value. If this is
8983 * not desired, disable lookups with #setLookupsDisabled, then set the value, then re-enable lookups.
8984 *
8985 * @class
8986 * @abstract
8987 * @deprecated Use OO.ui.LookupElement instead.
8988 *
8989 * @constructor
8990 * @param {OO.ui.TextInputWidget} input Input widget
8991 * @param {Object} [config] Configuration options
8992 * @cfg {jQuery} [$overlay] Overlay for dropdown; defaults to relative positioning
8993 * @cfg {jQuery} [$container=input.$element] Element to render menu under
8994 */
8995 OO.ui.LookupInputWidget = function OoUiLookupInputWidget( input, config ) {
8996 // Configuration initialization
8997 config = config || {};
8998
8999 // Properties
9000 this.lookupInput = input;
9001 this.$overlay = config.$overlay || this.$element;
9002 this.lookupMenu = new OO.ui.TextInputMenuSelectWidget( this, {
9003 input: this.lookupInput,
9004 $container: config.$container
9005 } );
9006 this.lookupCache = {};
9007 this.lookupQuery = null;
9008 this.lookupRequest = null;
9009 this.lookupsDisabled = false;
9010 this.lookupInputFocused = false;
9011
9012 // Events
9013 this.lookupInput.$input.on( {
9014 focus: this.onLookupInputFocus.bind( this ),
9015 blur: this.onLookupInputBlur.bind( this ),
9016 mousedown: this.onLookupInputMouseDown.bind( this )
9017 } );
9018 this.lookupInput.connect( this, { change: 'onLookupInputChange' } );
9019 this.lookupMenu.connect( this, { toggle: 'onLookupMenuToggle' } );
9020
9021 // Initialization
9022 this.$element.addClass( 'oo-ui-lookupWidget' );
9023 this.lookupMenu.$element.addClass( 'oo-ui-lookupWidget-menu' );
9024 this.$overlay.append( this.lookupMenu.$element );
9025 };
9026
9027 /* Methods */
9028
9029 /**
9030 * Handle input focus event.
9031 *
9032 * @param {jQuery.Event} e Input focus event
9033 */
9034 OO.ui.LookupInputWidget.prototype.onLookupInputFocus = function () {
9035 this.lookupInputFocused = true;
9036 this.populateLookupMenu();
9037 };
9038
9039 /**
9040 * Handle input blur event.
9041 *
9042 * @param {jQuery.Event} e Input blur event
9043 */
9044 OO.ui.LookupInputWidget.prototype.onLookupInputBlur = function () {
9045 this.closeLookupMenu();
9046 this.lookupInputFocused = false;
9047 };
9048
9049 /**
9050 * Handle input mouse down event.
9051 *
9052 * @param {jQuery.Event} e Input mouse down event
9053 */
9054 OO.ui.LookupInputWidget.prototype.onLookupInputMouseDown = function () {
9055 // Only open the menu if the input was already focused.
9056 // This way we allow the user to open the menu again after closing it with Esc
9057 // by clicking in the input. Opening (and populating) the menu when initially
9058 // clicking into the input is handled by the focus handler.
9059 if ( this.lookupInputFocused && !this.lookupMenu.isVisible() ) {
9060 this.populateLookupMenu();
9061 }
9062 };
9063
9064 /**
9065 * Handle input change event.
9066 *
9067 * @param {string} value New input value
9068 */
9069 OO.ui.LookupInputWidget.prototype.onLookupInputChange = function () {
9070 if ( this.lookupInputFocused ) {
9071 this.populateLookupMenu();
9072 }
9073 };
9074
9075 /**
9076 * Handle the lookup menu being shown/hidden.
9077 * @param {boolean} visible Whether the lookup menu is now visible.
9078 */
9079 OO.ui.LookupInputWidget.prototype.onLookupMenuToggle = function ( visible ) {
9080 if ( !visible ) {
9081 // When the menu is hidden, abort any active request and clear the menu.
9082 // This has to be done here in addition to closeLookupMenu(), because
9083 // MenuSelectWidget will close itself when the user presses Esc.
9084 this.abortLookupRequest();
9085 this.lookupMenu.clearItems();
9086 }
9087 };
9088
9089 /**
9090 * Get lookup menu.
9091 *
9092 * @return {OO.ui.TextInputMenuSelectWidget}
9093 */
9094 OO.ui.LookupInputWidget.prototype.getLookupMenu = function () {
9095 return this.lookupMenu;
9096 };
9097
9098 /**
9099 * Disable or re-enable lookups.
9100 *
9101 * When lookups are disabled, calls to #populateLookupMenu will be ignored.
9102 *
9103 * @param {boolean} disabled Disable lookups
9104 */
9105 OO.ui.LookupInputWidget.prototype.setLookupsDisabled = function ( disabled ) {
9106 this.lookupsDisabled = !!disabled;
9107 };
9108
9109 /**
9110 * Open the menu. If there are no entries in the menu, this does nothing.
9111 *
9112 * @chainable
9113 */
9114 OO.ui.LookupInputWidget.prototype.openLookupMenu = function () {
9115 if ( !this.lookupMenu.isEmpty() ) {
9116 this.lookupMenu.toggle( true );
9117 }
9118 return this;
9119 };
9120
9121 /**
9122 * Close the menu, empty it, and abort any pending request.
9123 *
9124 * @chainable
9125 */
9126 OO.ui.LookupInputWidget.prototype.closeLookupMenu = function () {
9127 this.lookupMenu.toggle( false );
9128 this.abortLookupRequest();
9129 this.lookupMenu.clearItems();
9130 return this;
9131 };
9132
9133 /**
9134 * Request menu items based on the input's current value, and when they arrive,
9135 * populate the menu with these items and show the menu.
9136 *
9137 * If lookups have been disabled with #setLookupsDisabled, this function does nothing.
9138 *
9139 * @chainable
9140 */
9141 OO.ui.LookupInputWidget.prototype.populateLookupMenu = function () {
9142 var widget = this,
9143 value = this.lookupInput.getValue();
9144
9145 if ( this.lookupsDisabled ) {
9146 return;
9147 }
9148
9149 // If the input is empty, clear the menu
9150 if ( value === '' ) {
9151 this.closeLookupMenu();
9152 // Skip population if there is already a request pending for the current value
9153 } else if ( value !== this.lookupQuery ) {
9154 this.getLookupMenuItems()
9155 .done( function ( items ) {
9156 widget.lookupMenu.clearItems();
9157 if ( items.length ) {
9158 widget.lookupMenu
9159 .addItems( items )
9160 .toggle( true );
9161 widget.initializeLookupMenuSelection();
9162 } else {
9163 widget.lookupMenu.toggle( false );
9164 }
9165 } )
9166 .fail( function () {
9167 widget.lookupMenu.clearItems();
9168 } );
9169 }
9170
9171 return this;
9172 };
9173
9174 /**
9175 * Select and highlight the first selectable item in the menu.
9176 *
9177 * @chainable
9178 */
9179 OO.ui.LookupInputWidget.prototype.initializeLookupMenuSelection = function () {
9180 if ( !this.lookupMenu.getSelectedItem() ) {
9181 this.lookupMenu.selectItem( this.lookupMenu.getFirstSelectableItem() );
9182 }
9183 this.lookupMenu.highlightItem( this.lookupMenu.getSelectedItem() );
9184 };
9185
9186 /**
9187 * Get lookup menu items for the current query.
9188 *
9189 * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument
9190 * of the done event. If the request was aborted to make way for a subsequent request,
9191 * this promise will not be rejected: it will remain pending forever.
9192 */
9193 OO.ui.LookupInputWidget.prototype.getLookupMenuItems = function () {
9194 var widget = this,
9195 value = this.lookupInput.getValue(),
9196 deferred = $.Deferred(),
9197 ourRequest;
9198
9199 this.abortLookupRequest();
9200 if ( Object.prototype.hasOwnProperty.call( this.lookupCache, value ) ) {
9201 deferred.resolve( this.getLookupMenuItemsFromData( this.lookupCache[ value ] ) );
9202 } else {
9203 this.lookupInput.pushPending();
9204 this.lookupQuery = value;
9205 ourRequest = this.lookupRequest = this.getLookupRequest();
9206 ourRequest
9207 .always( function () {
9208 // We need to pop pending even if this is an old request, otherwise
9209 // the widget will remain pending forever.
9210 // TODO: this assumes that an aborted request will fail or succeed soon after
9211 // being aborted, or at least eventually. It would be nice if we could popPending()
9212 // at abort time, but only if we knew that we hadn't already called popPending()
9213 // for that request.
9214 widget.lookupInput.popPending();
9215 } )
9216 .done( function ( data ) {
9217 // If this is an old request (and aborting it somehow caused it to still succeed),
9218 // ignore its success completely
9219 if ( ourRequest === widget.lookupRequest ) {
9220 widget.lookupQuery = null;
9221 widget.lookupRequest = null;
9222 widget.lookupCache[ value ] = widget.getLookupCacheItemFromData( data );
9223 deferred.resolve( widget.getLookupMenuItemsFromData( widget.lookupCache[ value ] ) );
9224 }
9225 } )
9226 .fail( function () {
9227 // If this is an old request (or a request failing because it's being aborted),
9228 // ignore its failure completely
9229 if ( ourRequest === widget.lookupRequest ) {
9230 widget.lookupQuery = null;
9231 widget.lookupRequest = null;
9232 deferred.reject();
9233 }
9234 } );
9235 }
9236 return deferred.promise();
9237 };
9238
9239 /**
9240 * Abort the currently pending lookup request, if any.
9241 */
9242 OO.ui.LookupInputWidget.prototype.abortLookupRequest = function () {
9243 var oldRequest = this.lookupRequest;
9244 if ( oldRequest ) {
9245 // First unset this.lookupRequest to the fail handler will notice
9246 // that the request is no longer current
9247 this.lookupRequest = null;
9248 this.lookupQuery = null;
9249 oldRequest.abort();
9250 }
9251 };
9252
9253 /**
9254 * Get a new request object of the current lookup query value.
9255 *
9256 * @abstract
9257 * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
9258 */
9259 OO.ui.LookupInputWidget.prototype.getLookupRequest = function () {
9260 // Stub, implemented in subclass
9261 return null;
9262 };
9263
9264 /**
9265 * Get a list of menu item widgets from the data stored by the lookup request's done handler.
9266 *
9267 * @abstract
9268 * @param {Mixed} data Cached result data, usually an array
9269 * @return {OO.ui.MenuOptionWidget[]} Menu items
9270 */
9271 OO.ui.LookupInputWidget.prototype.getLookupMenuItemsFromData = function () {
9272 // Stub, implemented in subclass
9273 return [];
9274 };
9275
9276 /**
9277 * Get lookup cache item from server response data.
9278 *
9279 * @abstract
9280 * @param {Mixed} data Response from server
9281 * @return {Mixed} Cached result data
9282 */
9283 OO.ui.LookupInputWidget.prototype.getLookupCacheItemFromData = function () {
9284 // Stub, implemented in subclass
9285 return [];
9286 };
9287
9288 /**
9289 * Set of controls for an OO.ui.OutlineSelectWidget.
9290 *
9291 * Controls include moving items up and down, removing items, and adding different kinds of items.
9292 *
9293 * @class
9294 * @extends OO.ui.Widget
9295 * @mixins OO.ui.GroupElement
9296 * @mixins OO.ui.IconElement
9297 *
9298 * @constructor
9299 * @param {OO.ui.OutlineSelectWidget} outline Outline to control
9300 * @param {Object} [config] Configuration options
9301 */
9302 OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
9303 // Configuration initialization
9304 config = $.extend( { icon: 'add' }, config );
9305
9306 // Parent constructor
9307 OO.ui.OutlineControlsWidget.super.call( this, config );
9308
9309 // Mixin constructors
9310 OO.ui.GroupElement.call( this, config );
9311 OO.ui.IconElement.call( this, config );
9312
9313 // Properties
9314 this.outline = outline;
9315 this.$movers = $( '<div>' );
9316 this.upButton = new OO.ui.ButtonWidget( {
9317 framed: false,
9318 icon: 'collapse',
9319 title: OO.ui.msg( 'ooui-outline-control-move-up' )
9320 } );
9321 this.downButton = new OO.ui.ButtonWidget( {
9322 framed: false,
9323 icon: 'expand',
9324 title: OO.ui.msg( 'ooui-outline-control-move-down' )
9325 } );
9326 this.removeButton = new OO.ui.ButtonWidget( {
9327 framed: false,
9328 icon: 'remove',
9329 title: OO.ui.msg( 'ooui-outline-control-remove' )
9330 } );
9331
9332 // Events
9333 outline.connect( this, {
9334 select: 'onOutlineChange',
9335 add: 'onOutlineChange',
9336 remove: 'onOutlineChange'
9337 } );
9338 this.upButton.connect( this, { click: [ 'emit', 'move', -1 ] } );
9339 this.downButton.connect( this, { click: [ 'emit', 'move', 1 ] } );
9340 this.removeButton.connect( this, { click: [ 'emit', 'remove' ] } );
9341
9342 // Initialization
9343 this.$element.addClass( 'oo-ui-outlineControlsWidget' );
9344 this.$group.addClass( 'oo-ui-outlineControlsWidget-items' );
9345 this.$movers
9346 .addClass( 'oo-ui-outlineControlsWidget-movers' )
9347 .append( this.removeButton.$element, this.upButton.$element, this.downButton.$element );
9348 this.$element.append( this.$icon, this.$group, this.$movers );
9349 };
9350
9351 /* Setup */
9352
9353 OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget );
9354 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.GroupElement );
9355 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.IconElement );
9356
9357 /* Events */
9358
9359 /**
9360 * @event move
9361 * @param {number} places Number of places to move
9362 */
9363
9364 /**
9365 * @event remove
9366 */
9367
9368 /* Methods */
9369
9370 /**
9371 * Handle outline change events.
9372 */
9373 OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () {
9374 var i, len, firstMovable, lastMovable,
9375 items = this.outline.getItems(),
9376 selectedItem = this.outline.getSelectedItem(),
9377 movable = selectedItem && selectedItem.isMovable(),
9378 removable = selectedItem && selectedItem.isRemovable();
9379
9380 if ( movable ) {
9381 i = -1;
9382 len = items.length;
9383 while ( ++i < len ) {
9384 if ( items[ i ].isMovable() ) {
9385 firstMovable = items[ i ];
9386 break;
9387 }
9388 }
9389 i = len;
9390 while ( i-- ) {
9391 if ( items[ i ].isMovable() ) {
9392 lastMovable = items[ i ];
9393 break;
9394 }
9395 }
9396 }
9397 this.upButton.setDisabled( !movable || selectedItem === firstMovable );
9398 this.downButton.setDisabled( !movable || selectedItem === lastMovable );
9399 this.removeButton.setDisabled( !removable );
9400 };
9401
9402 /**
9403 * Mixin for widgets with a boolean on/off state.
9404 *
9405 * @abstract
9406 * @class
9407 *
9408 * @constructor
9409 * @param {Object} [config] Configuration options
9410 * @cfg {boolean} [value=false] Initial value
9411 */
9412 OO.ui.ToggleWidget = function OoUiToggleWidget( config ) {
9413 // Configuration initialization
9414 config = config || {};
9415
9416 // Properties
9417 this.value = null;
9418
9419 // Initialization
9420 this.$element.addClass( 'oo-ui-toggleWidget' );
9421 this.setValue( !!config.value );
9422 };
9423
9424 /* Events */
9425
9426 /**
9427 * @event change
9428 * @param {boolean} value Changed value
9429 */
9430
9431 /* Methods */
9432
9433 /**
9434 * Get the value of the toggle.
9435 *
9436 * @return {boolean}
9437 */
9438 OO.ui.ToggleWidget.prototype.getValue = function () {
9439 return this.value;
9440 };
9441
9442 /**
9443 * Set the value of the toggle.
9444 *
9445 * @param {boolean} value New value
9446 * @fires change
9447 * @chainable
9448 */
9449 OO.ui.ToggleWidget.prototype.setValue = function ( value ) {
9450 value = !!value;
9451 if ( this.value !== value ) {
9452 this.value = value;
9453 this.emit( 'change', value );
9454 this.$element.toggleClass( 'oo-ui-toggleWidget-on', value );
9455 this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value );
9456 this.$element.attr( 'aria-checked', value.toString() );
9457 }
9458 return this;
9459 };
9460
9461 /**
9462 * Group widget for multiple related buttons.
9463 *
9464 * Use together with OO.ui.ButtonWidget.
9465 *
9466 * @class
9467 * @extends OO.ui.Widget
9468 * @mixins OO.ui.GroupElement
9469 *
9470 * @constructor
9471 * @param {Object} [config] Configuration options
9472 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
9473 */
9474 OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
9475 // Configuration initialization
9476 config = config || {};
9477
9478 // Parent constructor
9479 OO.ui.ButtonGroupWidget.super.call( this, config );
9480
9481 // Mixin constructors
9482 OO.ui.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
9483
9484 // Initialization
9485 this.$element.addClass( 'oo-ui-buttonGroupWidget' );
9486 if ( $.isArray( config.items ) ) {
9487 this.addItems( config.items );
9488 }
9489 };
9490
9491 /* Setup */
9492
9493 OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget );
9494 OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.GroupElement );
9495
9496 /**
9497 * ButtonWidget is a generic widget for buttons. A wide variety of looks,
9498 * feels, and functionality can be customized via the class’s configuration options
9499 * and methods. Please see the OOjs UI documentation on MediaWiki for more information
9500 * and examples.
9501 *
9502 * NOTE: HTML form buttons should use the OO.ui.ButtonInputWidget class.
9503 *
9504 * @class
9505 * @extends OO.ui.Widget
9506 * @mixins OO.ui.ButtonElement
9507 * @mixins OO.ui.IconElement
9508 * @mixins OO.ui.IndicatorElement
9509 * @mixins OO.ui.LabelElement
9510 * @mixins OO.ui.TitledElement
9511 * @mixins OO.ui.FlaggedElement
9512 * @mixins OO.ui.TabIndexedElement
9513 *
9514 * @constructor
9515 * @param {Object} [config] Configuration options
9516 * @cfg {string} [href] Hyperlink to visit when clicked
9517 * @cfg {string} [target] Target to open hyperlink in
9518 * @cfg {boolean} [nofollow] Search engine traversal hint (default: true)
9519 */
9520 OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
9521 // Configuration initialization
9522 config = config || {};
9523
9524 // Parent constructor
9525 OO.ui.ButtonWidget.super.call( this, config );
9526
9527 // Mixin constructors
9528 OO.ui.ButtonElement.call( this, config );
9529 OO.ui.IconElement.call( this, config );
9530 OO.ui.IndicatorElement.call( this, config );
9531 OO.ui.LabelElement.call( this, config );
9532 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
9533 OO.ui.FlaggedElement.call( this, config );
9534 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
9535
9536 // Properties
9537 this.href = null;
9538 this.target = null;
9539 this.nofollow = false;
9540 this.isHyperlink = false;
9541
9542 // Initialization
9543 this.$button.append( this.$icon, this.$label, this.$indicator );
9544 this.$element
9545 .addClass( 'oo-ui-buttonWidget' )
9546 .append( this.$button );
9547 this.setHref( config.href );
9548 this.setTarget( config.target );
9549 this.setNoFollow( config.nofollow );
9550 };
9551
9552 /* Setup */
9553
9554 OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget );
9555 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.ButtonElement );
9556 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IconElement );
9557 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IndicatorElement );
9558 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.LabelElement );
9559 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TitledElement );
9560 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.FlaggedElement );
9561 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TabIndexedElement );
9562
9563 /* Methods */
9564
9565 /**
9566 * @inheritdoc
9567 */
9568 OO.ui.ButtonWidget.prototype.onMouseDown = function ( e ) {
9569 if ( !this.isDisabled() ) {
9570 // Remove the tab-index while the button is down to prevent the button from stealing focus
9571 this.$button.removeAttr( 'tabindex' );
9572 }
9573
9574 return OO.ui.ButtonElement.prototype.onMouseDown.call( this, e );
9575 };
9576
9577 /**
9578 * @inheritdoc
9579 */
9580 OO.ui.ButtonWidget.prototype.onMouseUp = function ( e ) {
9581 if ( !this.isDisabled() ) {
9582 // Restore the tab-index after the button is up to restore the button's accessibility
9583 this.$button.attr( 'tabindex', this.tabIndex );
9584 }
9585
9586 return OO.ui.ButtonElement.prototype.onMouseUp.call( this, e );
9587 };
9588
9589 /**
9590 * @inheritdoc
9591 */
9592 OO.ui.ButtonWidget.prototype.onClick = function ( e ) {
9593 var ret = OO.ui.ButtonElement.prototype.onClick.call( this, e );
9594 if ( this.isHyperlink ) {
9595 return true;
9596 }
9597 return ret;
9598 };
9599
9600 /**
9601 * @inheritdoc
9602 */
9603 OO.ui.ButtonWidget.prototype.onKeyPress = function ( e ) {
9604 var ret = OO.ui.ButtonElement.prototype.onKeyPress.call( this, e );
9605 if ( this.isHyperlink ) {
9606 return true;
9607 }
9608 return ret;
9609 };
9610
9611 /**
9612 * Get hyperlink location.
9613 *
9614 * @return {string} Hyperlink location
9615 */
9616 OO.ui.ButtonWidget.prototype.getHref = function () {
9617 return this.href;
9618 };
9619
9620 /**
9621 * Get hyperlink target.
9622 *
9623 * @return {string} Hyperlink target
9624 */
9625 OO.ui.ButtonWidget.prototype.getTarget = function () {
9626 return this.target;
9627 };
9628
9629 /**
9630 * Get search engine traversal hint.
9631 *
9632 * @return {boolean} Whether search engines should avoid traversing this hyperlink
9633 */
9634 OO.ui.ButtonWidget.prototype.getNoFollow = function () {
9635 return this.nofollow;
9636 };
9637
9638 /**
9639 * Set hyperlink location.
9640 *
9641 * @param {string|null} href Hyperlink location, null to remove
9642 */
9643 OO.ui.ButtonWidget.prototype.setHref = function ( href ) {
9644 href = typeof href === 'string' ? href : null;
9645
9646 if ( href !== this.href ) {
9647 this.href = href;
9648 if ( href !== null ) {
9649 this.$button.attr( 'href', href );
9650 this.isHyperlink = true;
9651 } else {
9652 this.$button.removeAttr( 'href' );
9653 this.isHyperlink = false;
9654 }
9655 }
9656
9657 return this;
9658 };
9659
9660 /**
9661 * Set hyperlink target.
9662 *
9663 * @param {string|null} target Hyperlink target, null to remove
9664 */
9665 OO.ui.ButtonWidget.prototype.setTarget = function ( target ) {
9666 target = typeof target === 'string' ? target : null;
9667
9668 if ( target !== this.target ) {
9669 this.target = target;
9670 if ( target !== null ) {
9671 this.$button.attr( 'target', target );
9672 } else {
9673 this.$button.removeAttr( 'target' );
9674 }
9675 }
9676
9677 return this;
9678 };
9679
9680 /**
9681 * Set search engine traversal hint.
9682 *
9683 * @param {boolean} nofollow True if search engines should avoid traversing this hyperlink
9684 */
9685 OO.ui.ButtonWidget.prototype.setNoFollow = function ( nofollow ) {
9686 nofollow = typeof nofollow === 'boolean' ? nofollow : true;
9687
9688 if ( nofollow !== this.nofollow ) {
9689 this.nofollow = nofollow;
9690 if ( nofollow ) {
9691 this.$button.attr( 'rel', 'nofollow' );
9692 } else {
9693 this.$button.removeAttr( 'rel' );
9694 }
9695 }
9696
9697 return this;
9698 };
9699
9700 /**
9701 * Button widget that executes an action and is managed by an OO.ui.ActionSet.
9702 *
9703 * @class
9704 * @extends OO.ui.ButtonWidget
9705 * @mixins OO.ui.PendingElement
9706 *
9707 * @constructor
9708 * @param {Object} [config] Configuration options
9709 * @cfg {string} [action] Symbolic action name
9710 * @cfg {string[]} [modes] Symbolic mode names
9711 * @cfg {boolean} [framed=false] Render button with a frame
9712 */
9713 OO.ui.ActionWidget = function OoUiActionWidget( config ) {
9714 // Configuration initialization
9715 config = $.extend( { framed: false }, config );
9716
9717 // Parent constructor
9718 OO.ui.ActionWidget.super.call( this, config );
9719
9720 // Mixin constructors
9721 OO.ui.PendingElement.call( this, config );
9722
9723 // Properties
9724 this.action = config.action || '';
9725 this.modes = config.modes || [];
9726 this.width = 0;
9727 this.height = 0;
9728
9729 // Initialization
9730 this.$element.addClass( 'oo-ui-actionWidget' );
9731 };
9732
9733 /* Setup */
9734
9735 OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget );
9736 OO.mixinClass( OO.ui.ActionWidget, OO.ui.PendingElement );
9737
9738 /* Events */
9739
9740 /**
9741 * @event resize
9742 */
9743
9744 /* Methods */
9745
9746 /**
9747 * Check if action is available in a certain mode.
9748 *
9749 * @param {string} mode Name of mode
9750 * @return {boolean} Has mode
9751 */
9752 OO.ui.ActionWidget.prototype.hasMode = function ( mode ) {
9753 return this.modes.indexOf( mode ) !== -1;
9754 };
9755
9756 /**
9757 * Get symbolic action name.
9758 *
9759 * @return {string}
9760 */
9761 OO.ui.ActionWidget.prototype.getAction = function () {
9762 return this.action;
9763 };
9764
9765 /**
9766 * Get symbolic action name.
9767 *
9768 * @return {string}
9769 */
9770 OO.ui.ActionWidget.prototype.getModes = function () {
9771 return this.modes.slice();
9772 };
9773
9774 /**
9775 * Emit a resize event if the size has changed.
9776 *
9777 * @chainable
9778 */
9779 OO.ui.ActionWidget.prototype.propagateResize = function () {
9780 var width, height;
9781
9782 if ( this.isElementAttached() ) {
9783 width = this.$element.width();
9784 height = this.$element.height();
9785
9786 if ( width !== this.width || height !== this.height ) {
9787 this.width = width;
9788 this.height = height;
9789 this.emit( 'resize' );
9790 }
9791 }
9792
9793 return this;
9794 };
9795
9796 /**
9797 * @inheritdoc
9798 */
9799 OO.ui.ActionWidget.prototype.setIcon = function () {
9800 // Mixin method
9801 OO.ui.IconElement.prototype.setIcon.apply( this, arguments );
9802 this.propagateResize();
9803
9804 return this;
9805 };
9806
9807 /**
9808 * @inheritdoc
9809 */
9810 OO.ui.ActionWidget.prototype.setLabel = function () {
9811 // Mixin method
9812 OO.ui.LabelElement.prototype.setLabel.apply( this, arguments );
9813 this.propagateResize();
9814
9815 return this;
9816 };
9817
9818 /**
9819 * @inheritdoc
9820 */
9821 OO.ui.ActionWidget.prototype.setFlags = function () {
9822 // Mixin method
9823 OO.ui.FlaggedElement.prototype.setFlags.apply( this, arguments );
9824 this.propagateResize();
9825
9826 return this;
9827 };
9828
9829 /**
9830 * @inheritdoc
9831 */
9832 OO.ui.ActionWidget.prototype.clearFlags = function () {
9833 // Mixin method
9834 OO.ui.FlaggedElement.prototype.clearFlags.apply( this, arguments );
9835 this.propagateResize();
9836
9837 return this;
9838 };
9839
9840 /**
9841 * Toggle visibility of button.
9842 *
9843 * @param {boolean} [show] Show button, omit to toggle visibility
9844 * @chainable
9845 */
9846 OO.ui.ActionWidget.prototype.toggle = function () {
9847 // Parent method
9848 OO.ui.ActionWidget.super.prototype.toggle.apply( this, arguments );
9849 this.propagateResize();
9850
9851 return this;
9852 };
9853
9854 /**
9855 * Button that shows and hides a popup.
9856 *
9857 * @class
9858 * @extends OO.ui.ButtonWidget
9859 * @mixins OO.ui.PopupElement
9860 *
9861 * @constructor
9862 * @param {Object} [config] Configuration options
9863 */
9864 OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
9865 // Parent constructor
9866 OO.ui.PopupButtonWidget.super.call( this, config );
9867
9868 // Mixin constructors
9869 OO.ui.PopupElement.call( this, config );
9870
9871 // Events
9872 this.connect( this, { click: 'onAction' } );
9873
9874 // Initialization
9875 this.$element
9876 .addClass( 'oo-ui-popupButtonWidget' )
9877 .attr( 'aria-haspopup', 'true' )
9878 .append( this.popup.$element );
9879 };
9880
9881 /* Setup */
9882
9883 OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
9884 OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.PopupElement );
9885
9886 /* Methods */
9887
9888 /**
9889 * Handle the button action being triggered.
9890 */
9891 OO.ui.PopupButtonWidget.prototype.onAction = function () {
9892 this.popup.toggle();
9893 };
9894
9895 /**
9896 * Button that toggles on and off.
9897 *
9898 * @class
9899 * @extends OO.ui.ButtonWidget
9900 * @mixins OO.ui.ToggleWidget
9901 *
9902 * @constructor
9903 * @param {Object} [config] Configuration options
9904 * @cfg {boolean} [value=false] Initial value
9905 */
9906 OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) {
9907 // Configuration initialization
9908 config = config || {};
9909
9910 // Parent constructor
9911 OO.ui.ToggleButtonWidget.super.call( this, config );
9912
9913 // Mixin constructors
9914 OO.ui.ToggleWidget.call( this, config );
9915
9916 // Events
9917 this.connect( this, { click: 'onAction' } );
9918
9919 // Initialization
9920 this.$element.addClass( 'oo-ui-toggleButtonWidget' );
9921 };
9922
9923 /* Setup */
9924
9925 OO.inheritClass( OO.ui.ToggleButtonWidget, OO.ui.ButtonWidget );
9926 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget );
9927
9928 /* Methods */
9929
9930 /**
9931 * Handle the button action being triggered.
9932 */
9933 OO.ui.ToggleButtonWidget.prototype.onAction = function () {
9934 this.setValue( !this.value );
9935 };
9936
9937 /**
9938 * @inheritdoc
9939 */
9940 OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
9941 value = !!value;
9942 if ( value !== this.value ) {
9943 this.$button.attr( 'aria-pressed', value.toString() );
9944 this.setActive( value );
9945 }
9946
9947 // Parent method (from mixin)
9948 OO.ui.ToggleWidget.prototype.setValue.call( this, value );
9949
9950 return this;
9951 };
9952
9953 /**
9954 * Dropdown menu of options.
9955 *
9956 * Dropdown menus provide a control for accessing a menu and compose a menu within the widget, which
9957 * can be accessed using the #getMenu method.
9958 *
9959 * Use with OO.ui.MenuOptionWidget.
9960 *
9961 * @class
9962 * @extends OO.ui.Widget
9963 * @mixins OO.ui.IconElement
9964 * @mixins OO.ui.IndicatorElement
9965 * @mixins OO.ui.LabelElement
9966 * @mixins OO.ui.TitledElement
9967 * @mixins OO.ui.TabIndexedElement
9968 *
9969 * @constructor
9970 * @param {Object} [config] Configuration options
9971 * @cfg {Object} [menu] Configuration options to pass to menu widget
9972 */
9973 OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
9974 // Configuration initialization
9975 config = $.extend( { indicator: 'down' }, config );
9976
9977 // Parent constructor
9978 OO.ui.DropdownWidget.super.call( this, config );
9979
9980 // Properties (must be set before TabIndexedElement constructor call)
9981 this.$handle = this.$( '<span>' );
9982
9983 // Mixin constructors
9984 OO.ui.IconElement.call( this, config );
9985 OO.ui.IndicatorElement.call( this, config );
9986 OO.ui.LabelElement.call( this, config );
9987 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
9988 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$handle } ) );
9989
9990 // Properties
9991 this.menu = new OO.ui.MenuSelectWidget( $.extend( { widget: this }, config.menu ) );
9992
9993 // Events
9994 this.$handle.on( {
9995 click: this.onClick.bind( this ),
9996 keypress: this.onKeyPress.bind( this )
9997 } );
9998 this.menu.connect( this, { select: 'onMenuSelect' } );
9999
10000 // Initialization
10001 this.$handle
10002 .addClass( 'oo-ui-dropdownWidget-handle' )
10003 .append( this.$icon, this.$label, this.$indicator );
10004 this.$element
10005 .addClass( 'oo-ui-dropdownWidget' )
10006 .append( this.$handle, this.menu.$element );
10007 };
10008
10009 /* Setup */
10010
10011 OO.inheritClass( OO.ui.DropdownWidget, OO.ui.Widget );
10012 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.IconElement );
10013 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.IndicatorElement );
10014 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.LabelElement );
10015 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.TitledElement );
10016 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.TabIndexedElement );
10017
10018 /* Methods */
10019
10020 /**
10021 * Get the menu.
10022 *
10023 * @return {OO.ui.MenuSelectWidget} Menu of widget
10024 */
10025 OO.ui.DropdownWidget.prototype.getMenu = function () {
10026 return this.menu;
10027 };
10028
10029 /**
10030 * Handles menu select events.
10031 *
10032 * @param {OO.ui.MenuOptionWidget} item Selected menu item
10033 */
10034 OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
10035 var selectedLabel;
10036
10037 if ( !item ) {
10038 return;
10039 }
10040
10041 selectedLabel = item.getLabel();
10042
10043 // If the label is a DOM element, clone it, because setLabel will append() it
10044 if ( selectedLabel instanceof jQuery ) {
10045 selectedLabel = selectedLabel.clone();
10046 }
10047
10048 this.setLabel( selectedLabel );
10049 };
10050
10051 /**
10052 * Handle mouse click events.
10053 *
10054 * @param {jQuery.Event} e Mouse click event
10055 */
10056 OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
10057 if ( !this.isDisabled() && e.which === 1 ) {
10058 if ( this.menu.isVisible() ) {
10059 this.menu.toggle( false );
10060 } else {
10061 this.menu.toggle( true );
10062 }
10063 }
10064 return false;
10065 };
10066
10067 /**
10068 * Handle key press events.
10069 *
10070 * @param {jQuery.Event} e Key press event
10071 */
10072 OO.ui.DropdownWidget.prototype.onKeyPress = function ( e ) {
10073 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
10074 if ( this.menu.isVisible() ) {
10075 this.menu.toggle( false );
10076 } else {
10077 this.menu.toggle( true );
10078 }
10079 }
10080 return false;
10081 };
10082
10083 /**
10084 * Icon widget.
10085 *
10086 * See OO.ui.IconElement for more information.
10087 *
10088 * @class
10089 * @extends OO.ui.Widget
10090 * @mixins OO.ui.IconElement
10091 * @mixins OO.ui.TitledElement
10092 *
10093 * @constructor
10094 * @param {Object} [config] Configuration options
10095 */
10096 OO.ui.IconWidget = function OoUiIconWidget( config ) {
10097 // Configuration initialization
10098 config = config || {};
10099
10100 // Parent constructor
10101 OO.ui.IconWidget.super.call( this, config );
10102
10103 // Mixin constructors
10104 OO.ui.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) );
10105 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
10106
10107 // Initialization
10108 this.$element.addClass( 'oo-ui-iconWidget' );
10109 };
10110
10111 /* Setup */
10112
10113 OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
10114 OO.mixinClass( OO.ui.IconWidget, OO.ui.IconElement );
10115 OO.mixinClass( OO.ui.IconWidget, OO.ui.TitledElement );
10116
10117 /* Static Properties */
10118
10119 OO.ui.IconWidget.static.tagName = 'span';
10120
10121 /**
10122 * Indicator widget.
10123 *
10124 * See OO.ui.IndicatorElement for more information.
10125 *
10126 * @class
10127 * @extends OO.ui.Widget
10128 * @mixins OO.ui.IndicatorElement
10129 * @mixins OO.ui.TitledElement
10130 *
10131 * @constructor
10132 * @param {Object} [config] Configuration options
10133 */
10134 OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
10135 // Configuration initialization
10136 config = config || {};
10137
10138 // Parent constructor
10139 OO.ui.IndicatorWidget.super.call( this, config );
10140
10141 // Mixin constructors
10142 OO.ui.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) );
10143 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
10144
10145 // Initialization
10146 this.$element.addClass( 'oo-ui-indicatorWidget' );
10147 };
10148
10149 /* Setup */
10150
10151 OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
10152 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.IndicatorElement );
10153 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.TitledElement );
10154
10155 /* Static Properties */
10156
10157 OO.ui.IndicatorWidget.static.tagName = 'span';
10158
10159 /**
10160 * Base class for input widgets.
10161 *
10162 * @abstract
10163 * @class
10164 * @extends OO.ui.Widget
10165 * @mixins OO.ui.FlaggedElement
10166 * @mixins OO.ui.TabIndexedElement
10167 *
10168 * @constructor
10169 * @param {Object} [config] Configuration options
10170 * @cfg {string} [name=''] HTML input name
10171 * @cfg {string} [value=''] Input value
10172 * @cfg {Function} [inputFilter] Filter function to apply to the input. Takes a string argument and returns a string.
10173 */
10174 OO.ui.InputWidget = function OoUiInputWidget( config ) {
10175 // Configuration initialization
10176 config = config || {};
10177
10178 // Parent constructor
10179 OO.ui.InputWidget.super.call( this, config );
10180
10181 // Properties
10182 this.$input = this.getInputElement( config );
10183 this.value = '';
10184 this.inputFilter = config.inputFilter;
10185
10186 // Mixin constructors
10187 OO.ui.FlaggedElement.call( this, config );
10188 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$input } ) );
10189
10190 // Events
10191 this.$input.on( 'keydown mouseup cut paste change input select', this.onEdit.bind( this ) );
10192
10193 // Initialization
10194 this.$input
10195 .attr( 'name', config.name )
10196 .prop( 'disabled', this.isDisabled() );
10197 this.$element.addClass( 'oo-ui-inputWidget' ).append( this.$input, $( '<span>' ) );
10198 this.setValue( config.value );
10199 };
10200
10201 /* Setup */
10202
10203 OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
10204 OO.mixinClass( OO.ui.InputWidget, OO.ui.FlaggedElement );
10205 OO.mixinClass( OO.ui.InputWidget, OO.ui.TabIndexedElement );
10206
10207 /* Events */
10208
10209 /**
10210 * @event change
10211 * @param {string} value
10212 */
10213
10214 /* Methods */
10215
10216 /**
10217 * Get input element.
10218 *
10219 * Subclasses of OO.ui.InputWidget use the `config` parameter to produce different elements in
10220 * different circumstances. The element must have a `value` property (like form elements).
10221 *
10222 * @private
10223 * @param {Object} config Configuration options
10224 * @return {jQuery} Input element
10225 */
10226 OO.ui.InputWidget.prototype.getInputElement = function () {
10227 return $( '<input>' );
10228 };
10229
10230 /**
10231 * Handle potentially value-changing events.
10232 *
10233 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
10234 */
10235 OO.ui.InputWidget.prototype.onEdit = function () {
10236 var widget = this;
10237 if ( !this.isDisabled() ) {
10238 // Allow the stack to clear so the value will be updated
10239 setTimeout( function () {
10240 widget.setValue( widget.$input.val() );
10241 } );
10242 }
10243 };
10244
10245 /**
10246 * Get the value of the input.
10247 *
10248 * @return {string} Input value
10249 */
10250 OO.ui.InputWidget.prototype.getValue = function () {
10251 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
10252 // it, and we won't know unless they're kind enough to trigger a 'change' event.
10253 var value = this.$input.val();
10254 if ( this.value !== value ) {
10255 this.setValue( value );
10256 }
10257 return this.value;
10258 };
10259
10260 /**
10261 * Sets the direction of the current input, either RTL or LTR
10262 *
10263 * @param {boolean} isRTL
10264 */
10265 OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
10266 this.$input.prop( 'dir', isRTL ? 'rtl' : 'ltr' );
10267 };
10268
10269 /**
10270 * Set the value of the input.
10271 *
10272 * @param {string} value New value
10273 * @fires change
10274 * @chainable
10275 */
10276 OO.ui.InputWidget.prototype.setValue = function ( value ) {
10277 value = this.cleanUpValue( value );
10278 // Update the DOM if it has changed. Note that with cleanUpValue, it
10279 // is possible for the DOM value to change without this.value changing.
10280 if ( this.$input.val() !== value ) {
10281 this.$input.val( value );
10282 }
10283 if ( this.value !== value ) {
10284 this.value = value;
10285 this.emit( 'change', this.value );
10286 }
10287 return this;
10288 };
10289
10290 /**
10291 * Clean up incoming value.
10292 *
10293 * Ensures value is a string, and converts undefined and null to empty string.
10294 *
10295 * @private
10296 * @param {string} value Original value
10297 * @return {string} Cleaned up value
10298 */
10299 OO.ui.InputWidget.prototype.cleanUpValue = function ( value ) {
10300 if ( value === undefined || value === null ) {
10301 return '';
10302 } else if ( this.inputFilter ) {
10303 return this.inputFilter( String( value ) );
10304 } else {
10305 return String( value );
10306 }
10307 };
10308
10309 /**
10310 * Simulate the behavior of clicking on a label bound to this input.
10311 */
10312 OO.ui.InputWidget.prototype.simulateLabelClick = function () {
10313 if ( !this.isDisabled() ) {
10314 if ( this.$input.is( ':checkbox,:radio' ) ) {
10315 this.$input.click();
10316 } else if ( this.$input.is( ':input' ) ) {
10317 this.$input[ 0 ].focus();
10318 }
10319 }
10320 };
10321
10322 /**
10323 * @inheritdoc
10324 */
10325 OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
10326 OO.ui.InputWidget.super.prototype.setDisabled.call( this, state );
10327 if ( this.$input ) {
10328 this.$input.prop( 'disabled', this.isDisabled() );
10329 }
10330 return this;
10331 };
10332
10333 /**
10334 * Focus the input.
10335 *
10336 * @chainable
10337 */
10338 OO.ui.InputWidget.prototype.focus = function () {
10339 this.$input[ 0 ].focus();
10340 return this;
10341 };
10342
10343 /**
10344 * Blur the input.
10345 *
10346 * @chainable
10347 */
10348 OO.ui.InputWidget.prototype.blur = function () {
10349 this.$input[ 0 ].blur();
10350 return this;
10351 };
10352
10353 /**
10354 * A button that is an input widget. Intended to be used within a OO.ui.FormLayout.
10355 *
10356 * @class
10357 * @extends OO.ui.InputWidget
10358 * @mixins OO.ui.ButtonElement
10359 * @mixins OO.ui.IconElement
10360 * @mixins OO.ui.IndicatorElement
10361 * @mixins OO.ui.LabelElement
10362 * @mixins OO.ui.TitledElement
10363 * @mixins OO.ui.FlaggedElement
10364 *
10365 * @constructor
10366 * @param {Object} [config] Configuration options
10367 * @cfg {string} [type='button'] HTML tag `type` attribute, may be 'button', 'submit' or 'reset'
10368 * @cfg {boolean} [useInputTag=false] Whether to use `<input/>` rather than `<button/>`. Only useful
10369 * if you need IE 6 support in a form with multiple buttons. If you use this option, icons and
10370 * indicators will not be displayed, it won't be possible to have a non-plaintext label, and it
10371 * won't be possible to set a value (which will internally become identical to the label).
10372 */
10373 OO.ui.ButtonInputWidget = function OoUiButtonInputWidget( config ) {
10374 // Configuration initialization
10375 config = $.extend( { type: 'button', useInputTag: false }, config );
10376
10377 // Properties (must be set before parent constructor, which calls #setValue)
10378 this.useInputTag = config.useInputTag;
10379
10380 // Parent constructor
10381 OO.ui.ButtonInputWidget.super.call( this, config );
10382
10383 // Mixin constructors
10384 OO.ui.ButtonElement.call( this, $.extend( {}, config, { $button: this.$input } ) );
10385 OO.ui.IconElement.call( this, config );
10386 OO.ui.IndicatorElement.call( this, config );
10387 OO.ui.LabelElement.call( this, config );
10388 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
10389 OO.ui.FlaggedElement.call( this, config );
10390
10391 // Initialization
10392 if ( !config.useInputTag ) {
10393 this.$input.append( this.$icon, this.$label, this.$indicator );
10394 }
10395 this.$element.addClass( 'oo-ui-buttonInputWidget' );
10396 };
10397
10398 /* Setup */
10399
10400 OO.inheritClass( OO.ui.ButtonInputWidget, OO.ui.InputWidget );
10401 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.ButtonElement );
10402 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.IconElement );
10403 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.IndicatorElement );
10404 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.LabelElement );
10405 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.TitledElement );
10406 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.FlaggedElement );
10407
10408 /* Methods */
10409
10410 /**
10411 * @inheritdoc
10412 * @private
10413 */
10414 OO.ui.ButtonInputWidget.prototype.getInputElement = function ( config ) {
10415 var html = '<' + ( config.useInputTag ? 'input' : 'button' ) + ' type="' + config.type + '">';
10416 return $( html );
10417 };
10418
10419 /**
10420 * Set label value.
10421 *
10422 * Overridden to support setting the 'value' of `<input/>` elements.
10423 *
10424 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
10425 * text; or null for no label
10426 * @chainable
10427 */
10428 OO.ui.ButtonInputWidget.prototype.setLabel = function ( label ) {
10429 OO.ui.LabelElement.prototype.setLabel.call( this, label );
10430
10431 if ( this.useInputTag ) {
10432 if ( typeof label === 'function' ) {
10433 label = OO.ui.resolveMsg( label );
10434 }
10435 if ( label instanceof jQuery ) {
10436 label = label.text();
10437 }
10438 if ( !label ) {
10439 label = '';
10440 }
10441 this.$input.val( label );
10442 }
10443
10444 return this;
10445 };
10446
10447 /**
10448 * Set the value of the input.
10449 *
10450 * Overridden to disable for `<input/>` elements, which have value identical to the label.
10451 *
10452 * @param {string} value New value
10453 * @chainable
10454 */
10455 OO.ui.ButtonInputWidget.prototype.setValue = function ( value ) {
10456 if ( !this.useInputTag ) {
10457 OO.ui.ButtonInputWidget.super.prototype.setValue.call( this, value );
10458 }
10459 return this;
10460 };
10461
10462 /**
10463 * Checkbox input widget.
10464 *
10465 * @class
10466 * @extends OO.ui.InputWidget
10467 *
10468 * @constructor
10469 * @param {Object} [config] Configuration options
10470 * @cfg {boolean} [selected=false] Whether the checkbox is initially selected
10471 */
10472 OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
10473 // Configuration initialization
10474 config = config || {};
10475
10476 // Parent constructor
10477 OO.ui.CheckboxInputWidget.super.call( this, config );
10478
10479 // Initialization
10480 this.$element.addClass( 'oo-ui-checkboxInputWidget' );
10481 this.setSelected( config.selected !== undefined ? config.selected : false );
10482 };
10483
10484 /* Setup */
10485
10486 OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
10487
10488 /* Methods */
10489
10490 /**
10491 * @inheritdoc
10492 * @private
10493 */
10494 OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
10495 return $( '<input type="checkbox" />' );
10496 };
10497
10498 /**
10499 * @inheritdoc
10500 */
10501 OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
10502 var widget = this;
10503 if ( !this.isDisabled() ) {
10504 // Allow the stack to clear so the value will be updated
10505 setTimeout( function () {
10506 widget.setSelected( widget.$input.prop( 'checked' ) );
10507 } );
10508 }
10509 };
10510
10511 /**
10512 * Set selection state of this checkbox.
10513 *
10514 * @param {boolean} state Whether the checkbox is selected
10515 * @chainable
10516 */
10517 OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state ) {
10518 state = !!state;
10519 if ( this.selected !== state ) {
10520 this.selected = state;
10521 this.$input.prop( 'checked', this.selected );
10522 this.emit( 'change', this.selected );
10523 }
10524 return this;
10525 };
10526
10527 /**
10528 * Check if this checkbox is selected.
10529 *
10530 * @return {boolean} Checkbox is selected
10531 */
10532 OO.ui.CheckboxInputWidget.prototype.isSelected = function () {
10533 // Resynchronize our internal data with DOM data. Other scripts executing on the page can modify
10534 // it, and we won't know unless they're kind enough to trigger a 'change' event.
10535 var selected = this.$input.prop( 'checked' );
10536 if ( this.selected !== selected ) {
10537 this.setSelected( selected );
10538 }
10539 return this.selected;
10540 };
10541
10542 /**
10543 * A OO.ui.DropdownWidget synchronized with a `<input type=hidden>` for form submission. Intended to
10544 * be used within a OO.ui.FormLayout.
10545 *
10546 * @class
10547 * @extends OO.ui.InputWidget
10548 *
10549 * @constructor
10550 * @param {Object} [config] Configuration options
10551 * @cfg {Object[]} [options=[]] Array of menu options in the format `{ data: …, label: … }`
10552 */
10553 OO.ui.DropdownInputWidget = function OoUiDropdownInputWidget( config ) {
10554 // Configuration initialization
10555 config = config || {};
10556
10557 // Properties (must be done before parent constructor which calls #setDisabled)
10558 this.dropdownWidget = new OO.ui.DropdownWidget();
10559
10560 // Parent constructor
10561 OO.ui.DropdownInputWidget.super.call( this, config );
10562
10563 // Events
10564 this.dropdownWidget.getMenu().connect( this, { select: 'onMenuSelect' } );
10565
10566 // Initialization
10567 this.setOptions( config.options || [] );
10568 this.$element
10569 .addClass( 'oo-ui-dropdownInputWidget' )
10570 .append( this.dropdownWidget.$element );
10571 };
10572
10573 /* Setup */
10574
10575 OO.inheritClass( OO.ui.DropdownInputWidget, OO.ui.InputWidget );
10576
10577 /* Methods */
10578
10579 /**
10580 * @inheritdoc
10581 * @private
10582 */
10583 OO.ui.DropdownInputWidget.prototype.getInputElement = function () {
10584 return $( '<input type="hidden">' );
10585 };
10586
10587 /**
10588 * Handles menu select events.
10589 *
10590 * @param {OO.ui.MenuOptionWidget} item Selected menu item
10591 */
10592 OO.ui.DropdownInputWidget.prototype.onMenuSelect = function ( item ) {
10593 this.setValue( item.getData() );
10594 };
10595
10596 /**
10597 * @inheritdoc
10598 */
10599 OO.ui.DropdownInputWidget.prototype.setValue = function ( value ) {
10600 var item = this.dropdownWidget.getMenu().getItemFromData( value );
10601 if ( item ) {
10602 this.dropdownWidget.getMenu().selectItem( item );
10603 }
10604 OO.ui.DropdownInputWidget.super.prototype.setValue.call( this, value );
10605 return this;
10606 };
10607
10608 /**
10609 * @inheritdoc
10610 */
10611 OO.ui.DropdownInputWidget.prototype.setDisabled = function ( state ) {
10612 this.dropdownWidget.setDisabled( state );
10613 OO.ui.DropdownInputWidget.super.prototype.setDisabled.call( this, state );
10614 return this;
10615 };
10616
10617 /**
10618 * Set the options available for this input.
10619 *
10620 * @param {Object[]} options Array of menu options in the format `{ data: …, label: … }`
10621 * @chainable
10622 */
10623 OO.ui.DropdownInputWidget.prototype.setOptions = function ( options ) {
10624 var value = this.getValue();
10625
10626 // Rebuild the dropdown menu
10627 this.dropdownWidget.getMenu()
10628 .clearItems()
10629 .addItems( options.map( function ( opt ) {
10630 return new OO.ui.MenuOptionWidget( {
10631 data: opt.data,
10632 label: opt.label !== undefined ? opt.label : opt.data
10633 } );
10634 } ) );
10635
10636 // Restore the previous value, or reset to something sensible
10637 if ( this.dropdownWidget.getMenu().getItemFromData( value ) ) {
10638 // Previous value is still available, ensure consistency with the dropdown
10639 this.setValue( value );
10640 } else {
10641 // No longer valid, reset
10642 if ( options.length ) {
10643 this.setValue( options[ 0 ].data );
10644 }
10645 }
10646
10647 return this;
10648 };
10649
10650 /**
10651 * @inheritdoc
10652 */
10653 OO.ui.DropdownInputWidget.prototype.focus = function () {
10654 this.dropdownWidget.getMenu().toggle( true );
10655 return this;
10656 };
10657
10658 /**
10659 * @inheritdoc
10660 */
10661 OO.ui.DropdownInputWidget.prototype.blur = function () {
10662 this.dropdownWidget.getMenu().toggle( false );
10663 return this;
10664 };
10665
10666 /**
10667 * Radio input widget.
10668 *
10669 * Radio buttons only make sense as a set, and you probably want to use the OO.ui.RadioSelectWidget
10670 * class instead of using this class directly.
10671 *
10672 * @class
10673 * @extends OO.ui.InputWidget
10674 *
10675 * @constructor
10676 * @param {Object} [config] Configuration options
10677 * @cfg {boolean} [selected=false] Whether the radio button is initially selected
10678 */
10679 OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) {
10680 // Configuration initialization
10681 config = config || {};
10682
10683 // Parent constructor
10684 OO.ui.RadioInputWidget.super.call( this, config );
10685
10686 // Initialization
10687 this.$element.addClass( 'oo-ui-radioInputWidget' );
10688 this.setSelected( config.selected !== undefined ? config.selected : false );
10689 };
10690
10691 /* Setup */
10692
10693 OO.inheritClass( OO.ui.RadioInputWidget, OO.ui.InputWidget );
10694
10695 /* Methods */
10696
10697 /**
10698 * @inheritdoc
10699 * @private
10700 */
10701 OO.ui.RadioInputWidget.prototype.getInputElement = function () {
10702 return $( '<input type="radio" />' );
10703 };
10704
10705 /**
10706 * @inheritdoc
10707 */
10708 OO.ui.RadioInputWidget.prototype.onEdit = function () {
10709 // RadioInputWidget doesn't track its state.
10710 };
10711
10712 /**
10713 * Set selection state of this radio button.
10714 *
10715 * @param {boolean} state Whether the button is selected
10716 * @chainable
10717 */
10718 OO.ui.RadioInputWidget.prototype.setSelected = function ( state ) {
10719 // RadioInputWidget doesn't track its state.
10720 this.$input.prop( 'checked', state );
10721 return this;
10722 };
10723
10724 /**
10725 * Check if this radio button is selected.
10726 *
10727 * @return {boolean} Radio is selected
10728 */
10729 OO.ui.RadioInputWidget.prototype.isSelected = function () {
10730 return this.$input.prop( 'checked' );
10731 };
10732
10733 /**
10734 * Input widget with a text field.
10735 *
10736 * @class
10737 * @extends OO.ui.InputWidget
10738 * @mixins OO.ui.IconElement
10739 * @mixins OO.ui.IndicatorElement
10740 * @mixins OO.ui.PendingElement
10741 *
10742 * @constructor
10743 * @param {Object} [config] Configuration options
10744 * @cfg {string} [type='text'] HTML tag `type` attribute
10745 * @cfg {string} [placeholder] Placeholder text
10746 * @cfg {boolean} [autofocus=false] Ask the browser to focus this widget, using the 'autofocus' HTML
10747 * attribute
10748 * @cfg {boolean} [readOnly=false] Prevent changes
10749 * @cfg {number} [maxLength] Maximum allowed number of characters to input
10750 * @cfg {boolean} [multiline=false] Allow multiple lines of text
10751 * @cfg {boolean} [autosize=false] Automatically resize to fit content
10752 * @cfg {boolean} [maxRows=10] Maximum number of rows to make visible when autosizing
10753 * @cfg {string} [labelPosition='after'] Label position, 'before' or 'after'
10754 * @cfg {boolean} [required=false] Mark the field as required
10755 * @cfg {RegExp|string} [validate] Regular expression to validate against (or symbolic name referencing
10756 * one, see #static-validationPatterns)
10757 */
10758 OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
10759 // Configuration initialization
10760 config = $.extend( {
10761 type: 'text',
10762 labelPosition: 'after',
10763 maxRows: 10
10764 }, config );
10765
10766 // Parent constructor
10767 OO.ui.TextInputWidget.super.call( this, config );
10768
10769 // Mixin constructors
10770 OO.ui.IconElement.call( this, config );
10771 OO.ui.IndicatorElement.call( this, config );
10772 OO.ui.PendingElement.call( this, config );
10773 OO.ui.LabelElement.call( this, config );
10774
10775 // Properties
10776 this.readOnly = false;
10777 this.multiline = !!config.multiline;
10778 this.autosize = !!config.autosize;
10779 this.maxRows = config.maxRows;
10780 this.validate = null;
10781
10782 // Clone for resizing
10783 if ( this.autosize ) {
10784 this.$clone = this.$input
10785 .clone()
10786 .insertAfter( this.$input )
10787 .attr( 'aria-hidden', 'true' )
10788 .addClass( 'oo-ui-element-hidden' );
10789 }
10790
10791 this.setValidation( config.validate );
10792 this.setPosition( config.labelPosition );
10793
10794 // Events
10795 this.$input.on( {
10796 keypress: this.onKeyPress.bind( this ),
10797 blur: this.setValidityFlag.bind( this )
10798 } );
10799 this.$element.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach.bind( this ) );
10800 this.$icon.on( 'mousedown', this.onIconMouseDown.bind( this ) );
10801 this.$indicator.on( 'mousedown', this.onIndicatorMouseDown.bind( this ) );
10802 this.on( 'labelChange', this.updatePosition.bind( this ) );
10803
10804 // Initialization
10805 this.$element
10806 .addClass( 'oo-ui-textInputWidget' )
10807 .append( this.$icon, this.$indicator, this.$label );
10808 this.setReadOnly( !!config.readOnly );
10809 if ( config.placeholder ) {
10810 this.$input.attr( 'placeholder', config.placeholder );
10811 }
10812 if ( config.maxLength ) {
10813 this.$input.attr( 'maxlength', config.maxLength );
10814 }
10815 if ( config.autofocus ) {
10816 this.$input.attr( 'autofocus', 'autofocus' );
10817 }
10818 if ( config.required ) {
10819 this.$input.attr( 'required', 'true' );
10820 }
10821 };
10822
10823 /* Setup */
10824
10825 OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
10826 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IconElement );
10827 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IndicatorElement );
10828 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.PendingElement );
10829 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.LabelElement );
10830
10831 /* Static properties */
10832
10833 OO.ui.TextInputWidget.static.validationPatterns = {
10834 'non-empty': /.+/,
10835 integer: /^\d+$/
10836 };
10837
10838 /* Events */
10839
10840 /**
10841 * User presses enter inside the text box.
10842 *
10843 * Not called if input is multiline.
10844 *
10845 * @event enter
10846 */
10847
10848 /**
10849 * User clicks the icon.
10850 *
10851 * @event icon
10852 */
10853
10854 /**
10855 * User clicks the indicator.
10856 *
10857 * @event indicator
10858 */
10859
10860 /* Methods */
10861
10862 /**
10863 * Handle icon mouse down events.
10864 *
10865 * @param {jQuery.Event} e Mouse down event
10866 * @fires icon
10867 */
10868 OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
10869 if ( e.which === 1 ) {
10870 this.$input[ 0 ].focus();
10871 this.emit( 'icon' );
10872 return false;
10873 }
10874 };
10875
10876 /**
10877 * Handle indicator mouse down events.
10878 *
10879 * @param {jQuery.Event} e Mouse down event
10880 * @fires indicator
10881 */
10882 OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
10883 if ( e.which === 1 ) {
10884 this.$input[ 0 ].focus();
10885 this.emit( 'indicator' );
10886 return false;
10887 }
10888 };
10889
10890 /**
10891 * Handle key press events.
10892 *
10893 * @param {jQuery.Event} e Key press event
10894 * @fires enter If enter key is pressed and input is not multiline
10895 */
10896 OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
10897 if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) {
10898 this.emit( 'enter', e );
10899 }
10900 };
10901
10902 /**
10903 * Handle element attach events.
10904 *
10905 * @param {jQuery.Event} e Element attach event
10906 */
10907 OO.ui.TextInputWidget.prototype.onElementAttach = function () {
10908 // Any previously calculated size is now probably invalid if we reattached elsewhere
10909 this.valCache = null;
10910 this.adjustSize();
10911 this.positionLabel();
10912 };
10913
10914 /**
10915 * @inheritdoc
10916 */
10917 OO.ui.TextInputWidget.prototype.onEdit = function () {
10918 this.adjustSize();
10919
10920 // Parent method
10921 return OO.ui.TextInputWidget.super.prototype.onEdit.call( this );
10922 };
10923
10924 /**
10925 * @inheritdoc
10926 */
10927 OO.ui.TextInputWidget.prototype.setValue = function ( value ) {
10928 // Parent method
10929 OO.ui.TextInputWidget.super.prototype.setValue.call( this, value );
10930
10931 this.setValidityFlag();
10932 this.adjustSize();
10933 return this;
10934 };
10935
10936 /**
10937 * Check if the widget is read-only.
10938 *
10939 * @return {boolean}
10940 */
10941 OO.ui.TextInputWidget.prototype.isReadOnly = function () {
10942 return this.readOnly;
10943 };
10944
10945 /**
10946 * Set the read-only state of the widget.
10947 *
10948 * This should probably change the widget's appearance and prevent it from being used.
10949 *
10950 * @param {boolean} state Make input read-only
10951 * @chainable
10952 */
10953 OO.ui.TextInputWidget.prototype.setReadOnly = function ( state ) {
10954 this.readOnly = !!state;
10955 this.$input.prop( 'readOnly', this.readOnly );
10956 return this;
10957 };
10958
10959 /**
10960 * Automatically adjust the size of the text input.
10961 *
10962 * This only affects multi-line inputs that are auto-sized.
10963 *
10964 * @chainable
10965 */
10966 OO.ui.TextInputWidget.prototype.adjustSize = function () {
10967 var scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError, idealHeight;
10968
10969 if ( this.multiline && this.autosize && this.$input.val() !== this.valCache ) {
10970 this.$clone
10971 .val( this.$input.val() )
10972 .attr( 'rows', '' )
10973 // Set inline height property to 0 to measure scroll height
10974 .css( 'height', 0 );
10975
10976 this.$clone.removeClass( 'oo-ui-element-hidden' );
10977
10978 this.valCache = this.$input.val();
10979
10980 scrollHeight = this.$clone[ 0 ].scrollHeight;
10981
10982 // Remove inline height property to measure natural heights
10983 this.$clone.css( 'height', '' );
10984 innerHeight = this.$clone.innerHeight();
10985 outerHeight = this.$clone.outerHeight();
10986
10987 // Measure max rows height
10988 this.$clone
10989 .attr( 'rows', this.maxRows )
10990 .css( 'height', 'auto' )
10991 .val( '' );
10992 maxInnerHeight = this.$clone.innerHeight();
10993
10994 // Difference between reported innerHeight and scrollHeight with no scrollbars present
10995 // Equals 1 on Blink-based browsers and 0 everywhere else
10996 measurementError = maxInnerHeight - this.$clone[ 0 ].scrollHeight;
10997 idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
10998
10999 this.$clone.addClass( 'oo-ui-element-hidden' );
11000
11001 // Only apply inline height when expansion beyond natural height is needed
11002 if ( idealHeight > innerHeight ) {
11003 // Use the difference between the inner and outer height as a buffer
11004 this.$input.css( 'height', idealHeight + ( outerHeight - innerHeight ) );
11005 } else {
11006 this.$input.css( 'height', '' );
11007 }
11008 }
11009 return this;
11010 };
11011
11012 /**
11013 * @inheritdoc
11014 * @private
11015 */
11016 OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
11017 return config.multiline ? $( '<textarea>' ) : $( '<input type="' + config.type + '" />' );
11018 };
11019
11020 /**
11021 * Check if input supports multiple lines.
11022 *
11023 * @return {boolean}
11024 */
11025 OO.ui.TextInputWidget.prototype.isMultiline = function () {
11026 return !!this.multiline;
11027 };
11028
11029 /**
11030 * Check if input automatically adjusts its size.
11031 *
11032 * @return {boolean}
11033 */
11034 OO.ui.TextInputWidget.prototype.isAutosizing = function () {
11035 return !!this.autosize;
11036 };
11037
11038 /**
11039 * Select the contents of the input.
11040 *
11041 * @chainable
11042 */
11043 OO.ui.TextInputWidget.prototype.select = function () {
11044 this.$input.select();
11045 return this;
11046 };
11047
11048 /**
11049 * Sets the validation pattern to use.
11050 * @param {RegExp|string|null} validate Regular expression (or symbolic name referencing
11051 * one, see #static-validationPatterns)
11052 */
11053 OO.ui.TextInputWidget.prototype.setValidation = function ( validate ) {
11054 if ( validate instanceof RegExp ) {
11055 this.validate = validate;
11056 } else {
11057 this.validate = this.constructor.static.validationPatterns[ validate ] || /.*/;
11058 }
11059 };
11060
11061 /**
11062 * Sets the 'invalid' flag appropriately.
11063 */
11064 OO.ui.TextInputWidget.prototype.setValidityFlag = function () {
11065 var widget = this;
11066 this.isValid().done( function ( valid ) {
11067 widget.setFlags( { invalid: !valid } );
11068 } );
11069 };
11070
11071 /**
11072 * Returns whether or not the current value is considered valid, according to the
11073 * supplied validation pattern.
11074 *
11075 * @return {jQuery.Deferred}
11076 */
11077 OO.ui.TextInputWidget.prototype.isValid = function () {
11078 return $.Deferred().resolve( !!this.getValue().match( this.validate ) ).promise();
11079 };
11080
11081 /**
11082 * Set the position of the inline label.
11083 *
11084 * @param {string} labelPosition Label position, 'before' or 'after'
11085 * @chainable
11086 */
11087 OO.ui.TextInputWidget.prototype.setPosition = function ( labelPosition ) {
11088 this.labelPosition = labelPosition;
11089 this.updatePosition();
11090 return this;
11091 };
11092
11093 /**
11094 * Update the position of the inline label.
11095 *
11096 * @chainable
11097 */
11098 OO.ui.TextInputWidget.prototype.updatePosition = function () {
11099 var after = this.labelPosition === 'after';
11100
11101 this.$element
11102 .toggleClass( 'oo-ui-textInputWidget-labelPosition-after', this.label && after )
11103 .toggleClass( 'oo-ui-textInputWidget-labelPosition-before', this.label && !after );
11104
11105 if ( this.label ) {
11106 this.positionLabel();
11107 }
11108
11109 return this;
11110 };
11111
11112 /**
11113 * Position the label by setting the correct padding on the input.
11114 *
11115 * @chainable
11116 */
11117 OO.ui.TextInputWidget.prototype.positionLabel = function () {
11118 // Clear old values
11119 this.$input
11120 // Clear old values if present
11121 .css( {
11122 'padding-right': '',
11123 'padding-left': ''
11124 } );
11125
11126 if ( !this.$label.text() ) {
11127 return;
11128 }
11129
11130 var after = this.labelPosition === 'after',
11131 rtl = this.$element.css( 'direction' ) === 'rtl',
11132 property = after === rtl ? 'padding-left' : 'padding-right';
11133
11134 this.$input.css( property, this.$label.outerWidth() );
11135
11136 return this;
11137 };
11138
11139 /**
11140 * Text input with a menu of optional values.
11141 *
11142 * @class
11143 * @extends OO.ui.Widget
11144 * @mixins OO.ui.TabIndexedElement
11145 *
11146 * @constructor
11147 * @param {Object} [config] Configuration options
11148 * @cfg {Object} [menu] Configuration options to pass to menu widget
11149 * @cfg {Object} [input] Configuration options to pass to input widget
11150 * @cfg {jQuery} [$overlay] Overlay layer; defaults to relative positioning
11151 */
11152 OO.ui.ComboBoxWidget = function OoUiComboBoxWidget( config ) {
11153 // Configuration initialization
11154 config = config || {};
11155
11156 // Parent constructor
11157 OO.ui.ComboBoxWidget.super.call( this, config );
11158
11159 // Properties (must be set before TabIndexedElement constructor call)
11160 this.$indicator = this.$( '<span>' );
11161
11162 // Mixin constructors
11163 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$indicator } ) );
11164
11165 // Properties
11166 this.$overlay = config.$overlay || this.$element;
11167 this.input = new OO.ui.TextInputWidget( $.extend(
11168 {
11169 indicator: 'down',
11170 $indicator: this.$indicator,
11171 disabled: this.isDisabled()
11172 },
11173 config.input
11174 ) );
11175 this.menu = new OO.ui.TextInputMenuSelectWidget( this.input, $.extend(
11176 {
11177 widget: this,
11178 input: this.input,
11179 disabled: this.isDisabled()
11180 },
11181 config.menu
11182 ) );
11183
11184 // Events
11185 this.$indicator.on( {
11186 click: this.onClick.bind( this ),
11187 keypress: this.onKeyPress.bind( this )
11188 } );
11189 this.input.connect( this, {
11190 change: 'onInputChange',
11191 enter: 'onInputEnter'
11192 } );
11193 this.menu.connect( this, {
11194 choose: 'onMenuChoose',
11195 add: 'onMenuItemsChange',
11196 remove: 'onMenuItemsChange'
11197 } );
11198
11199 // Initialization
11200 this.$element.addClass( 'oo-ui-comboBoxWidget' ).append( this.input.$element );
11201 this.$overlay.append( this.menu.$element );
11202 this.onMenuItemsChange();
11203 };
11204
11205 /* Setup */
11206
11207 OO.inheritClass( OO.ui.ComboBoxWidget, OO.ui.Widget );
11208 OO.mixinClass( OO.ui.ComboBoxWidget, OO.ui.TabIndexedElement );
11209
11210 /* Methods */
11211
11212 /**
11213 * Get the combobox's menu.
11214 * @return {OO.ui.TextInputMenuSelectWidget} Menu widget
11215 */
11216 OO.ui.ComboBoxWidget.prototype.getMenu = function () {
11217 return this.menu;
11218 };
11219
11220 /**
11221 * Handle input change events.
11222 *
11223 * @param {string} value New value
11224 */
11225 OO.ui.ComboBoxWidget.prototype.onInputChange = function ( value ) {
11226 var match = this.menu.getItemFromData( value );
11227
11228 this.menu.selectItem( match );
11229
11230 if ( !this.isDisabled() ) {
11231 this.menu.toggle( true );
11232 }
11233 };
11234
11235 /**
11236 * Handle mouse click events.
11237 *
11238 * @param {jQuery.Event} e Mouse click event
11239 */
11240 OO.ui.ComboBoxWidget.prototype.onClick = function ( e ) {
11241 if ( !this.isDisabled() && e.which === 1 ) {
11242 this.menu.toggle();
11243 this.input.$input[ 0 ].focus();
11244 }
11245 return false;
11246 };
11247
11248 /**
11249 * Handle key press events.
11250 *
11251 * @param {jQuery.Event} e Key press event
11252 */
11253 OO.ui.ComboBoxWidget.prototype.onKeyPress = function ( e ) {
11254 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
11255 this.menu.toggle();
11256 this.input.$input[ 0 ].focus();
11257 }
11258 return false;
11259 };
11260
11261 /**
11262 * Handle input enter events.
11263 */
11264 OO.ui.ComboBoxWidget.prototype.onInputEnter = function () {
11265 if ( !this.isDisabled() ) {
11266 this.menu.toggle( false );
11267 }
11268 };
11269
11270 /**
11271 * Handle menu choose events.
11272 *
11273 * @param {OO.ui.OptionWidget} item Chosen item
11274 */
11275 OO.ui.ComboBoxWidget.prototype.onMenuChoose = function ( item ) {
11276 if ( item ) {
11277 this.input.setValue( item.getData() );
11278 }
11279 };
11280
11281 /**
11282 * Handle menu item change events.
11283 */
11284 OO.ui.ComboBoxWidget.prototype.onMenuItemsChange = function () {
11285 var match = this.menu.getItemFromData( this.input.getValue() );
11286 this.menu.selectItem( match );
11287 this.$element.toggleClass( 'oo-ui-comboBoxWidget-empty', this.menu.isEmpty() );
11288 };
11289
11290 /**
11291 * @inheritdoc
11292 */
11293 OO.ui.ComboBoxWidget.prototype.setDisabled = function ( disabled ) {
11294 // Parent method
11295 OO.ui.ComboBoxWidget.super.prototype.setDisabled.call( this, disabled );
11296
11297 if ( this.input ) {
11298 this.input.setDisabled( this.isDisabled() );
11299 }
11300 if ( this.menu ) {
11301 this.menu.setDisabled( this.isDisabled() );
11302 }
11303
11304 return this;
11305 };
11306
11307 /**
11308 * Label widget.
11309 *
11310 * @class
11311 * @extends OO.ui.Widget
11312 * @mixins OO.ui.LabelElement
11313 *
11314 * @constructor
11315 * @param {Object} [config] Configuration options
11316 * @cfg {OO.ui.InputWidget} [input] Input widget this label is for
11317 */
11318 OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
11319 // Configuration initialization
11320 config = config || {};
11321
11322 // Parent constructor
11323 OO.ui.LabelWidget.super.call( this, config );
11324
11325 // Mixin constructors
11326 OO.ui.LabelElement.call( this, $.extend( {}, config, { $label: this.$element } ) );
11327 OO.ui.TitledElement.call( this, config );
11328
11329 // Properties
11330 this.input = config.input;
11331
11332 // Events
11333 if ( this.input instanceof OO.ui.InputWidget ) {
11334 this.$element.on( 'click', this.onClick.bind( this ) );
11335 }
11336
11337 // Initialization
11338 this.$element.addClass( 'oo-ui-labelWidget' );
11339 };
11340
11341 /* Setup */
11342
11343 OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
11344 OO.mixinClass( OO.ui.LabelWidget, OO.ui.LabelElement );
11345 OO.mixinClass( OO.ui.LabelWidget, OO.ui.TitledElement );
11346
11347 /* Static Properties */
11348
11349 OO.ui.LabelWidget.static.tagName = 'span';
11350
11351 /* Methods */
11352
11353 /**
11354 * Handles label mouse click events.
11355 *
11356 * @param {jQuery.Event} e Mouse click event
11357 */
11358 OO.ui.LabelWidget.prototype.onClick = function () {
11359 this.input.simulateLabelClick();
11360 return false;
11361 };
11362
11363 /**
11364 * Generic option widget for use with OO.ui.SelectWidget.
11365 *
11366 * @class
11367 * @extends OO.ui.Widget
11368 * @mixins OO.ui.LabelElement
11369 * @mixins OO.ui.FlaggedElement
11370 *
11371 * @constructor
11372 * @param {Object} [config] Configuration options
11373 */
11374 OO.ui.OptionWidget = function OoUiOptionWidget( config ) {
11375 // Configuration initialization
11376 config = config || {};
11377
11378 // Parent constructor
11379 OO.ui.OptionWidget.super.call( this, config );
11380
11381 // Mixin constructors
11382 OO.ui.ItemWidget.call( this );
11383 OO.ui.LabelElement.call( this, config );
11384 OO.ui.FlaggedElement.call( this, config );
11385
11386 // Properties
11387 this.selected = false;
11388 this.highlighted = false;
11389 this.pressed = false;
11390
11391 // Initialization
11392 this.$element
11393 .data( 'oo-ui-optionWidget', this )
11394 .attr( 'role', 'option' )
11395 .addClass( 'oo-ui-optionWidget' )
11396 .append( this.$label );
11397 };
11398
11399 /* Setup */
11400
11401 OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
11402 OO.mixinClass( OO.ui.OptionWidget, OO.ui.ItemWidget );
11403 OO.mixinClass( OO.ui.OptionWidget, OO.ui.LabelElement );
11404 OO.mixinClass( OO.ui.OptionWidget, OO.ui.FlaggedElement );
11405
11406 /* Static Properties */
11407
11408 OO.ui.OptionWidget.static.selectable = true;
11409
11410 OO.ui.OptionWidget.static.highlightable = true;
11411
11412 OO.ui.OptionWidget.static.pressable = true;
11413
11414 OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
11415
11416 /* Methods */
11417
11418 /**
11419 * Check if option can be selected.
11420 *
11421 * @return {boolean} Item is selectable
11422 */
11423 OO.ui.OptionWidget.prototype.isSelectable = function () {
11424 return this.constructor.static.selectable && !this.isDisabled();
11425 };
11426
11427 /**
11428 * Check if option can be highlighted.
11429 *
11430 * @return {boolean} Item is highlightable
11431 */
11432 OO.ui.OptionWidget.prototype.isHighlightable = function () {
11433 return this.constructor.static.highlightable && !this.isDisabled();
11434 };
11435
11436 /**
11437 * Check if option can be pressed.
11438 *
11439 * @return {boolean} Item is pressable
11440 */
11441 OO.ui.OptionWidget.prototype.isPressable = function () {
11442 return this.constructor.static.pressable && !this.isDisabled();
11443 };
11444
11445 /**
11446 * Check if option is selected.
11447 *
11448 * @return {boolean} Item is selected
11449 */
11450 OO.ui.OptionWidget.prototype.isSelected = function () {
11451 return this.selected;
11452 };
11453
11454 /**
11455 * Check if option is highlighted.
11456 *
11457 * @return {boolean} Item is highlighted
11458 */
11459 OO.ui.OptionWidget.prototype.isHighlighted = function () {
11460 return this.highlighted;
11461 };
11462
11463 /**
11464 * Check if option is pressed.
11465 *
11466 * @return {boolean} Item is pressed
11467 */
11468 OO.ui.OptionWidget.prototype.isPressed = function () {
11469 return this.pressed;
11470 };
11471
11472 /**
11473 * Set selected state.
11474 *
11475 * @param {boolean} [state=false] Select option
11476 * @chainable
11477 */
11478 OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
11479 if ( this.constructor.static.selectable ) {
11480 this.selected = !!state;
11481 this.$element
11482 .toggleClass( 'oo-ui-optionWidget-selected', state )
11483 .attr( 'aria-selected', state.toString() );
11484 if ( state && this.constructor.static.scrollIntoViewOnSelect ) {
11485 this.scrollElementIntoView();
11486 }
11487 this.updateThemeClasses();
11488 }
11489 return this;
11490 };
11491
11492 /**
11493 * Set highlighted state.
11494 *
11495 * @param {boolean} [state=false] Highlight option
11496 * @chainable
11497 */
11498 OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
11499 if ( this.constructor.static.highlightable ) {
11500 this.highlighted = !!state;
11501 this.$element.toggleClass( 'oo-ui-optionWidget-highlighted', state );
11502 this.updateThemeClasses();
11503 }
11504 return this;
11505 };
11506
11507 /**
11508 * Set pressed state.
11509 *
11510 * @param {boolean} [state=false] Press option
11511 * @chainable
11512 */
11513 OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
11514 if ( this.constructor.static.pressable ) {
11515 this.pressed = !!state;
11516 this.$element.toggleClass( 'oo-ui-optionWidget-pressed', state );
11517 this.updateThemeClasses();
11518 }
11519 return this;
11520 };
11521
11522 /**
11523 * Option widget with an option icon and indicator.
11524 *
11525 * Use together with OO.ui.SelectWidget.
11526 *
11527 * @class
11528 * @extends OO.ui.OptionWidget
11529 * @mixins OO.ui.IconElement
11530 * @mixins OO.ui.IndicatorElement
11531 *
11532 * @constructor
11533 * @param {Object} [config] Configuration options
11534 */
11535 OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( config ) {
11536 // Parent constructor
11537 OO.ui.DecoratedOptionWidget.super.call( this, config );
11538
11539 // Mixin constructors
11540 OO.ui.IconElement.call( this, config );
11541 OO.ui.IndicatorElement.call( this, config );
11542
11543 // Initialization
11544 this.$element
11545 .addClass( 'oo-ui-decoratedOptionWidget' )
11546 .prepend( this.$icon )
11547 .append( this.$indicator );
11548 };
11549
11550 /* Setup */
11551
11552 OO.inheritClass( OO.ui.DecoratedOptionWidget, OO.ui.OptionWidget );
11553 OO.mixinClass( OO.ui.OptionWidget, OO.ui.IconElement );
11554 OO.mixinClass( OO.ui.OptionWidget, OO.ui.IndicatorElement );
11555
11556 /**
11557 * Option widget that looks like a button.
11558 *
11559 * Use together with OO.ui.ButtonSelectWidget.
11560 *
11561 * @class
11562 * @extends OO.ui.DecoratedOptionWidget
11563 * @mixins OO.ui.ButtonElement
11564 * @mixins OO.ui.TabIndexedElement
11565 *
11566 * @constructor
11567 * @param {Object} [config] Configuration options
11568 */
11569 OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( config ) {
11570 // Parent constructor
11571 OO.ui.ButtonOptionWidget.super.call( this, config );
11572
11573 // Mixin constructors
11574 OO.ui.ButtonElement.call( this, config );
11575 OO.ui.TabIndexedElement.call( this, $.extend( {}, config, { $tabIndexed: this.$button } ) );
11576
11577 // Initialization
11578 this.$element.addClass( 'oo-ui-buttonOptionWidget' );
11579 this.$button.append( this.$element.contents() );
11580 this.$element.append( this.$button );
11581 };
11582
11583 /* Setup */
11584
11585 OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.DecoratedOptionWidget );
11586 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.ButtonElement );
11587 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.TabIndexedElement );
11588
11589 /* Static Properties */
11590
11591 // Allow button mouse down events to pass through so they can be handled by the parent select widget
11592 OO.ui.ButtonOptionWidget.static.cancelButtonMouseDownEvents = false;
11593
11594 /* Methods */
11595
11596 /**
11597 * @inheritdoc
11598 */
11599 OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) {
11600 OO.ui.ButtonOptionWidget.super.prototype.setSelected.call( this, state );
11601
11602 if ( this.constructor.static.selectable ) {
11603 this.setActive( state );
11604 }
11605
11606 return this;
11607 };
11608
11609 /**
11610 * Option widget that looks like a radio button.
11611 *
11612 * Use together with OO.ui.RadioSelectWidget.
11613 *
11614 * @class
11615 * @extends OO.ui.OptionWidget
11616 *
11617 * @constructor
11618 * @param {Object} [config] Configuration options
11619 */
11620 OO.ui.RadioOptionWidget = function OoUiRadioOptionWidget( config ) {
11621 // Parent constructor
11622 OO.ui.RadioOptionWidget.super.call( this, config );
11623
11624 // Properties
11625 this.radio = new OO.ui.RadioInputWidget( { value: config.data } );
11626
11627 // Initialization
11628 this.$element
11629 .addClass( 'oo-ui-radioOptionWidget' )
11630 .prepend( this.radio.$element );
11631 };
11632
11633 /* Setup */
11634
11635 OO.inheritClass( OO.ui.RadioOptionWidget, OO.ui.OptionWidget );
11636
11637 /* Static Properties */
11638
11639 OO.ui.RadioOptionWidget.static.highlightable = false;
11640
11641 OO.ui.RadioOptionWidget.static.scrollIntoViewOnSelect = true;
11642
11643 OO.ui.RadioOptionWidget.static.pressable = false;
11644
11645 OO.ui.RadioOptionWidget.static.tagName = 'label';
11646
11647 /* Methods */
11648
11649 /**
11650 * @inheritdoc
11651 */
11652 OO.ui.RadioOptionWidget.prototype.setSelected = function ( state ) {
11653 OO.ui.RadioOptionWidget.super.prototype.setSelected.call( this, state );
11654
11655 this.radio.setSelected( state );
11656
11657 return this;
11658 };
11659
11660 /**
11661 * Item of an OO.ui.MenuSelectWidget.
11662 *
11663 * @class
11664 * @extends OO.ui.DecoratedOptionWidget
11665 *
11666 * @constructor
11667 * @param {Object} [config] Configuration options
11668 */
11669 OO.ui.MenuOptionWidget = function OoUiMenuOptionWidget( config ) {
11670 // Configuration initialization
11671 config = $.extend( { icon: 'check' }, config );
11672
11673 // Parent constructor
11674 OO.ui.MenuOptionWidget.super.call( this, config );
11675
11676 // Initialization
11677 this.$element
11678 .attr( 'role', 'menuitem' )
11679 .addClass( 'oo-ui-menuOptionWidget' );
11680 };
11681
11682 /* Setup */
11683
11684 OO.inheritClass( OO.ui.MenuOptionWidget, OO.ui.DecoratedOptionWidget );
11685
11686 /* Static Properties */
11687
11688 OO.ui.MenuOptionWidget.static.scrollIntoViewOnSelect = true;
11689
11690 /**
11691 * Section to group one or more items in a OO.ui.MenuSelectWidget.
11692 *
11693 * @class
11694 * @extends OO.ui.DecoratedOptionWidget
11695 *
11696 * @constructor
11697 * @param {Object} [config] Configuration options
11698 */
11699 OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( config ) {
11700 // Parent constructor
11701 OO.ui.MenuSectionOptionWidget.super.call( this, config );
11702
11703 // Initialization
11704 this.$element.addClass( 'oo-ui-menuSectionOptionWidget' );
11705 };
11706
11707 /* Setup */
11708
11709 OO.inheritClass( OO.ui.MenuSectionOptionWidget, OO.ui.DecoratedOptionWidget );
11710
11711 /* Static Properties */
11712
11713 OO.ui.MenuSectionOptionWidget.static.selectable = false;
11714
11715 OO.ui.MenuSectionOptionWidget.static.highlightable = false;
11716
11717 /**
11718 * Items for an OO.ui.OutlineSelectWidget.
11719 *
11720 * @class
11721 * @extends OO.ui.DecoratedOptionWidget
11722 *
11723 * @constructor
11724 * @param {Object} [config] Configuration options
11725 * @cfg {number} [level] Indentation level
11726 * @cfg {boolean} [movable] Allow modification from outline controls
11727 */
11728 OO.ui.OutlineOptionWidget = function OoUiOutlineOptionWidget( config ) {
11729 // Configuration initialization
11730 config = config || {};
11731
11732 // Parent constructor
11733 OO.ui.OutlineOptionWidget.super.call( this, config );
11734
11735 // Properties
11736 this.level = 0;
11737 this.movable = !!config.movable;
11738 this.removable = !!config.removable;
11739
11740 // Initialization
11741 this.$element.addClass( 'oo-ui-outlineOptionWidget' );
11742 this.setLevel( config.level );
11743 };
11744
11745 /* Setup */
11746
11747 OO.inheritClass( OO.ui.OutlineOptionWidget, OO.ui.DecoratedOptionWidget );
11748
11749 /* Static Properties */
11750
11751 OO.ui.OutlineOptionWidget.static.highlightable = false;
11752
11753 OO.ui.OutlineOptionWidget.static.scrollIntoViewOnSelect = true;
11754
11755 OO.ui.OutlineOptionWidget.static.levelClass = 'oo-ui-outlineOptionWidget-level-';
11756
11757 OO.ui.OutlineOptionWidget.static.levels = 3;
11758
11759 /* Methods */
11760
11761 /**
11762 * Check if item is movable.
11763 *
11764 * Movability is used by outline controls.
11765 *
11766 * @return {boolean} Item is movable
11767 */
11768 OO.ui.OutlineOptionWidget.prototype.isMovable = function () {
11769 return this.movable;
11770 };
11771
11772 /**
11773 * Check if item is removable.
11774 *
11775 * Removability is used by outline controls.
11776 *
11777 * @return {boolean} Item is removable
11778 */
11779 OO.ui.OutlineOptionWidget.prototype.isRemovable = function () {
11780 return this.removable;
11781 };
11782
11783 /**
11784 * Get indentation level.
11785 *
11786 * @return {number} Indentation level
11787 */
11788 OO.ui.OutlineOptionWidget.prototype.getLevel = function () {
11789 return this.level;
11790 };
11791
11792 /**
11793 * Set movability.
11794 *
11795 * Movability is used by outline controls.
11796 *
11797 * @param {boolean} movable Item is movable
11798 * @chainable
11799 */
11800 OO.ui.OutlineOptionWidget.prototype.setMovable = function ( movable ) {
11801 this.movable = !!movable;
11802 this.updateThemeClasses();
11803 return this;
11804 };
11805
11806 /**
11807 * Set removability.
11808 *
11809 * Removability is used by outline controls.
11810 *
11811 * @param {boolean} movable Item is removable
11812 * @chainable
11813 */
11814 OO.ui.OutlineOptionWidget.prototype.setRemovable = function ( removable ) {
11815 this.removable = !!removable;
11816 this.updateThemeClasses();
11817 return this;
11818 };
11819
11820 /**
11821 * Set indentation level.
11822 *
11823 * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
11824 * @chainable
11825 */
11826 OO.ui.OutlineOptionWidget.prototype.setLevel = function ( level ) {
11827 var levels = this.constructor.static.levels,
11828 levelClass = this.constructor.static.levelClass,
11829 i = levels;
11830
11831 this.level = level ? Math.max( 0, Math.min( levels - 1, level ) ) : 0;
11832 while ( i-- ) {
11833 if ( this.level === i ) {
11834 this.$element.addClass( levelClass + i );
11835 } else {
11836 this.$element.removeClass( levelClass + i );
11837 }
11838 }
11839 this.updateThemeClasses();
11840
11841 return this;
11842 };
11843
11844 /**
11845 * Container for content that is overlaid and positioned absolutely.
11846 *
11847 * @class
11848 * @extends OO.ui.Widget
11849 * @mixins OO.ui.LabelElement
11850 *
11851 * @constructor
11852 * @param {Object} [config] Configuration options
11853 * @cfg {number} [width=320] Width of popup in pixels
11854 * @cfg {number} [height] Height of popup, omit to use automatic height
11855 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
11856 * @cfg {string} [align='center'] Alignment of popup to origin
11857 * @cfg {jQuery} [$container] Container to prevent popup from rendering outside of
11858 * @cfg {number} [containerPadding=10] How much padding to keep between popup and container
11859 * @cfg {jQuery} [$content] Content to append to the popup's body
11860 * @cfg {boolean} [autoClose=false] Popup auto-closes when it loses focus
11861 * @cfg {jQuery} [$autoCloseIgnore] Elements to not auto close when clicked
11862 * @cfg {boolean} [head] Show label and close button at the top
11863 * @cfg {boolean} [padded] Add padding to the body
11864 */
11865 OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
11866 // Configuration initialization
11867 config = config || {};
11868
11869 // Parent constructor
11870 OO.ui.PopupWidget.super.call( this, config );
11871
11872 // Properties (must be set before ClippableElement constructor call)
11873 this.$body = $( '<div>' );
11874
11875 // Mixin constructors
11876 OO.ui.LabelElement.call( this, config );
11877 OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$body } ) );
11878
11879 // Properties
11880 this.$popup = $( '<div>' );
11881 this.$head = $( '<div>' );
11882 this.$anchor = $( '<div>' );
11883 // If undefined, will be computed lazily in updateDimensions()
11884 this.$container = config.$container;
11885 this.containerPadding = config.containerPadding !== undefined ? config.containerPadding : 10;
11886 this.autoClose = !!config.autoClose;
11887 this.$autoCloseIgnore = config.$autoCloseIgnore;
11888 this.transitionTimeout = null;
11889 this.anchor = null;
11890 this.width = config.width !== undefined ? config.width : 320;
11891 this.height = config.height !== undefined ? config.height : null;
11892 this.align = config.align || 'center';
11893 this.closeButton = new OO.ui.ButtonWidget( { framed: false, icon: 'close' } );
11894 this.onMouseDownHandler = this.onMouseDown.bind( this );
11895
11896 // Events
11897 this.closeButton.connect( this, { click: 'onCloseButtonClick' } );
11898
11899 // Initialization
11900 this.toggleAnchor( config.anchor === undefined || config.anchor );
11901 this.$body.addClass( 'oo-ui-popupWidget-body' );
11902 this.$anchor.addClass( 'oo-ui-popupWidget-anchor' );
11903 this.$head
11904 .addClass( 'oo-ui-popupWidget-head' )
11905 .append( this.$label, this.closeButton.$element );
11906 if ( !config.head ) {
11907 this.$head.addClass( 'oo-ui-element-hidden' );
11908 }
11909 this.$popup
11910 .addClass( 'oo-ui-popupWidget-popup' )
11911 .append( this.$head, this.$body );
11912 this.$element
11913 .addClass( 'oo-ui-popupWidget' )
11914 .append( this.$popup, this.$anchor );
11915 // Move content, which was added to #$element by OO.ui.Widget, to the body
11916 if ( config.$content instanceof jQuery ) {
11917 this.$body.append( config.$content );
11918 }
11919 if ( config.padded ) {
11920 this.$body.addClass( 'oo-ui-popupWidget-body-padded' );
11921 }
11922
11923 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
11924 // that reference properties not initialized at that time of parent class construction
11925 // TODO: Find a better way to handle post-constructor setup
11926 this.visible = false;
11927 this.$element.addClass( 'oo-ui-element-hidden' );
11928 };
11929
11930 /* Setup */
11931
11932 OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
11933 OO.mixinClass( OO.ui.PopupWidget, OO.ui.LabelElement );
11934 OO.mixinClass( OO.ui.PopupWidget, OO.ui.ClippableElement );
11935
11936 /* Methods */
11937
11938 /**
11939 * Handles mouse down events.
11940 *
11941 * @param {jQuery.Event} e Mouse down event
11942 */
11943 OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
11944 if (
11945 this.isVisible() &&
11946 !$.contains( this.$element[ 0 ], e.target ) &&
11947 ( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length )
11948 ) {
11949 this.toggle( false );
11950 }
11951 };
11952
11953 /**
11954 * Bind mouse down listener.
11955 */
11956 OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
11957 // Capture clicks outside popup
11958 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
11959 };
11960
11961 /**
11962 * Handles close button click events.
11963 */
11964 OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
11965 if ( this.isVisible() ) {
11966 this.toggle( false );
11967 }
11968 };
11969
11970 /**
11971 * Unbind mouse down listener.
11972 */
11973 OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
11974 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
11975 };
11976
11977 /**
11978 * Set whether to show a anchor.
11979 *
11980 * @param {boolean} [show] Show anchor, omit to toggle
11981 */
11982 OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) {
11983 show = show === undefined ? !this.anchored : !!show;
11984
11985 if ( this.anchored !== show ) {
11986 if ( show ) {
11987 this.$element.addClass( 'oo-ui-popupWidget-anchored' );
11988 } else {
11989 this.$element.removeClass( 'oo-ui-popupWidget-anchored' );
11990 }
11991 this.anchored = show;
11992 }
11993 };
11994
11995 /**
11996 * Check if showing a anchor.
11997 *
11998 * @return {boolean} anchor is visible
11999 */
12000 OO.ui.PopupWidget.prototype.hasAnchor = function () {
12001 return this.anchor;
12002 };
12003
12004 /**
12005 * @inheritdoc
12006 */
12007 OO.ui.PopupWidget.prototype.toggle = function ( show ) {
12008 show = show === undefined ? !this.isVisible() : !!show;
12009
12010 var change = show !== this.isVisible();
12011
12012 // Parent method
12013 OO.ui.PopupWidget.super.prototype.toggle.call( this, show );
12014
12015 if ( change ) {
12016 if ( show ) {
12017 if ( this.autoClose ) {
12018 this.bindMouseDownListener();
12019 }
12020 this.updateDimensions();
12021 this.toggleClipping( true );
12022 } else {
12023 this.toggleClipping( false );
12024 if ( this.autoClose ) {
12025 this.unbindMouseDownListener();
12026 }
12027 }
12028 }
12029
12030 return this;
12031 };
12032
12033 /**
12034 * Set the size of the popup.
12035 *
12036 * Changing the size may also change the popup's position depending on the alignment.
12037 *
12038 * @param {number} width Width
12039 * @param {number} height Height
12040 * @param {boolean} [transition=false] Use a smooth transition
12041 * @chainable
12042 */
12043 OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) {
12044 this.width = width;
12045 this.height = height !== undefined ? height : null;
12046 if ( this.isVisible() ) {
12047 this.updateDimensions( transition );
12048 }
12049 };
12050
12051 /**
12052 * Update the size and position.
12053 *
12054 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
12055 * be called automatically.
12056 *
12057 * @param {boolean} [transition=false] Use a smooth transition
12058 * @chainable
12059 */
12060 OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
12061 var popupOffset, originOffset, containerLeft, containerWidth, containerRight,
12062 popupLeft, popupRight, overlapLeft, overlapRight, anchorWidth,
12063 widget = this;
12064
12065 if ( !this.$container ) {
12066 // Lazy-initialize $container if not specified in constructor
12067 this.$container = $( this.getClosestScrollableElementContainer() );
12068 }
12069
12070 // Set height and width before measuring things, since it might cause our measurements
12071 // to change (e.g. due to scrollbars appearing or disappearing)
12072 this.$popup.css( {
12073 width: this.width,
12074 height: this.height !== null ? this.height : 'auto'
12075 } );
12076
12077 // Compute initial popupOffset based on alignment
12078 popupOffset = this.width * ( { left: 0, center: -0.5, right: -1 } )[ this.align ];
12079
12080 // Figure out if this will cause the popup to go beyond the edge of the container
12081 originOffset = this.$element.offset().left;
12082 containerLeft = this.$container.offset().left;
12083 containerWidth = this.$container.innerWidth();
12084 containerRight = containerLeft + containerWidth;
12085 popupLeft = popupOffset - this.containerPadding;
12086 popupRight = popupOffset + this.containerPadding + this.width + this.containerPadding;
12087 overlapLeft = ( originOffset + popupLeft ) - containerLeft;
12088 overlapRight = containerRight - ( originOffset + popupRight );
12089
12090 // Adjust offset to make the popup not go beyond the edge, if needed
12091 if ( overlapRight < 0 ) {
12092 popupOffset += overlapRight;
12093 } else if ( overlapLeft < 0 ) {
12094 popupOffset -= overlapLeft;
12095 }
12096
12097 // Adjust offset to avoid anchor being rendered too close to the edge
12098 // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
12099 // TODO: Find a measurement that works for CSS anchors and image anchors
12100 anchorWidth = this.$anchor[ 0 ].scrollWidth * 2;
12101 if ( popupOffset + this.width < anchorWidth ) {
12102 popupOffset = anchorWidth - this.width;
12103 } else if ( -popupOffset < anchorWidth ) {
12104 popupOffset = -anchorWidth;
12105 }
12106
12107 // Prevent transition from being interrupted
12108 clearTimeout( this.transitionTimeout );
12109 if ( transition ) {
12110 // Enable transition
12111 this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
12112 }
12113
12114 // Position body relative to anchor
12115 this.$popup.css( 'margin-left', popupOffset );
12116
12117 if ( transition ) {
12118 // Prevent transitioning after transition is complete
12119 this.transitionTimeout = setTimeout( function () {
12120 widget.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
12121 }, 200 );
12122 } else {
12123 // Prevent transitioning immediately
12124 this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
12125 }
12126
12127 // Reevaluate clipping state since we've relocated and resized the popup
12128 this.clip();
12129
12130 return this;
12131 };
12132
12133 /**
12134 * Progress bar widget.
12135 *
12136 * @class
12137 * @extends OO.ui.Widget
12138 *
12139 * @constructor
12140 * @param {Object} [config] Configuration options
12141 * @cfg {number|boolean} [progress=false] Initial progress percent or false for indeterminate
12142 */
12143 OO.ui.ProgressBarWidget = function OoUiProgressBarWidget( config ) {
12144 // Configuration initialization
12145 config = config || {};
12146
12147 // Parent constructor
12148 OO.ui.ProgressBarWidget.super.call( this, config );
12149
12150 // Properties
12151 this.$bar = $( '<div>' );
12152 this.progress = null;
12153
12154 // Initialization
12155 this.setProgress( config.progress !== undefined ? config.progress : false );
12156 this.$bar.addClass( 'oo-ui-progressBarWidget-bar' );
12157 this.$element
12158 .attr( {
12159 role: 'progressbar',
12160 'aria-valuemin': 0,
12161 'aria-valuemax': 100
12162 } )
12163 .addClass( 'oo-ui-progressBarWidget' )
12164 .append( this.$bar );
12165 };
12166
12167 /* Setup */
12168
12169 OO.inheritClass( OO.ui.ProgressBarWidget, OO.ui.Widget );
12170
12171 /* Static Properties */
12172
12173 OO.ui.ProgressBarWidget.static.tagName = 'div';
12174
12175 /* Methods */
12176
12177 /**
12178 * Get progress percent
12179 *
12180 * @return {number} Progress percent
12181 */
12182 OO.ui.ProgressBarWidget.prototype.getProgress = function () {
12183 return this.progress;
12184 };
12185
12186 /**
12187 * Set progress percent
12188 *
12189 * @param {number|boolean} progress Progress percent or false for indeterminate
12190 */
12191 OO.ui.ProgressBarWidget.prototype.setProgress = function ( progress ) {
12192 this.progress = progress;
12193
12194 if ( progress !== false ) {
12195 this.$bar.css( 'width', this.progress + '%' );
12196 this.$element.attr( 'aria-valuenow', this.progress );
12197 } else {
12198 this.$bar.css( 'width', '' );
12199 this.$element.removeAttr( 'aria-valuenow' );
12200 }
12201 this.$element.toggleClass( 'oo-ui-progressBarWidget-indeterminate', !progress );
12202 };
12203
12204 /**
12205 * Search widget.
12206 *
12207 * Search widgets combine a query input, placed above, and a results selection widget, placed below.
12208 * Results are cleared and populated each time the query is changed.
12209 *
12210 * @class
12211 * @extends OO.ui.Widget
12212 *
12213 * @constructor
12214 * @param {Object} [config] Configuration options
12215 * @cfg {string|jQuery} [placeholder] Placeholder text for query input
12216 * @cfg {string} [value] Initial query value
12217 */
12218 OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
12219 // Configuration initialization
12220 config = config || {};
12221
12222 // Parent constructor
12223 OO.ui.SearchWidget.super.call( this, config );
12224
12225 // Properties
12226 this.query = new OO.ui.TextInputWidget( {
12227 icon: 'search',
12228 placeholder: config.placeholder,
12229 value: config.value
12230 } );
12231 this.results = new OO.ui.SelectWidget();
12232 this.$query = $( '<div>' );
12233 this.$results = $( '<div>' );
12234
12235 // Events
12236 this.query.connect( this, {
12237 change: 'onQueryChange',
12238 enter: 'onQueryEnter'
12239 } );
12240 this.results.connect( this, {
12241 highlight: 'onResultsHighlight',
12242 select: 'onResultsSelect'
12243 } );
12244 this.query.$input.on( 'keydown', this.onQueryKeydown.bind( this ) );
12245
12246 // Initialization
12247 this.$query
12248 .addClass( 'oo-ui-searchWidget-query' )
12249 .append( this.query.$element );
12250 this.$results
12251 .addClass( 'oo-ui-searchWidget-results' )
12252 .append( this.results.$element );
12253 this.$element
12254 .addClass( 'oo-ui-searchWidget' )
12255 .append( this.$results, this.$query );
12256 };
12257
12258 /* Setup */
12259
12260 OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget );
12261
12262 /* Events */
12263
12264 /**
12265 * @event highlight
12266 * @param {Object|null} item Item data or null if no item is highlighted
12267 */
12268
12269 /**
12270 * @event select
12271 * @param {Object|null} item Item data or null if no item is selected
12272 */
12273
12274 /* Methods */
12275
12276 /**
12277 * Handle query key down events.
12278 *
12279 * @param {jQuery.Event} e Key down event
12280 */
12281 OO.ui.SearchWidget.prototype.onQueryKeydown = function ( e ) {
12282 var highlightedItem, nextItem,
12283 dir = e.which === OO.ui.Keys.DOWN ? 1 : ( e.which === OO.ui.Keys.UP ? -1 : 0 );
12284
12285 if ( dir ) {
12286 highlightedItem = this.results.getHighlightedItem();
12287 if ( !highlightedItem ) {
12288 highlightedItem = this.results.getSelectedItem();
12289 }
12290 nextItem = this.results.getRelativeSelectableItem( highlightedItem, dir );
12291 this.results.highlightItem( nextItem );
12292 nextItem.scrollElementIntoView();
12293 }
12294 };
12295
12296 /**
12297 * Handle select widget select events.
12298 *
12299 * Clears existing results. Subclasses should repopulate items according to new query.
12300 *
12301 * @param {string} value New value
12302 */
12303 OO.ui.SearchWidget.prototype.onQueryChange = function () {
12304 // Reset
12305 this.results.clearItems();
12306 };
12307
12308 /**
12309 * Handle select widget enter key events.
12310 *
12311 * Selects highlighted item.
12312 *
12313 * @param {string} value New value
12314 */
12315 OO.ui.SearchWidget.prototype.onQueryEnter = function () {
12316 // Reset
12317 this.results.selectItem( this.results.getHighlightedItem() );
12318 };
12319
12320 /**
12321 * Handle select widget highlight events.
12322 *
12323 * @param {OO.ui.OptionWidget} item Highlighted item
12324 * @fires highlight
12325 */
12326 OO.ui.SearchWidget.prototype.onResultsHighlight = function ( item ) {
12327 this.emit( 'highlight', item ? item.getData() : null );
12328 };
12329
12330 /**
12331 * Handle select widget select events.
12332 *
12333 * @param {OO.ui.OptionWidget} item Selected item
12334 * @fires select
12335 */
12336 OO.ui.SearchWidget.prototype.onResultsSelect = function ( item ) {
12337 this.emit( 'select', item ? item.getData() : null );
12338 };
12339
12340 /**
12341 * Get the query input.
12342 *
12343 * @return {OO.ui.TextInputWidget} Query input
12344 */
12345 OO.ui.SearchWidget.prototype.getQuery = function () {
12346 return this.query;
12347 };
12348
12349 /**
12350 * Get the results list.
12351 *
12352 * @return {OO.ui.SelectWidget} Select list
12353 */
12354 OO.ui.SearchWidget.prototype.getResults = function () {
12355 return this.results;
12356 };
12357
12358 /**
12359 * Generic selection of options.
12360 *
12361 * Items can contain any rendering. Any widget that provides options, from which the user must
12362 * choose one, should be built on this class.
12363 *
12364 * Use together with OO.ui.OptionWidget.
12365 *
12366 * @class
12367 * @extends OO.ui.Widget
12368 * @mixins OO.ui.GroupElement
12369 *
12370 * @constructor
12371 * @param {Object} [config] Configuration options
12372 * @cfg {OO.ui.OptionWidget[]} [items] Options to add
12373 */
12374 OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
12375 // Configuration initialization
12376 config = config || {};
12377
12378 // Parent constructor
12379 OO.ui.SelectWidget.super.call( this, config );
12380
12381 // Mixin constructors
12382 OO.ui.GroupWidget.call( this, $.extend( {}, config, { $group: this.$element } ) );
12383
12384 // Properties
12385 this.pressed = false;
12386 this.selecting = null;
12387 this.onMouseUpHandler = this.onMouseUp.bind( this );
12388 this.onMouseMoveHandler = this.onMouseMove.bind( this );
12389
12390 // Events
12391 this.$element.on( {
12392 mousedown: this.onMouseDown.bind( this ),
12393 mouseover: this.onMouseOver.bind( this ),
12394 mouseleave: this.onMouseLeave.bind( this )
12395 } );
12396
12397 // Initialization
12398 this.$element
12399 .addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' )
12400 .attr( 'role', 'listbox' );
12401 if ( $.isArray( config.items ) ) {
12402 this.addItems( config.items );
12403 }
12404 };
12405
12406 /* Setup */
12407
12408 OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
12409
12410 // Need to mixin base class as well
12411 OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupElement );
12412 OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupWidget );
12413
12414 /* Events */
12415
12416 /**
12417 * @event highlight
12418 * @param {OO.ui.OptionWidget|null} item Highlighted item
12419 */
12420
12421 /**
12422 * @event press
12423 * @param {OO.ui.OptionWidget|null} item Pressed item
12424 */
12425
12426 /**
12427 * @event select
12428 * @param {OO.ui.OptionWidget|null} item Selected item
12429 */
12430
12431 /**
12432 * @event choose
12433 * @param {OO.ui.OptionWidget|null} item Chosen item
12434 */
12435
12436 /**
12437 * @event add
12438 * @param {OO.ui.OptionWidget[]} items Added items
12439 * @param {number} index Index items were added at
12440 */
12441
12442 /**
12443 * @event remove
12444 * @param {OO.ui.OptionWidget[]} items Removed items
12445 */
12446
12447 /* Methods */
12448
12449 /**
12450 * Handle mouse down events.
12451 *
12452 * @private
12453 * @param {jQuery.Event} e Mouse down event
12454 */
12455 OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
12456 var item;
12457
12458 if ( !this.isDisabled() && e.which === 1 ) {
12459 this.togglePressed( true );
12460 item = this.getTargetItem( e );
12461 if ( item && item.isSelectable() ) {
12462 this.pressItem( item );
12463 this.selecting = item;
12464 this.getElementDocument().addEventListener(
12465 'mouseup',
12466 this.onMouseUpHandler,
12467 true
12468 );
12469 this.getElementDocument().addEventListener(
12470 'mousemove',
12471 this.onMouseMoveHandler,
12472 true
12473 );
12474 }
12475 }
12476 return false;
12477 };
12478
12479 /**
12480 * Handle mouse up events.
12481 *
12482 * @private
12483 * @param {jQuery.Event} e Mouse up event
12484 */
12485 OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
12486 var item;
12487
12488 this.togglePressed( false );
12489 if ( !this.selecting ) {
12490 item = this.getTargetItem( e );
12491 if ( item && item.isSelectable() ) {
12492 this.selecting = item;
12493 }
12494 }
12495 if ( !this.isDisabled() && e.which === 1 && this.selecting ) {
12496 this.pressItem( null );
12497 this.chooseItem( this.selecting );
12498 this.selecting = null;
12499 }
12500
12501 this.getElementDocument().removeEventListener(
12502 'mouseup',
12503 this.onMouseUpHandler,
12504 true
12505 );
12506 this.getElementDocument().removeEventListener(
12507 'mousemove',
12508 this.onMouseMoveHandler,
12509 true
12510 );
12511
12512 return false;
12513 };
12514
12515 /**
12516 * Handle mouse move events.
12517 *
12518 * @private
12519 * @param {jQuery.Event} e Mouse move event
12520 */
12521 OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
12522 var item;
12523
12524 if ( !this.isDisabled() && this.pressed ) {
12525 item = this.getTargetItem( e );
12526 if ( item && item !== this.selecting && item.isSelectable() ) {
12527 this.pressItem( item );
12528 this.selecting = item;
12529 }
12530 }
12531 return false;
12532 };
12533
12534 /**
12535 * Handle mouse over events.
12536 *
12537 * @private
12538 * @param {jQuery.Event} e Mouse over event
12539 */
12540 OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
12541 var item;
12542
12543 if ( !this.isDisabled() ) {
12544 item = this.getTargetItem( e );
12545 this.highlightItem( item && item.isHighlightable() ? item : null );
12546 }
12547 return false;
12548 };
12549
12550 /**
12551 * Handle mouse leave events.
12552 *
12553 * @private
12554 * @param {jQuery.Event} e Mouse over event
12555 */
12556 OO.ui.SelectWidget.prototype.onMouseLeave = function () {
12557 if ( !this.isDisabled() ) {
12558 this.highlightItem( null );
12559 }
12560 return false;
12561 };
12562
12563 /**
12564 * Get the closest item to a jQuery.Event.
12565 *
12566 * @private
12567 * @param {jQuery.Event} e
12568 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
12569 */
12570 OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) {
12571 var $item = $( e.target ).closest( '.oo-ui-optionWidget' );
12572 if ( $item.length ) {
12573 return $item.data( 'oo-ui-optionWidget' );
12574 }
12575 return null;
12576 };
12577
12578 /**
12579 * Get selected item.
12580 *
12581 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
12582 */
12583 OO.ui.SelectWidget.prototype.getSelectedItem = function () {
12584 var i, len;
12585
12586 for ( i = 0, len = this.items.length; i < len; i++ ) {
12587 if ( this.items[ i ].isSelected() ) {
12588 return this.items[ i ];
12589 }
12590 }
12591 return null;
12592 };
12593
12594 /**
12595 * Get highlighted item.
12596 *
12597 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
12598 */
12599 OO.ui.SelectWidget.prototype.getHighlightedItem = function () {
12600 var i, len;
12601
12602 for ( i = 0, len = this.items.length; i < len; i++ ) {
12603 if ( this.items[ i ].isHighlighted() ) {
12604 return this.items[ i ];
12605 }
12606 }
12607 return null;
12608 };
12609
12610 /**
12611 * Toggle pressed state.
12612 *
12613 * @param {boolean} pressed An option is being pressed
12614 */
12615 OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) {
12616 if ( pressed === undefined ) {
12617 pressed = !this.pressed;
12618 }
12619 if ( pressed !== this.pressed ) {
12620 this.$element
12621 .toggleClass( 'oo-ui-selectWidget-pressed', pressed )
12622 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed );
12623 this.pressed = pressed;
12624 }
12625 };
12626
12627 /**
12628 * Highlight an item.
12629 *
12630 * Highlighting is mutually exclusive.
12631 *
12632 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit to deselect all
12633 * @fires highlight
12634 * @chainable
12635 */
12636 OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
12637 var i, len, highlighted,
12638 changed = false;
12639
12640 for ( i = 0, len = this.items.length; i < len; i++ ) {
12641 highlighted = this.items[ i ] === item;
12642 if ( this.items[ i ].isHighlighted() !== highlighted ) {
12643 this.items[ i ].setHighlighted( highlighted );
12644 changed = true;
12645 }
12646 }
12647 if ( changed ) {
12648 this.emit( 'highlight', item );
12649 }
12650
12651 return this;
12652 };
12653
12654 /**
12655 * Select an item.
12656 *
12657 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
12658 * @fires select
12659 * @chainable
12660 */
12661 OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
12662 var i, len, selected,
12663 changed = false;
12664
12665 for ( i = 0, len = this.items.length; i < len; i++ ) {
12666 selected = this.items[ i ] === item;
12667 if ( this.items[ i ].isSelected() !== selected ) {
12668 this.items[ i ].setSelected( selected );
12669 changed = true;
12670 }
12671 }
12672 if ( changed ) {
12673 this.emit( 'select', item );
12674 }
12675
12676 return this;
12677 };
12678
12679 /**
12680 * Press an item.
12681 *
12682 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
12683 * @fires press
12684 * @chainable
12685 */
12686 OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
12687 var i, len, pressed,
12688 changed = false;
12689
12690 for ( i = 0, len = this.items.length; i < len; i++ ) {
12691 pressed = this.items[ i ] === item;
12692 if ( this.items[ i ].isPressed() !== pressed ) {
12693 this.items[ i ].setPressed( pressed );
12694 changed = true;
12695 }
12696 }
12697 if ( changed ) {
12698 this.emit( 'press', item );
12699 }
12700
12701 return this;
12702 };
12703
12704 /**
12705 * Choose an item.
12706 *
12707 * Identical to #selectItem, but may vary in subclasses that want to take additional action when
12708 * an item is selected using the keyboard or mouse.
12709 *
12710 * @param {OO.ui.OptionWidget} item Item to choose
12711 * @fires choose
12712 * @chainable
12713 */
12714 OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
12715 this.selectItem( item );
12716 this.emit( 'choose', item );
12717
12718 return this;
12719 };
12720
12721 /**
12722 * Get an item relative to another one.
12723 *
12724 * @param {OO.ui.OptionWidget|null} item Item to start at, null to get relative to list start
12725 * @param {number} direction Direction to move in, -1 to move backward, 1 to move forward
12726 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the menu
12727 */
12728 OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction ) {
12729 var currentIndex, nextIndex, i,
12730 increase = direction > 0 ? 1 : -1,
12731 len = this.items.length;
12732
12733 if ( item instanceof OO.ui.OptionWidget ) {
12734 currentIndex = $.inArray( item, this.items );
12735 nextIndex = ( currentIndex + increase + len ) % len;
12736 } else {
12737 // If no item is selected and moving forward, start at the beginning.
12738 // If moving backward, start at the end.
12739 nextIndex = direction > 0 ? 0 : len - 1;
12740 }
12741
12742 for ( i = 0; i < len; i++ ) {
12743 item = this.items[ nextIndex ];
12744 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
12745 return item;
12746 }
12747 nextIndex = ( nextIndex + increase + len ) % len;
12748 }
12749 return null;
12750 };
12751
12752 /**
12753 * Get the next selectable item.
12754 *
12755 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
12756 */
12757 OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
12758 var i, len, item;
12759
12760 for ( i = 0, len = this.items.length; i < len; i++ ) {
12761 item = this.items[ i ];
12762 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
12763 return item;
12764 }
12765 }
12766
12767 return null;
12768 };
12769
12770 /**
12771 * Add items.
12772 *
12773 * @param {OO.ui.OptionWidget[]} items Items to add
12774 * @param {number} [index] Index to insert items after
12775 * @fires add
12776 * @chainable
12777 */
12778 OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
12779 // Mixin method
12780 OO.ui.GroupWidget.prototype.addItems.call( this, items, index );
12781
12782 // Always provide an index, even if it was omitted
12783 this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index );
12784
12785 return this;
12786 };
12787
12788 /**
12789 * Remove items.
12790 *
12791 * Items will be detached, not removed, so they can be used later.
12792 *
12793 * @param {OO.ui.OptionWidget[]} items Items to remove
12794 * @fires remove
12795 * @chainable
12796 */
12797 OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
12798 var i, len, item;
12799
12800 // Deselect items being removed
12801 for ( i = 0, len = items.length; i < len; i++ ) {
12802 item = items[ i ];
12803 if ( item.isSelected() ) {
12804 this.selectItem( null );
12805 }
12806 }
12807
12808 // Mixin method
12809 OO.ui.GroupWidget.prototype.removeItems.call( this, items );
12810
12811 this.emit( 'remove', items );
12812
12813 return this;
12814 };
12815
12816 /**
12817 * Clear all items.
12818 *
12819 * Items will be detached, not removed, so they can be used later.
12820 *
12821 * @fires remove
12822 * @chainable
12823 */
12824 OO.ui.SelectWidget.prototype.clearItems = function () {
12825 var items = this.items.slice();
12826
12827 // Mixin method
12828 OO.ui.GroupWidget.prototype.clearItems.call( this );
12829
12830 // Clear selection
12831 this.selectItem( null );
12832
12833 this.emit( 'remove', items );
12834
12835 return this;
12836 };
12837
12838 /**
12839 * Select widget containing button options.
12840 *
12841 * Use together with OO.ui.ButtonOptionWidget.
12842 *
12843 * @class
12844 * @extends OO.ui.SelectWidget
12845 *
12846 * @constructor
12847 * @param {Object} [config] Configuration options
12848 */
12849 OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
12850 // Parent constructor
12851 OO.ui.ButtonSelectWidget.super.call( this, config );
12852
12853 // Initialization
12854 this.$element.addClass( 'oo-ui-buttonSelectWidget' );
12855 };
12856
12857 /* Setup */
12858
12859 OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
12860
12861 /**
12862 * Select widget containing radio button options.
12863 *
12864 * Use together with OO.ui.RadioOptionWidget.
12865 *
12866 * @class
12867 * @extends OO.ui.SelectWidget
12868 *
12869 * @constructor
12870 * @param {Object} [config] Configuration options
12871 */
12872 OO.ui.RadioSelectWidget = function OoUiRadioSelectWidget( config ) {
12873 // Parent constructor
12874 OO.ui.RadioSelectWidget.super.call( this, config );
12875
12876 // Initialization
12877 this.$element.addClass( 'oo-ui-radioSelectWidget' );
12878 };
12879
12880 /* Setup */
12881
12882 OO.inheritClass( OO.ui.RadioSelectWidget, OO.ui.SelectWidget );
12883
12884 /**
12885 * Overlaid menu of options.
12886 *
12887 * Menus are clipped to the visible viewport. They do not provide a control for opening or closing
12888 * the menu.
12889 *
12890 * Use together with OO.ui.MenuOptionWidget.
12891 *
12892 * @class
12893 * @extends OO.ui.SelectWidget
12894 * @mixins OO.ui.ClippableElement
12895 *
12896 * @constructor
12897 * @param {Object} [config] Configuration options
12898 * @cfg {OO.ui.InputWidget} [input] Input to bind keyboard handlers to
12899 * @cfg {OO.ui.Widget} [widget] Widget to bind mouse handlers to
12900 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu
12901 */
12902 OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) {
12903 // Configuration initialization
12904 config = config || {};
12905
12906 // Parent constructor
12907 OO.ui.MenuSelectWidget.super.call( this, config );
12908
12909 // Mixin constructors
12910 OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
12911
12912 // Properties
12913 this.newItems = null;
12914 this.autoHide = config.autoHide === undefined || !!config.autoHide;
12915 this.$input = config.input ? config.input.$input : null;
12916 this.$widget = config.widget ? config.widget.$element : null;
12917 this.onKeyDownHandler = this.onKeyDown.bind( this );
12918 this.onDocumentMouseDownHandler = this.onDocumentMouseDown.bind( this );
12919
12920 // Initialization
12921 this.$element
12922 .addClass( 'oo-ui-menuSelectWidget' )
12923 .attr( 'role', 'menu' );
12924
12925 // Initially hidden - using #toggle may cause errors if subclasses override toggle with methods
12926 // that reference properties not initialized at that time of parent class construction
12927 // TODO: Find a better way to handle post-constructor setup
12928 this.visible = false;
12929 this.$element.addClass( 'oo-ui-element-hidden' );
12930 };
12931
12932 /* Setup */
12933
12934 OO.inheritClass( OO.ui.MenuSelectWidget, OO.ui.SelectWidget );
12935 OO.mixinClass( OO.ui.MenuSelectWidget, OO.ui.ClippableElement );
12936
12937 /* Methods */
12938
12939 /**
12940 * Handles document mouse down events.
12941 *
12942 * @param {jQuery.Event} e Key down event
12943 */
12944 OO.ui.MenuSelectWidget.prototype.onDocumentMouseDown = function ( e ) {
12945 if (
12946 !OO.ui.contains( this.$element[ 0 ], e.target, true ) &&
12947 ( !this.$widget || !OO.ui.contains( this.$widget[ 0 ], e.target, true ) )
12948 ) {
12949 this.toggle( false );
12950 }
12951 };
12952
12953 /**
12954 * Handles key down events.
12955 *
12956 * @param {jQuery.Event} e Key down event
12957 */
12958 OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) {
12959 var nextItem,
12960 handled = false,
12961 highlightItem = this.getHighlightedItem();
12962
12963 if ( !this.isDisabled() && this.isVisible() ) {
12964 if ( !highlightItem ) {
12965 highlightItem = this.getSelectedItem();
12966 }
12967 switch ( e.keyCode ) {
12968 case OO.ui.Keys.ENTER:
12969 this.chooseItem( highlightItem );
12970 handled = true;
12971 break;
12972 case OO.ui.Keys.UP:
12973 nextItem = this.getRelativeSelectableItem( highlightItem, -1 );
12974 handled = true;
12975 break;
12976 case OO.ui.Keys.DOWN:
12977 nextItem = this.getRelativeSelectableItem( highlightItem, 1 );
12978 handled = true;
12979 break;
12980 case OO.ui.Keys.ESCAPE:
12981 case OO.ui.Keys.TAB:
12982 if ( highlightItem ) {
12983 highlightItem.setHighlighted( false );
12984 }
12985 this.toggle( false );
12986 // Don't prevent tabbing away
12987 handled = ( e.keyCode === OO.ui.Keys.ESCAPE );
12988 break;
12989 }
12990
12991 if ( nextItem ) {
12992 this.highlightItem( nextItem );
12993 nextItem.scrollElementIntoView();
12994 }
12995
12996 if ( handled ) {
12997 e.preventDefault();
12998 e.stopPropagation();
12999 return false;
13000 }
13001 }
13002 };
13003
13004 /**
13005 * Bind key down listener.
13006 */
13007 OO.ui.MenuSelectWidget.prototype.bindKeyDownListener = function () {
13008 if ( this.$input ) {
13009 this.$input.on( 'keydown', this.onKeyDownHandler );
13010 } else {
13011 // Capture menu navigation keys
13012 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
13013 }
13014 };
13015
13016 /**
13017 * Unbind key down listener.
13018 */
13019 OO.ui.MenuSelectWidget.prototype.unbindKeyDownListener = function () {
13020 if ( this.$input ) {
13021 this.$input.off( 'keydown', this.onKeyDownHandler );
13022 } else {
13023 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
13024 }
13025 };
13026
13027 /**
13028 * Choose an item.
13029 *
13030 * This will close the menu, unlike #selectItem which only changes selection.
13031 *
13032 * @param {OO.ui.OptionWidget} item Item to choose
13033 * @chainable
13034 */
13035 OO.ui.MenuSelectWidget.prototype.chooseItem = function ( item ) {
13036 OO.ui.MenuSelectWidget.super.prototype.chooseItem.call( this, item );
13037 this.toggle( false );
13038 return this;
13039 };
13040
13041 /**
13042 * @inheritdoc
13043 */
13044 OO.ui.MenuSelectWidget.prototype.addItems = function ( items, index ) {
13045 var i, len, item;
13046
13047 // Parent method
13048 OO.ui.MenuSelectWidget.super.prototype.addItems.call( this, items, index );
13049
13050 // Auto-initialize
13051 if ( !this.newItems ) {
13052 this.newItems = [];
13053 }
13054
13055 for ( i = 0, len = items.length; i < len; i++ ) {
13056 item = items[ i ];
13057 if ( this.isVisible() ) {
13058 // Defer fitting label until item has been attached
13059 item.fitLabel();
13060 } else {
13061 this.newItems.push( item );
13062 }
13063 }
13064
13065 // Reevaluate clipping
13066 this.clip();
13067
13068 return this;
13069 };
13070
13071 /**
13072 * @inheritdoc
13073 */
13074 OO.ui.MenuSelectWidget.prototype.removeItems = function ( items ) {
13075 // Parent method
13076 OO.ui.MenuSelectWidget.super.prototype.removeItems.call( this, items );
13077
13078 // Reevaluate clipping
13079 this.clip();
13080
13081 return this;
13082 };
13083
13084 /**
13085 * @inheritdoc
13086 */
13087 OO.ui.MenuSelectWidget.prototype.clearItems = function () {
13088 // Parent method
13089 OO.ui.MenuSelectWidget.super.prototype.clearItems.call( this );
13090
13091 // Reevaluate clipping
13092 this.clip();
13093
13094 return this;
13095 };
13096
13097 /**
13098 * @inheritdoc
13099 */
13100 OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
13101 visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
13102
13103 var i, len,
13104 change = visible !== this.isVisible();
13105
13106 // Parent method
13107 OO.ui.MenuSelectWidget.super.prototype.toggle.call( this, visible );
13108
13109 if ( change ) {
13110 if ( visible ) {
13111 this.bindKeyDownListener();
13112
13113 if ( this.newItems && this.newItems.length ) {
13114 for ( i = 0, len = this.newItems.length; i < len; i++ ) {
13115 this.newItems[ i ].fitLabel();
13116 }
13117 this.newItems = null;
13118 }
13119 this.toggleClipping( true );
13120
13121 // Auto-hide
13122 if ( this.autoHide ) {
13123 this.getElementDocument().addEventListener(
13124 'mousedown', this.onDocumentMouseDownHandler, true
13125 );
13126 }
13127 } else {
13128 this.unbindKeyDownListener();
13129 this.getElementDocument().removeEventListener(
13130 'mousedown', this.onDocumentMouseDownHandler, true
13131 );
13132 this.toggleClipping( false );
13133 }
13134 }
13135
13136 return this;
13137 };
13138
13139 /**
13140 * Menu for a text input widget.
13141 *
13142 * This menu is specially designed to be positioned beneath a text input widget. The menu's position
13143 * is automatically calculated and maintained when the menu is toggled or the window is resized.
13144 *
13145 * @class
13146 * @extends OO.ui.MenuSelectWidget
13147 *
13148 * @constructor
13149 * @param {OO.ui.TextInputWidget} input Text input widget to provide menu for
13150 * @param {Object} [config] Configuration options
13151 * @cfg {jQuery} [$container=input.$element] Element to render menu under
13152 */
13153 OO.ui.TextInputMenuSelectWidget = function OoUiTextInputMenuSelectWidget( input, config ) {
13154 // Configuration initialization
13155 config = config || {};
13156
13157 // Parent constructor
13158 OO.ui.TextInputMenuSelectWidget.super.call( this, config );
13159
13160 // Properties
13161 this.input = input;
13162 this.$container = config.$container || this.input.$element;
13163 this.onWindowResizeHandler = this.onWindowResize.bind( this );
13164
13165 // Initialization
13166 this.$element.addClass( 'oo-ui-textInputMenuSelectWidget' );
13167 };
13168
13169 /* Setup */
13170
13171 OO.inheritClass( OO.ui.TextInputMenuSelectWidget, OO.ui.MenuSelectWidget );
13172
13173 /* Methods */
13174
13175 /**
13176 * Handle window resize event.
13177 *
13178 * @param {jQuery.Event} e Window resize event
13179 */
13180 OO.ui.TextInputMenuSelectWidget.prototype.onWindowResize = function () {
13181 this.position();
13182 };
13183
13184 /**
13185 * @inheritdoc
13186 */
13187 OO.ui.TextInputMenuSelectWidget.prototype.toggle = function ( visible ) {
13188 visible = visible === undefined ? !this.isVisible() : !!visible;
13189
13190 var change = visible !== this.isVisible();
13191
13192 if ( change && visible ) {
13193 // Make sure the width is set before the parent method runs.
13194 // After this we have to call this.position(); again to actually
13195 // position ourselves correctly.
13196 this.position();
13197 }
13198
13199 // Parent method
13200 OO.ui.TextInputMenuSelectWidget.super.prototype.toggle.call( this, visible );
13201
13202 if ( change ) {
13203 if ( this.isVisible() ) {
13204 this.position();
13205 $( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
13206 } else {
13207 $( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
13208 }
13209 }
13210
13211 return this;
13212 };
13213
13214 /**
13215 * Position the menu.
13216 *
13217 * @chainable
13218 */
13219 OO.ui.TextInputMenuSelectWidget.prototype.position = function () {
13220 var $container = this.$container,
13221 pos = OO.ui.Element.static.getRelativePosition( $container, this.$element.offsetParent() );
13222
13223 // Position under input
13224 pos.top += $container.height();
13225 this.$element.css( pos );
13226
13227 // Set width
13228 this.setIdealSize( $container.width() );
13229 // We updated the position, so re-evaluate the clipping state
13230 this.clip();
13231
13232 return this;
13233 };
13234
13235 /**
13236 * Structured list of items.
13237 *
13238 * Use with OO.ui.OutlineOptionWidget.
13239 *
13240 * @class
13241 * @extends OO.ui.SelectWidget
13242 *
13243 * @constructor
13244 * @param {Object} [config] Configuration options
13245 */
13246 OO.ui.OutlineSelectWidget = function OoUiOutlineSelectWidget( config ) {
13247 // Configuration initialization
13248 config = config || {};
13249
13250 // Parent constructor
13251 OO.ui.OutlineSelectWidget.super.call( this, config );
13252
13253 // Initialization
13254 this.$element.addClass( 'oo-ui-outlineSelectWidget' );
13255 };
13256
13257 /* Setup */
13258
13259 OO.inheritClass( OO.ui.OutlineSelectWidget, OO.ui.SelectWidget );
13260
13261 /**
13262 * Switch that slides on and off.
13263 *
13264 * @class
13265 * @extends OO.ui.Widget
13266 * @mixins OO.ui.ToggleWidget
13267 * @mixins OO.ui.TabIndexedElement
13268 *
13269 * @constructor
13270 * @param {Object} [config] Configuration options
13271 * @cfg {boolean} [value=false] Initial value
13272 */
13273 OO.ui.ToggleSwitchWidget = function OoUiToggleSwitchWidget( config ) {
13274 // Parent constructor
13275 OO.ui.ToggleSwitchWidget.super.call( this, config );
13276
13277 // Mixin constructors
13278 OO.ui.ToggleWidget.call( this, config );
13279 OO.ui.TabIndexedElement.call( this, config );
13280
13281 // Properties
13282 this.dragging = false;
13283 this.dragStart = null;
13284 this.sliding = false;
13285 this.$glow = $( '<span>' );
13286 this.$grip = $( '<span>' );
13287
13288 // Events
13289 this.$element.on( {
13290 click: this.onClick.bind( this ),
13291 keypress: this.onKeyPress.bind( this )
13292 } );
13293
13294 // Initialization
13295 this.$glow.addClass( 'oo-ui-toggleSwitchWidget-glow' );
13296 this.$grip.addClass( 'oo-ui-toggleSwitchWidget-grip' );
13297 this.$element
13298 .addClass( 'oo-ui-toggleSwitchWidget' )
13299 .attr( 'role', 'checkbox' )
13300 .append( this.$glow, this.$grip );
13301 };
13302
13303 /* Setup */
13304
13305 OO.inheritClass( OO.ui.ToggleSwitchWidget, OO.ui.Widget );
13306 OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.ToggleWidget );
13307 OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.TabIndexedElement );
13308
13309 /* Methods */
13310
13311 /**
13312 * Handle mouse click events.
13313 *
13314 * @param {jQuery.Event} e Mouse click event
13315 */
13316 OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
13317 if ( !this.isDisabled() && e.which === 1 ) {
13318 this.setValue( !this.value );
13319 }
13320 return false;
13321 };
13322
13323 /**
13324 * Handle key press events.
13325 *
13326 * @param {jQuery.Event} e Key press event
13327 */
13328 OO.ui.ToggleSwitchWidget.prototype.onKeyPress = function ( e ) {
13329 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
13330 this.setValue( !this.value );
13331 }
13332 return false;
13333 };
13334
13335 }( OO ) );