Update OOjs UI to v0.6.0
[lhc/web/wiklou.git] / resources / lib / oojs-ui / oojs-ui.js
1 /*!
2 * OOjs UI v0.6.0
3 * https://www.mediawiki.org/wiki/OOjs_UI
4 *
5 * Copyright 2011–2014 OOjs Team and other contributors.
6 * Released under the MIT license
7 * http://oojs.mit-license.org
8 *
9 * Date: 2014-12-16T21:00:55Z
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], params = Array.prototype.slice.call( arguments, 1 );
175 if ( typeof message === 'string' ) {
176 // Perform $1 substitution
177 message = message.replace( /\$(\d+)/g, function ( unused, n ) {
178 var i = parseInt( n, 10 );
179 return params[i - 1] !== undefined ? params[i - 1] : '$' + n;
180 } );
181 } else {
182 // Return placeholder if message not found
183 message = '[' + key + ']';
184 }
185 return message;
186 };
187
188 /**
189 * Package a message and arguments for deferred resolution.
190 *
191 * Use this when you are statically specifying a message and the message may not yet be present.
192 *
193 * @param {string} key Message key
194 * @param {Mixed...} [params] Message parameters
195 * @return {Function} Function that returns the resolved message when executed
196 */
197 OO.ui.deferMsg = function () {
198 var args = arguments;
199 return function () {
200 return OO.ui.msg.apply( OO.ui, args );
201 };
202 };
203
204 /**
205 * Resolve a message.
206 *
207 * If the message is a function it will be executed, otherwise it will pass through directly.
208 *
209 * @param {Function|string} msg Deferred message, or message text
210 * @return {string} Resolved message
211 */
212 OO.ui.resolveMsg = function ( msg ) {
213 if ( $.isFunction( msg ) ) {
214 return msg();
215 }
216 return msg;
217 };
218
219 } )();
220
221 /**
222 * Element that can be marked as pending.
223 *
224 * @abstract
225 * @class
226 *
227 * @constructor
228 * @param {Object} [config] Configuration options
229 * @cfg {jQuery} [$pending] Element to mark as pending, defaults to this.$element
230 */
231 OO.ui.PendingElement = function OoUiPendingElement( config ) {
232 // Configuration initialization
233 config = config || {};
234
235 // Properties
236 this.pending = 0;
237 this.$pending = null;
238
239 // Initialisation
240 this.setPendingElement( config.$pending || this.$element );
241 };
242
243 /* Setup */
244
245 OO.initClass( OO.ui.PendingElement );
246
247 /* Methods */
248
249 /**
250 * Set the pending element (and clean up any existing one).
251 *
252 * @param {jQuery} $pending The element to set to pending.
253 */
254 OO.ui.PendingElement.prototype.setPendingElement = function ( $pending ) {
255 if ( this.$pending ) {
256 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
257 }
258
259 this.$pending = $pending;
260 if ( this.pending > 0 ) {
261 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
262 }
263 };
264
265 /**
266 * Check if input is pending.
267 *
268 * @return {boolean}
269 */
270 OO.ui.PendingElement.prototype.isPending = function () {
271 return !!this.pending;
272 };
273
274 /**
275 * Increase the pending stack.
276 *
277 * @chainable
278 */
279 OO.ui.PendingElement.prototype.pushPending = function () {
280 if ( this.pending === 0 ) {
281 this.$pending.addClass( 'oo-ui-pendingElement-pending' );
282 this.updateThemeClasses();
283 }
284 this.pending++;
285
286 return this;
287 };
288
289 /**
290 * Reduce the pending stack.
291 *
292 * Clamped at zero.
293 *
294 * @chainable
295 */
296 OO.ui.PendingElement.prototype.popPending = function () {
297 if ( this.pending === 1 ) {
298 this.$pending.removeClass( 'oo-ui-pendingElement-pending' );
299 this.updateThemeClasses();
300 }
301 this.pending = Math.max( 0, this.pending - 1 );
302
303 return this;
304 };
305
306 /**
307 * List of actions.
308 *
309 * @abstract
310 * @class
311 * @mixins OO.EventEmitter
312 *
313 * @constructor
314 * @param {Object} [config] Configuration options
315 */
316 OO.ui.ActionSet = function OoUiActionSet( config ) {
317 // Configuration initialization
318 config = config || {};
319
320 // Mixin constructors
321 OO.EventEmitter.call( this );
322
323 // Properties
324 this.list = [];
325 this.categories = {
326 actions: 'getAction',
327 flags: 'getFlags',
328 modes: 'getModes'
329 };
330 this.categorized = {};
331 this.special = {};
332 this.others = [];
333 this.organized = false;
334 this.changing = false;
335 this.changed = false;
336 };
337
338 /* Setup */
339
340 OO.mixinClass( OO.ui.ActionSet, OO.EventEmitter );
341
342 /* Static Properties */
343
344 /**
345 * Symbolic name of dialog.
346 *
347 * @abstract
348 * @static
349 * @inheritable
350 * @property {string}
351 */
352 OO.ui.ActionSet.static.specialFlags = [ 'safe', 'primary' ];
353
354 /* Events */
355
356 /**
357 * @event click
358 * @param {OO.ui.ActionWidget} action Action that was clicked
359 */
360
361 /**
362 * @event resize
363 * @param {OO.ui.ActionWidget} action Action that was resized
364 */
365
366 /**
367 * @event add
368 * @param {OO.ui.ActionWidget[]} added Actions added
369 */
370
371 /**
372 * @event remove
373 * @param {OO.ui.ActionWidget[]} added Actions removed
374 */
375
376 /**
377 * @event change
378 */
379
380 /* Methods */
381
382 /**
383 * Handle action change events.
384 *
385 * @fires change
386 */
387 OO.ui.ActionSet.prototype.onActionChange = function () {
388 this.organized = false;
389 if ( this.changing ) {
390 this.changed = true;
391 } else {
392 this.emit( 'change' );
393 }
394 };
395
396 /**
397 * Check if a action is one of the special actions.
398 *
399 * @param {OO.ui.ActionWidget} action Action to check
400 * @return {boolean} Action is special
401 */
402 OO.ui.ActionSet.prototype.isSpecial = function ( action ) {
403 var flag;
404
405 for ( flag in this.special ) {
406 if ( action === this.special[flag] ) {
407 return true;
408 }
409 }
410
411 return false;
412 };
413
414 /**
415 * Get actions.
416 *
417 * @param {Object} [filters] Filters to use, omit to get all actions
418 * @param {string|string[]} [filters.actions] Actions that actions must have
419 * @param {string|string[]} [filters.flags] Flags that actions must have
420 * @param {string|string[]} [filters.modes] Modes that actions must have
421 * @param {boolean} [filters.visible] Actions must be visible
422 * @param {boolean} [filters.disabled] Actions must be disabled
423 * @return {OO.ui.ActionWidget[]} Actions matching all criteria
424 */
425 OO.ui.ActionSet.prototype.get = function ( filters ) {
426 var i, len, list, category, actions, index, match, matches;
427
428 if ( filters ) {
429 this.organize();
430
431 // Collect category candidates
432 matches = [];
433 for ( category in this.categorized ) {
434 list = filters[category];
435 if ( list ) {
436 if ( !Array.isArray( list ) ) {
437 list = [ list ];
438 }
439 for ( i = 0, len = list.length; i < len; i++ ) {
440 actions = this.categorized[category][list[i]];
441 if ( Array.isArray( actions ) ) {
442 matches.push.apply( matches, actions );
443 }
444 }
445 }
446 }
447 // Remove by boolean filters
448 for ( i = 0, len = matches.length; i < len; i++ ) {
449 match = matches[i];
450 if (
451 ( filters.visible !== undefined && match.isVisible() !== filters.visible ) ||
452 ( filters.disabled !== undefined && match.isDisabled() !== filters.disabled )
453 ) {
454 matches.splice( i, 1 );
455 len--;
456 i--;
457 }
458 }
459 // Remove duplicates
460 for ( i = 0, len = matches.length; i < len; i++ ) {
461 match = matches[i];
462 index = matches.lastIndexOf( match );
463 while ( index !== i ) {
464 matches.splice( index, 1 );
465 len--;
466 index = matches.lastIndexOf( match );
467 }
468 }
469 return matches;
470 }
471 return this.list.slice();
472 };
473
474 /**
475 * Get special actions.
476 *
477 * Special actions are the first visible actions with special flags, such as 'safe' and 'primary'.
478 * Special flags can be configured by changing #static-specialFlags in a subclass.
479 *
480 * @return {OO.ui.ActionWidget|null} Safe action
481 */
482 OO.ui.ActionSet.prototype.getSpecial = function () {
483 this.organize();
484 return $.extend( {}, this.special );
485 };
486
487 /**
488 * Get other actions.
489 *
490 * Other actions include all non-special visible actions.
491 *
492 * @return {OO.ui.ActionWidget[]} Other actions
493 */
494 OO.ui.ActionSet.prototype.getOthers = function () {
495 this.organize();
496 return this.others.slice();
497 };
498
499 /**
500 * Toggle actions based on their modes.
501 *
502 * Unlike calling toggle on actions with matching flags, this will enforce mutually exclusive
503 * visibility; matching actions will be shown, non-matching actions will be hidden.
504 *
505 * @param {string} mode Mode actions must have
506 * @chainable
507 * @fires toggle
508 * @fires change
509 */
510 OO.ui.ActionSet.prototype.setMode = function ( mode ) {
511 var i, len, action;
512
513 this.changing = true;
514 for ( i = 0, len = this.list.length; i < len; i++ ) {
515 action = this.list[i];
516 action.toggle( action.hasMode( mode ) );
517 }
518
519 this.organized = false;
520 this.changing = false;
521 this.emit( 'change' );
522
523 return this;
524 };
525
526 /**
527 * Change which actions are able to be performed.
528 *
529 * Actions with matching actions will be disabled/enabled. Other actions will not be changed.
530 *
531 * @param {Object.<string,boolean>} actions List of abilities, keyed by action name, values
532 * indicate actions are able to be performed
533 * @chainable
534 */
535 OO.ui.ActionSet.prototype.setAbilities = function ( actions ) {
536 var i, len, action, item;
537
538 for ( i = 0, len = this.list.length; i < len; i++ ) {
539 item = this.list[i];
540 action = item.getAction();
541 if ( actions[action] !== undefined ) {
542 item.setDisabled( !actions[action] );
543 }
544 }
545
546 return this;
547 };
548
549 /**
550 * Executes a function once per action.
551 *
552 * When making changes to multiple actions, use this method instead of iterating over the actions
553 * manually to defer emitting a change event until after all actions have been changed.
554 *
555 * @param {Object|null} actions Filters to use for which actions to iterate over; see #get
556 * @param {Function} callback Callback to run for each action; callback is invoked with three
557 * arguments: the action, the action's index, the list of actions being iterated over
558 * @chainable
559 */
560 OO.ui.ActionSet.prototype.forEach = function ( filter, callback ) {
561 this.changed = false;
562 this.changing = true;
563 this.get( filter ).forEach( callback );
564 this.changing = false;
565 if ( this.changed ) {
566 this.emit( 'change' );
567 }
568
569 return this;
570 };
571
572 /**
573 * Add actions.
574 *
575 * @param {OO.ui.ActionWidget[]} actions Actions to add
576 * @chainable
577 * @fires add
578 * @fires change
579 */
580 OO.ui.ActionSet.prototype.add = function ( actions ) {
581 var i, len, action;
582
583 this.changing = true;
584 for ( i = 0, len = actions.length; i < len; i++ ) {
585 action = actions[i];
586 action.connect( this, {
587 click: [ 'emit', 'click', action ],
588 resize: [ 'emit', 'resize', action ],
589 toggle: [ 'onActionChange' ]
590 } );
591 this.list.push( action );
592 }
593 this.organized = false;
594 this.emit( 'add', actions );
595 this.changing = false;
596 this.emit( 'change' );
597
598 return this;
599 };
600
601 /**
602 * Remove actions.
603 *
604 * @param {OO.ui.ActionWidget[]} actions Actions to remove
605 * @chainable
606 * @fires remove
607 * @fires change
608 */
609 OO.ui.ActionSet.prototype.remove = function ( actions ) {
610 var i, len, index, action;
611
612 this.changing = true;
613 for ( i = 0, len = actions.length; i < len; i++ ) {
614 action = actions[i];
615 index = this.list.indexOf( action );
616 if ( index !== -1 ) {
617 action.disconnect( this );
618 this.list.splice( index, 1 );
619 }
620 }
621 this.organized = false;
622 this.emit( 'remove', actions );
623 this.changing = false;
624 this.emit( 'change' );
625
626 return this;
627 };
628
629 /**
630 * Remove all actions.
631 *
632 * @chainable
633 * @fires remove
634 * @fires change
635 */
636 OO.ui.ActionSet.prototype.clear = function () {
637 var i, len, action,
638 removed = this.list.slice();
639
640 this.changing = true;
641 for ( i = 0, len = this.list.length; i < len; i++ ) {
642 action = this.list[i];
643 action.disconnect( this );
644 }
645
646 this.list = [];
647
648 this.organized = false;
649 this.emit( 'remove', removed );
650 this.changing = false;
651 this.emit( 'change' );
652
653 return this;
654 };
655
656 /**
657 * Organize actions.
658 *
659 * This is called whenever organized information is requested. It will only reorganize the actions
660 * if something has changed since the last time it ran.
661 *
662 * @private
663 * @chainable
664 */
665 OO.ui.ActionSet.prototype.organize = function () {
666 var i, iLen, j, jLen, flag, action, category, list, item, special,
667 specialFlags = this.constructor.static.specialFlags;
668
669 if ( !this.organized ) {
670 this.categorized = {};
671 this.special = {};
672 this.others = [];
673 for ( i = 0, iLen = this.list.length; i < iLen; i++ ) {
674 action = this.list[i];
675 if ( action.isVisible() ) {
676 // Populate categories
677 for ( category in this.categories ) {
678 if ( !this.categorized[category] ) {
679 this.categorized[category] = {};
680 }
681 list = action[this.categories[category]]();
682 if ( !Array.isArray( list ) ) {
683 list = [ list ];
684 }
685 for ( j = 0, jLen = list.length; j < jLen; j++ ) {
686 item = list[j];
687 if ( !this.categorized[category][item] ) {
688 this.categorized[category][item] = [];
689 }
690 this.categorized[category][item].push( action );
691 }
692 }
693 // Populate special/others
694 special = false;
695 for ( j = 0, jLen = specialFlags.length; j < jLen; j++ ) {
696 flag = specialFlags[j];
697 if ( !this.special[flag] && action.hasFlag( flag ) ) {
698 this.special[flag] = action;
699 special = true;
700 break;
701 }
702 }
703 if ( !special ) {
704 this.others.push( action );
705 }
706 }
707 }
708 this.organized = true;
709 }
710
711 return this;
712 };
713
714 /**
715 * DOM element abstraction.
716 *
717 * @abstract
718 * @class
719 *
720 * @constructor
721 * @param {Object} [config] Configuration options
722 * @cfg {Function} [$] jQuery for the frame the widget is in
723 * @cfg {string[]} [classes] CSS class names to add
724 * @cfg {string} [text] Text to insert
725 * @cfg {jQuery} [$content] Content elements to append (after text)
726 * @cfg {Mixed} [data] Element data
727 */
728 OO.ui.Element = function OoUiElement( config ) {
729 // Configuration initialization
730 config = config || {};
731
732 // Properties
733 this.$ = config.$ || OO.ui.Element.static.getJQuery( document );
734 this.data = config.data;
735 this.$element = this.$( this.$.context.createElement( this.getTagName() ) );
736 this.elementGroup = null;
737 this.debouncedUpdateThemeClassesHandler = this.debouncedUpdateThemeClasses.bind( this );
738 this.updateThemeClassesPending = false;
739
740 // Initialization
741 if ( $.isArray( config.classes ) ) {
742 this.$element.addClass( config.classes.join( ' ' ) );
743 }
744 if ( config.text ) {
745 this.$element.text( config.text );
746 }
747 if ( config.$content ) {
748 this.$element.append( config.$content );
749 }
750 };
751
752 /* Setup */
753
754 OO.initClass( OO.ui.Element );
755
756 /* Static Properties */
757
758 /**
759 * HTML tag name.
760 *
761 * This may be ignored if #getTagName is overridden.
762 *
763 * @static
764 * @inheritable
765 * @property {string}
766 */
767 OO.ui.Element.static.tagName = 'div';
768
769 /* Static Methods */
770
771 /**
772 * Get a jQuery function within a specific document.
773 *
774 * @static
775 * @param {jQuery|HTMLElement|HTMLDocument|Window} context Context to bind the function to
776 * @param {jQuery} [$iframe] HTML iframe element that contains the document, omit if document is
777 * not in an iframe
778 * @return {Function} Bound jQuery function
779 */
780 OO.ui.Element.static.getJQuery = function ( context, $iframe ) {
781 function wrapper( selector ) {
782 return $( selector, wrapper.context );
783 }
784
785 wrapper.context = this.getDocument( context );
786
787 if ( $iframe ) {
788 wrapper.$iframe = $iframe;
789 }
790
791 return wrapper;
792 };
793
794 /**
795 * Get the document of an element.
796 *
797 * @static
798 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Object to get the document for
799 * @return {HTMLDocument|null} Document object
800 */
801 OO.ui.Element.static.getDocument = function ( obj ) {
802 // jQuery - selections created "offscreen" won't have a context, so .context isn't reliable
803 return ( obj[0] && obj[0].ownerDocument ) ||
804 // Empty jQuery selections might have a context
805 obj.context ||
806 // HTMLElement
807 obj.ownerDocument ||
808 // Window
809 obj.document ||
810 // HTMLDocument
811 ( obj.nodeType === 9 && obj ) ||
812 null;
813 };
814
815 /**
816 * Get the window of an element or document.
817 *
818 * @static
819 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the window for
820 * @return {Window} Window object
821 */
822 OO.ui.Element.static.getWindow = function ( obj ) {
823 var doc = this.getDocument( obj );
824 return doc.parentWindow || doc.defaultView;
825 };
826
827 /**
828 * Get the direction of an element or document.
829 *
830 * @static
831 * @param {jQuery|HTMLElement|HTMLDocument|Window} obj Context to get the direction for
832 * @return {string} Text direction, either 'ltr' or 'rtl'
833 */
834 OO.ui.Element.static.getDir = function ( obj ) {
835 var isDoc, isWin;
836
837 if ( obj instanceof jQuery ) {
838 obj = obj[0];
839 }
840 isDoc = obj.nodeType === 9;
841 isWin = obj.document !== undefined;
842 if ( isDoc || isWin ) {
843 if ( isWin ) {
844 obj = obj.document;
845 }
846 obj = obj.body;
847 }
848 return $( obj ).css( 'direction' );
849 };
850
851 /**
852 * Get the offset between two frames.
853 *
854 * TODO: Make this function not use recursion.
855 *
856 * @static
857 * @param {Window} from Window of the child frame
858 * @param {Window} [to=window] Window of the parent frame
859 * @param {Object} [offset] Offset to start with, used internally
860 * @return {Object} Offset object, containing left and top properties
861 */
862 OO.ui.Element.static.getFrameOffset = function ( from, to, offset ) {
863 var i, len, frames, frame, rect;
864
865 if ( !to ) {
866 to = window;
867 }
868 if ( !offset ) {
869 offset = { top: 0, left: 0 };
870 }
871 if ( from.parent === from ) {
872 return offset;
873 }
874
875 // Get iframe element
876 frames = from.parent.document.getElementsByTagName( 'iframe' );
877 for ( i = 0, len = frames.length; i < len; i++ ) {
878 if ( frames[i].contentWindow === from ) {
879 frame = frames[i];
880 break;
881 }
882 }
883
884 // Recursively accumulate offset values
885 if ( frame ) {
886 rect = frame.getBoundingClientRect();
887 offset.left += rect.left;
888 offset.top += rect.top;
889 if ( from !== to ) {
890 this.getFrameOffset( from.parent, offset );
891 }
892 }
893 return offset;
894 };
895
896 /**
897 * Get the offset between two elements.
898 *
899 * The two elements may be in a different frame, but in that case the frame $element is in must
900 * be contained in the frame $anchor is in.
901 *
902 * @static
903 * @param {jQuery} $element Element whose position to get
904 * @param {jQuery} $anchor Element to get $element's position relative to
905 * @return {Object} Translated position coordinates, containing top and left properties
906 */
907 OO.ui.Element.static.getRelativePosition = function ( $element, $anchor ) {
908 var iframe, iframePos,
909 pos = $element.offset(),
910 anchorPos = $anchor.offset(),
911 elementDocument = this.getDocument( $element ),
912 anchorDocument = this.getDocument( $anchor );
913
914 // If $element isn't in the same document as $anchor, traverse up
915 while ( elementDocument !== anchorDocument ) {
916 iframe = elementDocument.defaultView.frameElement;
917 if ( !iframe ) {
918 throw new Error( '$element frame is not contained in $anchor frame' );
919 }
920 iframePos = $( iframe ).offset();
921 pos.left += iframePos.left;
922 pos.top += iframePos.top;
923 elementDocument = iframe.ownerDocument;
924 }
925 pos.left -= anchorPos.left;
926 pos.top -= anchorPos.top;
927 return pos;
928 };
929
930 /**
931 * Get element border sizes.
932 *
933 * @static
934 * @param {HTMLElement} el Element to measure
935 * @return {Object} Dimensions object with `top`, `left`, `bottom` and `right` properties
936 */
937 OO.ui.Element.static.getBorders = function ( el ) {
938 var doc = el.ownerDocument,
939 win = doc.parentWindow || doc.defaultView,
940 style = win && win.getComputedStyle ?
941 win.getComputedStyle( el, null ) :
942 el.currentStyle,
943 $el = $( el ),
944 top = parseFloat( style ? style.borderTopWidth : $el.css( 'borderTopWidth' ) ) || 0,
945 left = parseFloat( style ? style.borderLeftWidth : $el.css( 'borderLeftWidth' ) ) || 0,
946 bottom = parseFloat( style ? style.borderBottomWidth : $el.css( 'borderBottomWidth' ) ) || 0,
947 right = parseFloat( style ? style.borderRightWidth : $el.css( 'borderRightWidth' ) ) || 0;
948
949 return {
950 top: Math.round( top ),
951 left: Math.round( left ),
952 bottom: Math.round( bottom ),
953 right: Math.round( right )
954 };
955 };
956
957 /**
958 * Get dimensions of an element or window.
959 *
960 * @static
961 * @param {HTMLElement|Window} el Element to measure
962 * @return {Object} Dimensions object with `borders`, `scroll`, `scrollbar` and `rect` properties
963 */
964 OO.ui.Element.static.getDimensions = function ( el ) {
965 var $el, $win,
966 doc = el.ownerDocument || el.document,
967 win = doc.parentWindow || doc.defaultView;
968
969 if ( win === el || el === doc.documentElement ) {
970 $win = $( win );
971 return {
972 borders: { top: 0, left: 0, bottom: 0, right: 0 },
973 scroll: {
974 top: $win.scrollTop(),
975 left: $win.scrollLeft()
976 },
977 scrollbar: { right: 0, bottom: 0 },
978 rect: {
979 top: 0,
980 left: 0,
981 bottom: $win.innerHeight(),
982 right: $win.innerWidth()
983 }
984 };
985 } else {
986 $el = $( el );
987 return {
988 borders: this.getBorders( el ),
989 scroll: {
990 top: $el.scrollTop(),
991 left: $el.scrollLeft()
992 },
993 scrollbar: {
994 right: $el.innerWidth() - el.clientWidth,
995 bottom: $el.innerHeight() - el.clientHeight
996 },
997 rect: el.getBoundingClientRect()
998 };
999 }
1000 };
1001
1002 /**
1003 * Get scrollable object parent
1004 *
1005 * documentElement can't be used to get or set the scrollTop
1006 * property on Blink. Changing and testing its value lets us
1007 * use 'body' or 'documentElement' based on what is working.
1008 *
1009 * https://code.google.com/p/chromium/issues/detail?id=303131
1010 *
1011 * @static
1012 * @param {HTMLElement} el Element to find scrollable parent for
1013 * @return {HTMLElement} Scrollable parent
1014 */
1015 OO.ui.Element.static.getRootScrollableElement = function ( el ) {
1016 var scrollTop, body;
1017
1018 if ( OO.ui.scrollableElement === undefined ) {
1019 body = el.ownerDocument.body;
1020 scrollTop = body.scrollTop;
1021 body.scrollTop = 1;
1022
1023 if ( body.scrollTop === 1 ) {
1024 body.scrollTop = scrollTop;
1025 OO.ui.scrollableElement = 'body';
1026 } else {
1027 OO.ui.scrollableElement = 'documentElement';
1028 }
1029 }
1030
1031 return el.ownerDocument[ OO.ui.scrollableElement ];
1032 };
1033
1034 /**
1035 * Get closest scrollable container.
1036 *
1037 * Traverses up until either a scrollable element or the root is reached, in which case the window
1038 * will be returned.
1039 *
1040 * @static
1041 * @param {HTMLElement} el Element to find scrollable container for
1042 * @param {string} [dimension] Dimension of scrolling to look for; `x`, `y` or omit for either
1043 * @return {HTMLElement} Closest scrollable container
1044 */
1045 OO.ui.Element.static.getClosestScrollableContainer = function ( el, dimension ) {
1046 var i, val,
1047 props = [ 'overflow' ],
1048 $parent = $( el ).parent();
1049
1050 if ( dimension === 'x' || dimension === 'y' ) {
1051 props.push( 'overflow-' + dimension );
1052 }
1053
1054 while ( $parent.length ) {
1055 if ( $parent[0] === this.getRootScrollableElement( el ) ) {
1056 return $parent[0];
1057 }
1058 i = props.length;
1059 while ( i-- ) {
1060 val = $parent.css( props[i] );
1061 if ( val === 'auto' || val === 'scroll' ) {
1062 return $parent[0];
1063 }
1064 }
1065 $parent = $parent.parent();
1066 }
1067 return this.getDocument( el ).body;
1068 };
1069
1070 /**
1071 * Scroll element into view.
1072 *
1073 * @static
1074 * @param {HTMLElement} el Element to scroll into view
1075 * @param {Object} [config] Configuration options
1076 * @param {string} [config.duration] jQuery animation duration value
1077 * @param {string} [config.direction] Scroll in only one direction, e.g. 'x' or 'y', omit
1078 * to scroll in both directions
1079 * @param {Function} [config.complete] Function to call when scrolling completes
1080 */
1081 OO.ui.Element.static.scrollIntoView = function ( el, config ) {
1082 // Configuration initialization
1083 config = config || {};
1084
1085 var rel, anim = {},
1086 callback = typeof config.complete === 'function' && config.complete,
1087 sc = this.getClosestScrollableContainer( el, config.direction ),
1088 $sc = $( sc ),
1089 eld = this.getDimensions( el ),
1090 scd = this.getDimensions( sc ),
1091 $win = $( this.getWindow( el ) );
1092
1093 // Compute the distances between the edges of el and the edges of the scroll viewport
1094 if ( $sc.is( 'html, body' ) ) {
1095 // If the scrollable container is the root, this is easy
1096 rel = {
1097 top: eld.rect.top,
1098 bottom: $win.innerHeight() - eld.rect.bottom,
1099 left: eld.rect.left,
1100 right: $win.innerWidth() - eld.rect.right
1101 };
1102 } else {
1103 // Otherwise, we have to subtract el's coordinates from sc's coordinates
1104 rel = {
1105 top: eld.rect.top - ( scd.rect.top + scd.borders.top ),
1106 bottom: scd.rect.bottom - scd.borders.bottom - scd.scrollbar.bottom - eld.rect.bottom,
1107 left: eld.rect.left - ( scd.rect.left + scd.borders.left ),
1108 right: scd.rect.right - scd.borders.right - scd.scrollbar.right - eld.rect.right
1109 };
1110 }
1111
1112 if ( !config.direction || config.direction === 'y' ) {
1113 if ( rel.top < 0 ) {
1114 anim.scrollTop = scd.scroll.top + rel.top;
1115 } else if ( rel.top > 0 && rel.bottom < 0 ) {
1116 anim.scrollTop = scd.scroll.top + Math.min( rel.top, -rel.bottom );
1117 }
1118 }
1119 if ( !config.direction || config.direction === 'x' ) {
1120 if ( rel.left < 0 ) {
1121 anim.scrollLeft = scd.scroll.left + rel.left;
1122 } else if ( rel.left > 0 && rel.right < 0 ) {
1123 anim.scrollLeft = scd.scroll.left + Math.min( rel.left, -rel.right );
1124 }
1125 }
1126 if ( !$.isEmptyObject( anim ) ) {
1127 $sc.stop( true ).animate( anim, config.duration || 'fast' );
1128 if ( callback ) {
1129 $sc.queue( function ( next ) {
1130 callback();
1131 next();
1132 } );
1133 }
1134 } else {
1135 if ( callback ) {
1136 callback();
1137 }
1138 }
1139 };
1140
1141 /* Methods */
1142
1143 /**
1144 * Get element data.
1145 *
1146 * @return {Mixed} Element data
1147 */
1148 OO.ui.Element.prototype.getData = function () {
1149 return this.data;
1150 };
1151
1152 /**
1153 * Set element data.
1154 *
1155 * @param {Mixed} Element data
1156 * @chainable
1157 */
1158 OO.ui.Element.prototype.setData = function ( data ) {
1159 this.data = data;
1160 return this;
1161 };
1162
1163 /**
1164 * Check if element supports one or more methods.
1165 *
1166 * @param {string|string[]} methods Method or list of methods to check
1167 * @return {boolean} All methods are supported
1168 */
1169 OO.ui.Element.prototype.supports = function ( methods ) {
1170 var i, len,
1171 support = 0;
1172
1173 methods = $.isArray( methods ) ? methods : [ methods ];
1174 for ( i = 0, len = methods.length; i < len; i++ ) {
1175 if ( $.isFunction( this[methods[i]] ) ) {
1176 support++;
1177 }
1178 }
1179
1180 return methods.length === support;
1181 };
1182
1183 /**
1184 * Update the theme-provided classes.
1185 *
1186 * @localdoc This is called in element mixins and widget classes any time state changes.
1187 * Updating is debounced, minimizing overhead of changing multiple attributes and
1188 * guaranteeing that theme updates do not occur within an element's constructor
1189 */
1190 OO.ui.Element.prototype.updateThemeClasses = function () {
1191 if ( !this.updateThemeClassesPending ) {
1192 this.updateThemeClassesPending = true;
1193 setTimeout( this.debouncedUpdateThemeClassesHandler );
1194 }
1195 };
1196
1197 /**
1198 * @private
1199 */
1200 OO.ui.Element.prototype.debouncedUpdateThemeClasses = function () {
1201 OO.ui.theme.updateElementClasses( this );
1202 this.updateThemeClassesPending = false;
1203 };
1204
1205 /**
1206 * Get the HTML tag name.
1207 *
1208 * Override this method to base the result on instance information.
1209 *
1210 * @return {string} HTML tag name
1211 */
1212 OO.ui.Element.prototype.getTagName = function () {
1213 return this.constructor.static.tagName;
1214 };
1215
1216 /**
1217 * Check if the element is attached to the DOM
1218 * @return {boolean} The element is attached to the DOM
1219 */
1220 OO.ui.Element.prototype.isElementAttached = function () {
1221 return $.contains( this.getElementDocument(), this.$element[0] );
1222 };
1223
1224 /**
1225 * Get the DOM document.
1226 *
1227 * @return {HTMLDocument} Document object
1228 */
1229 OO.ui.Element.prototype.getElementDocument = function () {
1230 // Don't use this.$.context because subclasses can rebind this.$
1231 // Don't cache this in other ways either because subclasses could can change this.$element
1232 return OO.ui.Element.static.getDocument( this.$element );
1233 };
1234
1235 /**
1236 * Get the DOM window.
1237 *
1238 * @return {Window} Window object
1239 */
1240 OO.ui.Element.prototype.getElementWindow = function () {
1241 return OO.ui.Element.static.getWindow( this.$element );
1242 };
1243
1244 /**
1245 * Get closest scrollable container.
1246 */
1247 OO.ui.Element.prototype.getClosestScrollableElementContainer = function () {
1248 return OO.ui.Element.static.getClosestScrollableContainer( this.$element[0] );
1249 };
1250
1251 /**
1252 * Get group element is in.
1253 *
1254 * @return {OO.ui.GroupElement|null} Group element, null if none
1255 */
1256 OO.ui.Element.prototype.getElementGroup = function () {
1257 return this.elementGroup;
1258 };
1259
1260 /**
1261 * Set group element is in.
1262 *
1263 * @param {OO.ui.GroupElement|null} group Group element, null if none
1264 * @chainable
1265 */
1266 OO.ui.Element.prototype.setElementGroup = function ( group ) {
1267 this.elementGroup = group;
1268 return this;
1269 };
1270
1271 /**
1272 * Scroll element into view.
1273 *
1274 * @param {Object} [config] Configuration options
1275 */
1276 OO.ui.Element.prototype.scrollElementIntoView = function ( config ) {
1277 return OO.ui.Element.static.scrollIntoView( this.$element[0], config );
1278 };
1279
1280 /**
1281 * Container for elements.
1282 *
1283 * @abstract
1284 * @class
1285 * @extends OO.ui.Element
1286 * @mixins OO.EventEmitter
1287 *
1288 * @constructor
1289 * @param {Object} [config] Configuration options
1290 */
1291 OO.ui.Layout = function OoUiLayout( config ) {
1292 // Configuration initialization
1293 config = config || {};
1294
1295 // Parent constructor
1296 OO.ui.Layout.super.call( this, config );
1297
1298 // Mixin constructors
1299 OO.EventEmitter.call( this );
1300
1301 // Initialization
1302 this.$element.addClass( 'oo-ui-layout' );
1303 };
1304
1305 /* Setup */
1306
1307 OO.inheritClass( OO.ui.Layout, OO.ui.Element );
1308 OO.mixinClass( OO.ui.Layout, OO.EventEmitter );
1309
1310 /**
1311 * User interface control.
1312 *
1313 * @abstract
1314 * @class
1315 * @extends OO.ui.Element
1316 * @mixins OO.EventEmitter
1317 *
1318 * @constructor
1319 * @param {Object} [config] Configuration options
1320 * @cfg {boolean} [disabled=false] Disable
1321 */
1322 OO.ui.Widget = function OoUiWidget( config ) {
1323 // Initialize config
1324 config = $.extend( { disabled: false }, config );
1325
1326 // Parent constructor
1327 OO.ui.Widget.super.call( this, config );
1328
1329 // Mixin constructors
1330 OO.EventEmitter.call( this );
1331
1332 // Properties
1333 this.visible = true;
1334 this.disabled = null;
1335 this.wasDisabled = null;
1336
1337 // Initialization
1338 this.$element.addClass( 'oo-ui-widget' );
1339 this.setDisabled( !!config.disabled );
1340 };
1341
1342 /* Setup */
1343
1344 OO.inheritClass( OO.ui.Widget, OO.ui.Element );
1345 OO.mixinClass( OO.ui.Widget, OO.EventEmitter );
1346
1347 /* Events */
1348
1349 /**
1350 * @event disable
1351 * @param {boolean} disabled Widget is disabled
1352 */
1353
1354 /**
1355 * @event toggle
1356 * @param {boolean} visible Widget is visible
1357 */
1358
1359 /* Methods */
1360
1361 /**
1362 * Check if the widget is disabled.
1363 *
1364 * @return {boolean} Button is disabled
1365 */
1366 OO.ui.Widget.prototype.isDisabled = function () {
1367 return this.disabled;
1368 };
1369
1370 /**
1371 * Check if widget is visible.
1372 *
1373 * @return {boolean} Widget is visible
1374 */
1375 OO.ui.Widget.prototype.isVisible = function () {
1376 return this.visible;
1377 };
1378
1379 /**
1380 * Set the disabled state of the widget.
1381 *
1382 * This should probably change the widgets' appearance and prevent it from being used.
1383 *
1384 * @param {boolean} disabled Disable widget
1385 * @chainable
1386 */
1387 OO.ui.Widget.prototype.setDisabled = function ( disabled ) {
1388 var isDisabled;
1389
1390 this.disabled = !!disabled;
1391 isDisabled = this.isDisabled();
1392 if ( isDisabled !== this.wasDisabled ) {
1393 this.$element.toggleClass( 'oo-ui-widget-disabled', isDisabled );
1394 this.$element.toggleClass( 'oo-ui-widget-enabled', !isDisabled );
1395 this.emit( 'disable', isDisabled );
1396 this.updateThemeClasses();
1397 }
1398 this.wasDisabled = isDisabled;
1399
1400 return this;
1401 };
1402
1403 /**
1404 * Toggle visibility of widget.
1405 *
1406 * @param {boolean} [show] Make widget visible, omit to toggle visibility
1407 * @fires visible
1408 * @chainable
1409 */
1410 OO.ui.Widget.prototype.toggle = function ( show ) {
1411 show = show === undefined ? !this.visible : !!show;
1412
1413 if ( show !== this.isVisible() ) {
1414 this.visible = show;
1415 this.$element.toggle( show );
1416 this.emit( 'toggle', show );
1417 }
1418
1419 return this;
1420 };
1421
1422 /**
1423 * Update the disabled state, in case of changes in parent widget.
1424 *
1425 * @chainable
1426 */
1427 OO.ui.Widget.prototype.updateDisabled = function () {
1428 this.setDisabled( this.disabled );
1429 return this;
1430 };
1431
1432 /**
1433 * Container for elements in a child frame.
1434 *
1435 * Use together with OO.ui.WindowManager.
1436 *
1437 * @abstract
1438 * @class
1439 * @extends OO.ui.Element
1440 * @mixins OO.EventEmitter
1441 *
1442 * When a window is opened, the setup and ready processes are executed. Similarly, the hold and
1443 * teardown processes are executed when the window is closed.
1444 *
1445 * - {@link OO.ui.WindowManager#openWindow} or {@link #open} methods are used to start opening
1446 * - Window manager begins opening window
1447 * - {@link #getSetupProcess} method is called and its result executed
1448 * - {@link #getReadyProcess} method is called and its result executed
1449 * - Window is now open
1450 *
1451 * - {@link OO.ui.WindowManager#closeWindow} or {@link #close} methods are used to start closing
1452 * - Window manager begins closing window
1453 * - {@link #getHoldProcess} method is called and its result executed
1454 * - {@link #getTeardownProcess} method is called and its result executed
1455 * - Window is now closed
1456 *
1457 * Each process (setup, ready, hold and teardown) can be extended in subclasses by overriding
1458 * {@link #getSetupProcess}, {@link #getReadyProcess}, {@link #getHoldProcess} and
1459 * {@link #getTeardownProcess} respectively. Each process is executed in series, so asynchronous
1460 * processing can complete. Always assume window processes are executed asynchronously. See
1461 * OO.ui.Process for more details about how to work with processes. Some events, as well as the
1462 * #open and #close methods, provide promises which are resolved when the window enters a new state.
1463 *
1464 * Sizing of windows is specified using symbolic names which are interpreted by the window manager.
1465 * If the requested size is not recognized, the window manager will choose a sensible fallback.
1466 *
1467 * @constructor
1468 * @param {Object} [config] Configuration options
1469 * @cfg {string} [size] Symbolic name of dialog size, `small`, `medium`, `large` or `full`; omit to
1470 * use #static-size
1471 */
1472 OO.ui.Window = function OoUiWindow( config ) {
1473 // Configuration initialization
1474 config = config || {};
1475
1476 // Parent constructor
1477 OO.ui.Window.super.call( this, config );
1478
1479 // Mixin constructors
1480 OO.EventEmitter.call( this );
1481
1482 // Properties
1483 this.manager = null;
1484 this.initialized = false;
1485 this.visible = false;
1486 this.opening = null;
1487 this.closing = null;
1488 this.opened = null;
1489 this.timing = null;
1490 this.loading = null;
1491 this.size = config.size || this.constructor.static.size;
1492 this.$frame = this.$( '<div>' );
1493 this.$overlay = this.$( '<div>' );
1494
1495 // Initialization
1496 this.$element
1497 .addClass( 'oo-ui-window' )
1498 .append( this.$frame, this.$overlay );
1499 this.$frame.addClass( 'oo-ui-window-frame' );
1500 this.$overlay.addClass( 'oo-ui-window-overlay' );
1501
1502 // NOTE: Additional initialization will occur when #setManager is called
1503 };
1504
1505 /* Setup */
1506
1507 OO.inheritClass( OO.ui.Window, OO.ui.Element );
1508 OO.mixinClass( OO.ui.Window, OO.EventEmitter );
1509
1510 /* Static Properties */
1511
1512 /**
1513 * Symbolic name of size.
1514 *
1515 * Size is used if no size is configured during construction.
1516 *
1517 * @static
1518 * @inheritable
1519 * @property {string}
1520 */
1521 OO.ui.Window.static.size = 'medium';
1522
1523 /* Static Methods */
1524
1525 /**
1526 * Transplant the CSS styles from as parent document to a frame's document.
1527 *
1528 * This loops over the style sheets in the parent document, and copies their nodes to the
1529 * frame's document. It then polls the document to see when all styles have loaded, and once they
1530 * have, resolves the promise.
1531 *
1532 * If the styles still haven't loaded after a long time (5 seconds by default), we give up waiting
1533 * and resolve the promise anyway. This protects against cases like a display: none; iframe in
1534 * Firefox, where the styles won't load until the iframe becomes visible.
1535 *
1536 * For details of how we arrived at the strategy used in this function, see #load.
1537 *
1538 * @static
1539 * @inheritable
1540 * @param {HTMLDocument} parentDoc Document to transplant styles from
1541 * @param {HTMLDocument} frameDoc Document to transplant styles to
1542 * @param {number} [timeout=5000] How long to wait before giving up (in ms). If 0, never give up.
1543 * @return {jQuery.Promise} Promise resolved when styles have loaded
1544 */
1545 OO.ui.Window.static.transplantStyles = function ( parentDoc, frameDoc, timeout ) {
1546 var i, numSheets, styleNode, styleText, newNode, timeoutID, pollNodeId, $pendingPollNodes,
1547 $pollNodes = $( [] ),
1548 // Fake font-family value
1549 fontFamily = 'oo-ui-frame-transplantStyles-loaded',
1550 nextIndex = parentDoc.oouiFrameTransplantStylesNextIndex || 0,
1551 deferred = $.Deferred();
1552
1553 for ( i = 0, numSheets = parentDoc.styleSheets.length; i < numSheets; i++ ) {
1554 styleNode = parentDoc.styleSheets[i].ownerNode;
1555 if ( styleNode.disabled ) {
1556 continue;
1557 }
1558
1559 if ( styleNode.nodeName.toLowerCase() === 'link' ) {
1560 // External stylesheet; use @import
1561 styleText = '@import url(' + styleNode.href + ');';
1562 } else {
1563 // Internal stylesheet; just copy the text
1564 // For IE10 we need to fall back to .cssText, BUT that's undefined in
1565 // other browsers, so fall back to '' rather than 'undefined'
1566 styleText = styleNode.textContent || parentDoc.styleSheets[i].cssText || '';
1567 }
1568
1569 // Create a node with a unique ID that we're going to monitor to see when the CSS
1570 // has loaded
1571 if ( styleNode.oouiFrameTransplantStylesId ) {
1572 // If we're nesting transplantStyles operations and this node already has
1573 // a CSS rule to wait for loading, reuse it
1574 pollNodeId = styleNode.oouiFrameTransplantStylesId;
1575 } else {
1576 // Otherwise, create a new ID
1577 pollNodeId = 'oo-ui-frame-transplantStyles-loaded-' + nextIndex;
1578 nextIndex++;
1579
1580 // Add #pollNodeId { font-family: ... } to the end of the stylesheet / after the @import
1581 // The font-family rule will only take effect once the @import finishes
1582 styleText += '\n' + '#' + pollNodeId + ' { font-family: ' + fontFamily + '; }';
1583 }
1584
1585 // Create a node with id=pollNodeId
1586 $pollNodes = $pollNodes.add( $( '<div>', frameDoc )
1587 .attr( 'id', pollNodeId )
1588 .appendTo( frameDoc.body )
1589 );
1590
1591 // Add our modified CSS as a <style> tag
1592 newNode = frameDoc.createElement( 'style' );
1593 newNode.textContent = styleText;
1594 newNode.oouiFrameTransplantStylesId = pollNodeId;
1595 frameDoc.head.appendChild( newNode );
1596 }
1597 frameDoc.oouiFrameTransplantStylesNextIndex = nextIndex;
1598
1599 // Poll every 100ms until all external stylesheets have loaded
1600 $pendingPollNodes = $pollNodes;
1601 timeoutID = setTimeout( function pollExternalStylesheets() {
1602 while (
1603 $pendingPollNodes.length > 0 &&
1604 $pendingPollNodes.eq( 0 ).css( 'font-family' ) === fontFamily
1605 ) {
1606 $pendingPollNodes = $pendingPollNodes.slice( 1 );
1607 }
1608
1609 if ( $pendingPollNodes.length === 0 ) {
1610 // We're done!
1611 if ( timeoutID !== null ) {
1612 timeoutID = null;
1613 $pollNodes.remove();
1614 deferred.resolve();
1615 }
1616 } else {
1617 timeoutID = setTimeout( pollExternalStylesheets, 100 );
1618 }
1619 }, 100 );
1620 // ...but give up after a while
1621 if ( timeout !== 0 ) {
1622 setTimeout( function () {
1623 if ( timeoutID ) {
1624 clearTimeout( timeoutID );
1625 timeoutID = null;
1626 $pollNodes.remove();
1627 deferred.reject();
1628 }
1629 }, timeout || 5000 );
1630 }
1631
1632 return deferred.promise();
1633 };
1634
1635 /* Methods */
1636
1637 /**
1638 * Handle mouse down events.
1639 *
1640 * @param {jQuery.Event} e Mouse down event
1641 */
1642 OO.ui.Window.prototype.onMouseDown = function ( e ) {
1643 // Prevent clicking on the click-block from stealing focus
1644 if ( e.target === this.$element[0] ) {
1645 return false;
1646 }
1647 };
1648
1649 /**
1650 * Check if window has been initialized.
1651 *
1652 * @return {boolean} Window has been initialized
1653 */
1654 OO.ui.Window.prototype.isInitialized = function () {
1655 return this.initialized;
1656 };
1657
1658 /**
1659 * Check if window is visible.
1660 *
1661 * @return {boolean} Window is visible
1662 */
1663 OO.ui.Window.prototype.isVisible = function () {
1664 return this.visible;
1665 };
1666
1667 /**
1668 * Check if window is loading.
1669 *
1670 * @return {boolean} Window is loading
1671 */
1672 OO.ui.Window.prototype.isLoading = function () {
1673 return this.loading && this.loading.state() === 'pending';
1674 };
1675
1676 /**
1677 * Check if window is loaded.
1678 *
1679 * @return {boolean} Window is loaded
1680 */
1681 OO.ui.Window.prototype.isLoaded = function () {
1682 return this.loading && this.loading.state() === 'resolved';
1683 };
1684
1685 /**
1686 * Check if window is opening.
1687 *
1688 * This is a wrapper around OO.ui.WindowManager#isOpening.
1689 *
1690 * @return {boolean} Window is opening
1691 */
1692 OO.ui.Window.prototype.isOpening = function () {
1693 return this.manager.isOpening( this );
1694 };
1695
1696 /**
1697 * Check if window is closing.
1698 *
1699 * This is a wrapper around OO.ui.WindowManager#isClosing.
1700 *
1701 * @return {boolean} Window is closing
1702 */
1703 OO.ui.Window.prototype.isClosing = function () {
1704 return this.manager.isClosing( this );
1705 };
1706
1707 /**
1708 * Check if window is opened.
1709 *
1710 * This is a wrapper around OO.ui.WindowManager#isOpened.
1711 *
1712 * @return {boolean} Window is opened
1713 */
1714 OO.ui.Window.prototype.isOpened = function () {
1715 return this.manager.isOpened( this );
1716 };
1717
1718 /**
1719 * Get the window manager.
1720 *
1721 * @return {OO.ui.WindowManager} Manager of window
1722 */
1723 OO.ui.Window.prototype.getManager = function () {
1724 return this.manager;
1725 };
1726
1727 /**
1728 * Get the window size.
1729 *
1730 * @return {string} Symbolic size name, e.g. 'small', 'medium', 'large', 'full'
1731 */
1732 OO.ui.Window.prototype.getSize = function () {
1733 return this.size;
1734 };
1735
1736 /**
1737 * Disable transitions on window's frame for the duration of the callback function, then enable them
1738 * back.
1739 *
1740 * @private
1741 * @param {Function} callback Function to call while transitions are disabled
1742 */
1743 OO.ui.Window.prototype.withoutSizeTransitions = function ( callback ) {
1744 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
1745 // Disable transitions first, otherwise we'll get values from when the window was animating.
1746 var oldTransition,
1747 styleObj = this.$frame[0].style;
1748 oldTransition = styleObj.transition || styleObj.OTransition || styleObj.MsTransition ||
1749 styleObj.MozTransition || styleObj.WebkitTransition;
1750 styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
1751 styleObj.MozTransition = styleObj.WebkitTransition = 'none';
1752 callback();
1753 // Force reflow to make sure the style changes done inside callback really are not transitioned
1754 this.$frame.height();
1755 styleObj.transition = styleObj.OTransition = styleObj.MsTransition =
1756 styleObj.MozTransition = styleObj.WebkitTransition = oldTransition;
1757 };
1758
1759 /**
1760 * Get the height of the dialog contents.
1761 *
1762 * @return {number} Content height
1763 */
1764 OO.ui.Window.prototype.getContentHeight = function () {
1765 var bodyHeight,
1766 win = this,
1767 bodyStyleObj = this.$body[0].style,
1768 frameStyleObj = this.$frame[0].style;
1769
1770 // Temporarily resize the frame so getBodyHeight() can use scrollHeight measurements.
1771 // Disable transitions first, otherwise we'll get values from when the window was animating.
1772 this.withoutSizeTransitions( function () {
1773 var oldHeight = frameStyleObj.height, oldPosition = bodyStyleObj.position;
1774 frameStyleObj.height = '1px';
1775 // Force body to resize to new width
1776 bodyStyleObj.position = 'relative';
1777 bodyHeight = win.getBodyHeight();
1778 frameStyleObj.height = oldHeight;
1779 bodyStyleObj.position = oldPosition;
1780 } );
1781
1782 return Math.round(
1783 // Add buffer for border
1784 ( this.$frame.outerHeight() - this.$frame.innerHeight() ) +
1785 // Use combined heights of children
1786 ( this.$head.outerHeight( true ) + bodyHeight + this.$foot.outerHeight( true ) )
1787 );
1788 };
1789
1790 /**
1791 * Get the height of the dialog contents.
1792 *
1793 * When this function is called, the dialog will temporarily have been resized
1794 * to height=1px, so .scrollHeight measurements can be taken accurately.
1795 *
1796 * @return {number} Height of content
1797 */
1798 OO.ui.Window.prototype.getBodyHeight = function () {
1799 return this.$body[0].scrollHeight;
1800 };
1801
1802 /**
1803 * Get the directionality of the frame
1804 *
1805 * @return {string} Directionality, 'ltr' or 'rtl'
1806 */
1807 OO.ui.Window.prototype.getDir = function () {
1808 return this.dir;
1809 };
1810
1811 /**
1812 * Get a process for setting up a window for use.
1813 *
1814 * Each time the window is opened this process will set it up for use in a particular context, based
1815 * on the `data` argument.
1816 *
1817 * When you override this method, you can add additional setup steps to the process the parent
1818 * method provides using the 'first' and 'next' methods.
1819 *
1820 * @abstract
1821 * @param {Object} [data] Window opening data
1822 * @return {OO.ui.Process} Setup process
1823 */
1824 OO.ui.Window.prototype.getSetupProcess = function () {
1825 return new OO.ui.Process();
1826 };
1827
1828 /**
1829 * Get a process for readying a window for use.
1830 *
1831 * Each time the window is open and setup, this process will ready it up for use in a particular
1832 * context, based on the `data` argument.
1833 *
1834 * When you override this method, you can add additional setup steps to the process the parent
1835 * method provides using the 'first' and 'next' methods.
1836 *
1837 * @abstract
1838 * @param {Object} [data] Window opening data
1839 * @return {OO.ui.Process} Setup process
1840 */
1841 OO.ui.Window.prototype.getReadyProcess = function () {
1842 return new OO.ui.Process();
1843 };
1844
1845 /**
1846 * Get a process for holding a window from use.
1847 *
1848 * Each time the window is closed, this process will hold it from use in a particular context, based
1849 * on the `data` argument.
1850 *
1851 * When you override this method, you can add additional setup steps to the process the parent
1852 * method provides using the 'first' and 'next' methods.
1853 *
1854 * @abstract
1855 * @param {Object} [data] Window closing data
1856 * @return {OO.ui.Process} Hold process
1857 */
1858 OO.ui.Window.prototype.getHoldProcess = function () {
1859 return new OO.ui.Process();
1860 };
1861
1862 /**
1863 * Get a process for tearing down a window after use.
1864 *
1865 * Each time the window is closed this process will tear it down and do something with the user's
1866 * interactions within the window, based on the `data` argument.
1867 *
1868 * When you override this method, you can add additional teardown steps to the process the parent
1869 * method provides using the 'first' and 'next' methods.
1870 *
1871 * @abstract
1872 * @param {Object} [data] Window closing data
1873 * @return {OO.ui.Process} Teardown process
1874 */
1875 OO.ui.Window.prototype.getTeardownProcess = function () {
1876 return new OO.ui.Process();
1877 };
1878
1879 /**
1880 * Toggle visibility of window.
1881 *
1882 * If the window is isolated and hasn't fully loaded yet, the visibility property will be used
1883 * instead of display.
1884 *
1885 * @param {boolean} [show] Make window visible, omit to toggle visibility
1886 * @fires toggle
1887 * @chainable
1888 */
1889 OO.ui.Window.prototype.toggle = function ( show ) {
1890 show = show === undefined ? !this.visible : !!show;
1891
1892 if ( show !== this.isVisible() ) {
1893 this.visible = show;
1894
1895 if ( this.isolated && !this.isLoaded() ) {
1896 // Hide the window using visibility instead of display until loading is complete
1897 // Can't use display: none; because that prevents the iframe from loading in Firefox
1898 this.$element.css( 'visibility', show ? 'visible' : 'hidden' );
1899 } else {
1900 this.$element.toggle( show ).css( 'visibility', '' );
1901 }
1902 this.emit( 'toggle', show );
1903 }
1904
1905 return this;
1906 };
1907
1908 /**
1909 * Set the window manager.
1910 *
1911 * This must be called before initialize. Calling it more than once will cause an error.
1912 *
1913 * @param {OO.ui.WindowManager} manager Manager for this window
1914 * @throws {Error} If called more than once
1915 * @chainable
1916 */
1917 OO.ui.Window.prototype.setManager = function ( manager ) {
1918 if ( this.manager ) {
1919 throw new Error( 'Cannot set window manager, window already has a manager' );
1920 }
1921
1922 // Properties
1923 this.manager = manager;
1924 this.isolated = manager.shouldIsolate();
1925
1926 // Initialization
1927 if ( this.isolated ) {
1928 this.$iframe = this.$( '<iframe>' );
1929 this.$iframe.attr( { frameborder: 0, scrolling: 'no' } );
1930 this.$frame.append( this.$iframe );
1931 this.$ = function () {
1932 throw new Error( 'this.$() cannot be used until the frame has been initialized.' );
1933 };
1934 // WARNING: Do not use this.$ again until #initialize is called
1935 } else {
1936 this.$content = this.$( '<div>' );
1937 this.$document = $( this.getElementDocument() );
1938 this.$content.addClass( 'oo-ui-window-content' ).attr( 'tabIndex', 0 );
1939 this.$frame.append( this.$content );
1940 }
1941 this.toggle( false );
1942
1943 // Figure out directionality:
1944 this.dir = OO.ui.Element.static.getDir( this.$iframe || this.$content ) || 'ltr';
1945
1946 return this;
1947 };
1948
1949 /**
1950 * Set the window size.
1951 *
1952 * @param {string} size Symbolic size name, e.g. 'small', 'medium', 'large', 'full'
1953 * @chainable
1954 */
1955 OO.ui.Window.prototype.setSize = function ( size ) {
1956 this.size = size;
1957 this.manager.updateWindowSize( this );
1958 return this;
1959 };
1960
1961 /**
1962 * Set window dimensions.
1963 *
1964 * Properties are applied to the frame container.
1965 *
1966 * @param {Object} dim CSS dimension properties
1967 * @param {string|number} [dim.width] Width
1968 * @param {string|number} [dim.minWidth] Minimum width
1969 * @param {string|number} [dim.maxWidth] Maximum width
1970 * @param {string|number} [dim.width] Height, omit to set based on height of contents
1971 * @param {string|number} [dim.minWidth] Minimum height
1972 * @param {string|number} [dim.maxWidth] Maximum height
1973 * @chainable
1974 */
1975 OO.ui.Window.prototype.setDimensions = function ( dim ) {
1976 var height,
1977 win = this,
1978 styleObj = this.$frame[0].style;
1979
1980 // Calculate the height we need to set using the correct width
1981 if ( dim.height === undefined ) {
1982 this.withoutSizeTransitions( function () {
1983 var oldWidth = styleObj.width;
1984 win.$frame.css( 'width', dim.width || '' );
1985 height = win.getContentHeight();
1986 styleObj.width = oldWidth;
1987 } );
1988 } else {
1989 height = dim.height;
1990 }
1991
1992 this.$frame.css( {
1993 width: dim.width || '',
1994 minWidth: dim.minWidth || '',
1995 maxWidth: dim.maxWidth || '',
1996 height: height || '',
1997 minHeight: dim.minHeight || '',
1998 maxHeight: dim.maxHeight || ''
1999 } );
2000
2001 return this;
2002 };
2003
2004 /**
2005 * Initialize window contents.
2006 *
2007 * The first time the window is opened, #initialize is called when it's safe to begin populating
2008 * its contents. See #getSetupProcess for a way to make changes each time the window opens.
2009 *
2010 * Once this method is called, this.$ can be used to create elements within the frame.
2011 *
2012 * @throws {Error} If not attached to a manager
2013 * @chainable
2014 */
2015 OO.ui.Window.prototype.initialize = function () {
2016 if ( !this.manager ) {
2017 throw new Error( 'Cannot initialize window, must be attached to a manager' );
2018 }
2019
2020 // Properties
2021 this.$head = this.$( '<div>' );
2022 this.$body = this.$( '<div>' );
2023 this.$foot = this.$( '<div>' );
2024 this.$innerOverlay = this.$( '<div>' );
2025
2026 // Events
2027 this.$element.on( 'mousedown', this.onMouseDown.bind( this ) );
2028
2029 // Initialization
2030 this.$head.addClass( 'oo-ui-window-head' );
2031 this.$body.addClass( 'oo-ui-window-body' );
2032 this.$foot.addClass( 'oo-ui-window-foot' );
2033 this.$innerOverlay.addClass( 'oo-ui-window-inner-overlay' );
2034 this.$content.append( this.$head, this.$body, this.$foot, this.$innerOverlay );
2035
2036 return this;
2037 };
2038
2039 /**
2040 * Open window.
2041 *
2042 * This is a wrapper around calling {@link OO.ui.WindowManager#openWindow} on the window manager.
2043 * To do something each time the window opens, use #getSetupProcess or #getReadyProcess.
2044 *
2045 * @param {Object} [data] Window opening data
2046 * @return {jQuery.Promise} Promise resolved when window is opened; when the promise is resolved the
2047 * first argument will be a promise which will be resolved when the window begins closing
2048 */
2049 OO.ui.Window.prototype.open = function ( data ) {
2050 return this.manager.openWindow( this, data );
2051 };
2052
2053 /**
2054 * Close window.
2055 *
2056 * This is a wrapper around calling OO.ui.WindowManager#closeWindow on the window manager.
2057 * To do something each time the window closes, use #getHoldProcess or #getTeardownProcess.
2058 *
2059 * @param {Object} [data] Window closing data
2060 * @return {jQuery.Promise} Promise resolved when window is closed
2061 */
2062 OO.ui.Window.prototype.close = function ( data ) {
2063 return this.manager.closeWindow( this, data );
2064 };
2065
2066 /**
2067 * Setup window.
2068 *
2069 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2070 * by other systems.
2071 *
2072 * @param {Object} [data] Window opening data
2073 * @return {jQuery.Promise} Promise resolved when window is setup
2074 */
2075 OO.ui.Window.prototype.setup = function ( data ) {
2076 var win = this,
2077 deferred = $.Deferred();
2078
2079 this.$element.show();
2080 this.visible = true;
2081 this.getSetupProcess( data ).execute().done( function () {
2082 // Force redraw by asking the browser to measure the elements' widths
2083 win.$element.addClass( 'oo-ui-window-setup' ).width();
2084 win.$content.addClass( 'oo-ui-window-content-setup' ).width();
2085 deferred.resolve();
2086 } );
2087
2088 return deferred.promise();
2089 };
2090
2091 /**
2092 * Ready window.
2093 *
2094 * This is called by OO.ui.WindowManager during window opening, and should not be called directly
2095 * by other systems.
2096 *
2097 * @param {Object} [data] Window opening data
2098 * @return {jQuery.Promise} Promise resolved when window is ready
2099 */
2100 OO.ui.Window.prototype.ready = function ( data ) {
2101 var win = this,
2102 deferred = $.Deferred();
2103
2104 this.$content.focus();
2105 this.getReadyProcess( data ).execute().done( function () {
2106 // Force redraw by asking the browser to measure the elements' widths
2107 win.$element.addClass( 'oo-ui-window-ready' ).width();
2108 win.$content.addClass( 'oo-ui-window-content-ready' ).width();
2109 deferred.resolve();
2110 } );
2111
2112 return deferred.promise();
2113 };
2114
2115 /**
2116 * Hold window.
2117 *
2118 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2119 * by other systems.
2120 *
2121 * @param {Object} [data] Window closing data
2122 * @return {jQuery.Promise} Promise resolved when window is held
2123 */
2124 OO.ui.Window.prototype.hold = function ( data ) {
2125 var win = this,
2126 deferred = $.Deferred();
2127
2128 this.getHoldProcess( data ).execute().done( function () {
2129 // Get the focused element within the window's content
2130 var $focus = win.$content.find( OO.ui.Element.static.getDocument( win.$content ).activeElement );
2131
2132 // Blur the focused element
2133 if ( $focus.length ) {
2134 $focus[0].blur();
2135 }
2136
2137 // Force redraw by asking the browser to measure the elements' widths
2138 win.$element.removeClass( 'oo-ui-window-ready' ).width();
2139 win.$content.removeClass( 'oo-ui-window-content-ready' ).width();
2140 deferred.resolve();
2141 } );
2142
2143 return deferred.promise();
2144 };
2145
2146 /**
2147 * Teardown window.
2148 *
2149 * This is called by OO.ui.WindowManager during window closing, and should not be called directly
2150 * by other systems.
2151 *
2152 * @param {Object} [data] Window closing data
2153 * @return {jQuery.Promise} Promise resolved when window is torn down
2154 */
2155 OO.ui.Window.prototype.teardown = function ( data ) {
2156 var win = this,
2157 deferred = $.Deferred();
2158
2159 this.getTeardownProcess( data ).execute().done( function () {
2160 // Force redraw by asking the browser to measure the elements' widths
2161 win.$element.removeClass( 'oo-ui-window-load oo-ui-window-setup' ).width();
2162 win.$content.removeClass( 'oo-ui-window-content-setup' ).width();
2163 win.$element.hide();
2164 win.visible = false;
2165 deferred.resolve();
2166 } );
2167
2168 return deferred.promise();
2169 };
2170
2171 /**
2172 * Load the frame contents.
2173 *
2174 * Once the iframe's stylesheets are loaded the returned promise will be resolved. Calling while
2175 * loading will return a promise but not trigger a new loading cycle. Calling after loading is
2176 * complete will return a promise that's already been resolved.
2177 *
2178 * Sounds simple right? Read on...
2179 *
2180 * When you create a dynamic iframe using open/write/close, the window.load event for the
2181 * iframe is triggered when you call close, and there's no further load event to indicate that
2182 * everything is actually loaded.
2183 *
2184 * In Chrome, stylesheets don't show up in document.styleSheets until they have loaded, so we could
2185 * just poll that array and wait for it to have the right length. However, in Firefox, stylesheets
2186 * are added to document.styleSheets immediately, and the only way you can determine whether they've
2187 * loaded is to attempt to access .cssRules and wait for that to stop throwing an exception. But
2188 * cross-domain stylesheets never allow .cssRules to be accessed even after they have loaded.
2189 *
2190 * The workaround is to change all `<link href="...">` tags to `<style>@import url(...)</style>`
2191 * tags. Because `@import` is blocking, Chrome won't add the stylesheet to document.styleSheets
2192 * until the `@import` has finished, and Firefox won't allow .cssRules to be accessed until the
2193 * `@import` has finished. And because the contents of the `<style>` tag are from the same origin,
2194 * accessing .cssRules is allowed.
2195 *
2196 * However, now that we control the styles we're injecting, we might as well do away with
2197 * browser-specific polling hacks like document.styleSheets and .cssRules, and instead inject
2198 * `<style>@import url(...); #foo { font-family: someValue; }</style>`, then create `<div id="foo">`
2199 * and wait for its font-family to change to someValue. Because `@import` is blocking, the
2200 * font-family rule is not applied until after the `@import` finishes.
2201 *
2202 * All this stylesheet injection and polling magic is in #transplantStyles.
2203 *
2204 * @return {jQuery.Promise} Promise resolved when loading is complete
2205 */
2206 OO.ui.Window.prototype.load = function () {
2207 var sub, doc, loading,
2208 win = this;
2209
2210 this.$element.addClass( 'oo-ui-window-load' );
2211
2212 // Non-isolated windows are already "loaded"
2213 if ( !this.loading && !this.isolated ) {
2214 this.loading = $.Deferred().resolve();
2215 this.initialize();
2216 // Set initialized state after so sub-classes aren't confused by it being set by calling
2217 // their parent initialize method
2218 this.initialized = true;
2219 }
2220
2221 // Return existing promise if already loading or loaded
2222 if ( this.loading ) {
2223 return this.loading.promise();
2224 }
2225
2226 // Load the frame
2227 loading = this.loading = $.Deferred();
2228 sub = this.$iframe.prop( 'contentWindow' );
2229 doc = sub.document;
2230
2231 // Initialize contents
2232 doc.open();
2233 doc.write(
2234 '<!doctype html>' +
2235 '<html>' +
2236 '<body class="oo-ui-window-isolated oo-ui-' + this.dir + '"' +
2237 ' style="direction:' + this.dir + ';" dir="' + this.dir + '">' +
2238 '<div class="oo-ui-window-content"></div>' +
2239 '</body>' +
2240 '</html>'
2241 );
2242 doc.close();
2243
2244 // Properties
2245 this.$ = OO.ui.Element.static.getJQuery( doc, this.$iframe );
2246 this.$content = this.$( '.oo-ui-window-content' ).attr( 'tabIndex', 0 );
2247 this.$document = this.$( doc );
2248
2249 // Initialization
2250 this.constructor.static.transplantStyles( this.getElementDocument(), this.$document[0] )
2251 .always( function () {
2252 // Initialize isolated windows
2253 win.initialize();
2254 // Set initialized state after so sub-classes aren't confused by it being set by calling
2255 // their parent initialize method
2256 win.initialized = true;
2257 // Undo the visibility: hidden; hack and apply display: none;
2258 // We can do this safely now that the iframe has initialized
2259 // (don't do this from within #initialize because it has to happen
2260 // after the all subclasses have been handled as well).
2261 win.toggle( win.isVisible() );
2262
2263 loading.resolve();
2264 } );
2265
2266 return loading.promise();
2267 };
2268
2269 /**
2270 * Base class for all dialogs.
2271 *
2272 * Logic:
2273 * - Manage the window (open and close, etc.).
2274 * - Store the internal name and display title.
2275 * - A stack to track one or more pending actions.
2276 * - Manage a set of actions that can be performed.
2277 * - Configure and create action widgets.
2278 *
2279 * User interface:
2280 * - Close the dialog with Escape key.
2281 * - Visually lock the dialog while an action is in
2282 * progress (aka "pending").
2283 *
2284 * Subclass responsibilities:
2285 * - Display the title somewhere.
2286 * - Add content to the dialog.
2287 * - Provide a UI to close the dialog.
2288 * - Display the action widgets somewhere.
2289 *
2290 * @abstract
2291 * @class
2292 * @extends OO.ui.Window
2293 * @mixins OO.ui.PendingElement
2294 *
2295 * @constructor
2296 * @param {Object} [config] Configuration options
2297 */
2298 OO.ui.Dialog = function OoUiDialog( config ) {
2299 // Parent constructor
2300 OO.ui.Dialog.super.call( this, config );
2301
2302 // Mixin constructors
2303 OO.ui.PendingElement.call( this );
2304
2305 // Properties
2306 this.actions = new OO.ui.ActionSet();
2307 this.attachedActions = [];
2308 this.currentAction = null;
2309 this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
2310
2311 // Events
2312 this.actions.connect( this, {
2313 click: 'onActionClick',
2314 resize: 'onActionResize',
2315 change: 'onActionsChange'
2316 } );
2317
2318 // Initialization
2319 this.$element
2320 .addClass( 'oo-ui-dialog' )
2321 .attr( 'role', 'dialog' );
2322 };
2323
2324 /* Setup */
2325
2326 OO.inheritClass( OO.ui.Dialog, OO.ui.Window );
2327 OO.mixinClass( OO.ui.Dialog, OO.ui.PendingElement );
2328
2329 /* Static Properties */
2330
2331 /**
2332 * Symbolic name of dialog.
2333 *
2334 * @abstract
2335 * @static
2336 * @inheritable
2337 * @property {string}
2338 */
2339 OO.ui.Dialog.static.name = '';
2340
2341 /**
2342 * Dialog title.
2343 *
2344 * @abstract
2345 * @static
2346 * @inheritable
2347 * @property {jQuery|string|Function} Label nodes, text or a function that returns nodes or text
2348 */
2349 OO.ui.Dialog.static.title = '';
2350
2351 /**
2352 * List of OO.ui.ActionWidget configuration options.
2353 *
2354 * @static
2355 * inheritable
2356 * @property {Object[]}
2357 */
2358 OO.ui.Dialog.static.actions = [];
2359
2360 /**
2361 * Close dialog when the escape key is pressed.
2362 *
2363 * @static
2364 * @abstract
2365 * @inheritable
2366 * @property {boolean}
2367 */
2368 OO.ui.Dialog.static.escapable = true;
2369
2370 /* Methods */
2371
2372 /**
2373 * Handle frame document key down events.
2374 *
2375 * @param {jQuery.Event} e Key down event
2376 */
2377 OO.ui.Dialog.prototype.onDocumentKeyDown = function ( e ) {
2378 if ( e.which === OO.ui.Keys.ESCAPE ) {
2379 this.close();
2380 e.preventDefault();
2381 e.stopPropagation();
2382 }
2383 };
2384
2385 /**
2386 * Handle action resized events.
2387 *
2388 * @param {OO.ui.ActionWidget} action Action that was resized
2389 */
2390 OO.ui.Dialog.prototype.onActionResize = function () {
2391 // Override in subclass
2392 };
2393
2394 /**
2395 * Handle action click events.
2396 *
2397 * @param {OO.ui.ActionWidget} action Action that was clicked
2398 */
2399 OO.ui.Dialog.prototype.onActionClick = function ( action ) {
2400 if ( !this.isPending() ) {
2401 this.currentAction = action;
2402 this.executeAction( action.getAction() );
2403 }
2404 };
2405
2406 /**
2407 * Handle actions change event.
2408 */
2409 OO.ui.Dialog.prototype.onActionsChange = function () {
2410 this.detachActions();
2411 if ( !this.isClosing() ) {
2412 this.attachActions();
2413 }
2414 };
2415
2416 /**
2417 * Get set of actions.
2418 *
2419 * @return {OO.ui.ActionSet}
2420 */
2421 OO.ui.Dialog.prototype.getActions = function () {
2422 return this.actions;
2423 };
2424
2425 /**
2426 * Get a process for taking action.
2427 *
2428 * When you override this method, you can add additional accept steps to the process the parent
2429 * method provides using the 'first' and 'next' methods.
2430 *
2431 * @abstract
2432 * @param {string} [action] Symbolic name of action
2433 * @return {OO.ui.Process} Action process
2434 */
2435 OO.ui.Dialog.prototype.getActionProcess = function ( action ) {
2436 return new OO.ui.Process()
2437 .next( function () {
2438 if ( !action ) {
2439 // An empty action always closes the dialog without data, which should always be
2440 // safe and make no changes
2441 this.close();
2442 }
2443 }, this );
2444 };
2445
2446 /**
2447 * @inheritdoc
2448 *
2449 * @param {Object} [data] Dialog opening data
2450 * @param {jQuery|string|Function|null} [data.title] Dialog title, omit to use #static-title
2451 * @param {Object[]} [data.actions] List of OO.ui.ActionWidget configuration options for each
2452 * action item, omit to use #static-actions
2453 */
2454 OO.ui.Dialog.prototype.getSetupProcess = function ( data ) {
2455 data = data || {};
2456
2457 // Parent method
2458 return OO.ui.Dialog.super.prototype.getSetupProcess.call( this, data )
2459 .next( function () {
2460 var i, len,
2461 items = [],
2462 config = this.constructor.static,
2463 actions = data.actions !== undefined ? data.actions : config.actions;
2464
2465 this.title.setLabel(
2466 data.title !== undefined ? data.title : this.constructor.static.title
2467 );
2468 for ( i = 0, len = actions.length; i < len; i++ ) {
2469 items.push(
2470 new OO.ui.ActionWidget( $.extend( { $: this.$ }, actions[i] ) )
2471 );
2472 }
2473 this.actions.add( items );
2474
2475 if ( this.constructor.static.escapable ) {
2476 this.$document.on( 'keydown', this.onDocumentKeyDownHandler );
2477 }
2478 }, this );
2479 };
2480
2481 /**
2482 * @inheritdoc
2483 */
2484 OO.ui.Dialog.prototype.getTeardownProcess = function ( data ) {
2485 // Parent method
2486 return OO.ui.Dialog.super.prototype.getTeardownProcess.call( this, data )
2487 .first( function () {
2488 if ( this.constructor.static.escapable ) {
2489 this.$document.off( 'keydown', this.onDocumentKeyDownHandler );
2490 }
2491
2492 this.actions.clear();
2493 this.currentAction = null;
2494 }, this );
2495 };
2496
2497 /**
2498 * @inheritdoc
2499 */
2500 OO.ui.Dialog.prototype.initialize = function () {
2501 // Parent method
2502 OO.ui.Dialog.super.prototype.initialize.call( this );
2503
2504 // Properties
2505 this.title = new OO.ui.LabelWidget( { $: this.$ } );
2506
2507 // Initialization
2508 this.$content.addClass( 'oo-ui-dialog-content' );
2509 this.setPendingElement( this.$head );
2510 };
2511
2512 /**
2513 * Attach action actions.
2514 */
2515 OO.ui.Dialog.prototype.attachActions = function () {
2516 // Remember the list of potentially attached actions
2517 this.attachedActions = this.actions.get();
2518 };
2519
2520 /**
2521 * Detach action actions.
2522 *
2523 * @chainable
2524 */
2525 OO.ui.Dialog.prototype.detachActions = function () {
2526 var i, len;
2527
2528 // Detach all actions that may have been previously attached
2529 for ( i = 0, len = this.attachedActions.length; i < len; i++ ) {
2530 this.attachedActions[i].$element.detach();
2531 }
2532 this.attachedActions = [];
2533 };
2534
2535 /**
2536 * Execute an action.
2537 *
2538 * @param {string} action Symbolic name of action to execute
2539 * @return {jQuery.Promise} Promise resolved when action completes, rejected if it fails
2540 */
2541 OO.ui.Dialog.prototype.executeAction = function ( action ) {
2542 this.pushPending();
2543 return this.getActionProcess( action ).execute()
2544 .always( this.popPending.bind( this ) );
2545 };
2546
2547 /**
2548 * Collection of windows.
2549 *
2550 * @class
2551 * @extends OO.ui.Element
2552 * @mixins OO.EventEmitter
2553 *
2554 * Managed windows are mutually exclusive. If a window is opened while there is a current window
2555 * already opening or opened, the current window will be closed without data. Empty closing data
2556 * should always result in the window being closed without causing constructive or destructive
2557 * action.
2558 *
2559 * As a window is opened and closed, it passes through several stages and the manager emits several
2560 * corresponding events.
2561 *
2562 * - {@link #openWindow} or {@link OO.ui.Window#open} methods are used to start opening
2563 * - {@link #event-opening} is emitted with `opening` promise
2564 * - {@link #getSetupDelay} is called the returned value is used to time a pause in execution
2565 * - {@link OO.ui.Window#getSetupProcess} method is called on the window and its result executed
2566 * - `setup` progress notification is emitted from opening promise
2567 * - {@link #getReadyDelay} is called the returned value is used to time a pause in execution
2568 * - {@link OO.ui.Window#getReadyProcess} method is called on the window and its result executed
2569 * - `ready` progress notification is emitted from opening promise
2570 * - `opening` promise is resolved with `opened` promise
2571 * - Window is now open
2572 *
2573 * - {@link #closeWindow} or {@link OO.ui.Window#close} methods are used to start closing
2574 * - `opened` promise is resolved with `closing` promise
2575 * - {@link #event-closing} is emitted with `closing` promise
2576 * - {@link #getHoldDelay} is called the returned value is used to time a pause in execution
2577 * - {@link OO.ui.Window#getHoldProcess} method is called on the window and its result executed
2578 * - `hold` progress notification is emitted from opening promise
2579 * - {@link #getTeardownDelay} is called the returned value is used to time a pause in execution
2580 * - {@link OO.ui.Window#getTeardownProcess} method is called on the window and its result executed
2581 * - `teardown` progress notification is emitted from opening promise
2582 * - Closing promise is resolved
2583 * - Window is now closed
2584 *
2585 * @constructor
2586 * @param {Object} [config] Configuration options
2587 * @cfg {boolean} [isolate] Configure managed windows to isolate their content using inline frames
2588 * @cfg {OO.Factory} [factory] Window factory to use for automatic instantiation
2589 * @cfg {boolean} [modal=true] Prevent interaction outside the dialog
2590 */
2591 OO.ui.WindowManager = function OoUiWindowManager( config ) {
2592 // Configuration initialization
2593 config = config || {};
2594
2595 // Parent constructor
2596 OO.ui.WindowManager.super.call( this, config );
2597
2598 // Mixin constructors
2599 OO.EventEmitter.call( this );
2600
2601 // Properties
2602 this.factory = config.factory;
2603 this.modal = config.modal === undefined || !!config.modal;
2604 this.isolate = !!config.isolate;
2605 this.windows = {};
2606 this.opening = null;
2607 this.opened = null;
2608 this.closing = null;
2609 this.preparingToOpen = null;
2610 this.preparingToClose = null;
2611 this.size = null;
2612 this.currentWindow = null;
2613 this.$ariaHidden = null;
2614 this.requestedSize = null;
2615 this.onWindowResizeTimeout = null;
2616 this.onWindowResizeHandler = this.onWindowResize.bind( this );
2617 this.afterWindowResizeHandler = this.afterWindowResize.bind( this );
2618 this.onWindowMouseWheelHandler = this.onWindowMouseWheel.bind( this );
2619 this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
2620
2621 // Initialization
2622 this.$element
2623 .addClass( 'oo-ui-windowManager' )
2624 .toggleClass( 'oo-ui-windowManager-modal', this.modal );
2625 };
2626
2627 /* Setup */
2628
2629 OO.inheritClass( OO.ui.WindowManager, OO.ui.Element );
2630 OO.mixinClass( OO.ui.WindowManager, OO.EventEmitter );
2631
2632 /* Events */
2633
2634 /**
2635 * Window is opening.
2636 *
2637 * Fired when the window begins to be opened.
2638 *
2639 * @event opening
2640 * @param {OO.ui.Window} win Window that's being opened
2641 * @param {jQuery.Promise} opening Promise resolved when window is opened; when the promise is
2642 * resolved the first argument will be a promise which will be resolved when the window begins
2643 * closing, the second argument will be the opening data; progress notifications will be fired on
2644 * the promise for `setup` and `ready` when those processes are completed respectively.
2645 * @param {Object} data Window opening data
2646 */
2647
2648 /**
2649 * Window is closing.
2650 *
2651 * Fired when the window begins to be closed.
2652 *
2653 * @event closing
2654 * @param {OO.ui.Window} win Window that's being closed
2655 * @param {jQuery.Promise} opening Promise resolved when window is closed; when the promise
2656 * is resolved the first argument will be a the closing data; progress notifications will be fired
2657 * on the promise for `hold` and `teardown` when those processes are completed respectively.
2658 * @param {Object} data Window closing data
2659 */
2660
2661 /**
2662 * Window was resized.
2663 *
2664 * @event resize
2665 * @param {OO.ui.Window} win Window that was resized
2666 */
2667
2668 /* Static Properties */
2669
2670 /**
2671 * Map of symbolic size names and CSS properties.
2672 *
2673 * @static
2674 * @inheritable
2675 * @property {Object}
2676 */
2677 OO.ui.WindowManager.static.sizes = {
2678 small: {
2679 width: 300
2680 },
2681 medium: {
2682 width: 500
2683 },
2684 large: {
2685 width: 700
2686 },
2687 full: {
2688 // These can be non-numeric because they are never used in calculations
2689 width: '100%',
2690 height: '100%'
2691 }
2692 };
2693
2694 /**
2695 * Symbolic name of default size.
2696 *
2697 * Default size is used if the window's requested size is not recognized.
2698 *
2699 * @static
2700 * @inheritable
2701 * @property {string}
2702 */
2703 OO.ui.WindowManager.static.defaultSize = 'medium';
2704
2705 /* Methods */
2706
2707 /**
2708 * Handle window resize events.
2709 *
2710 * @param {jQuery.Event} e Window resize event
2711 */
2712 OO.ui.WindowManager.prototype.onWindowResize = function () {
2713 clearTimeout( this.onWindowResizeTimeout );
2714 this.onWindowResizeTimeout = setTimeout( this.afterWindowResizeHandler, 200 );
2715 };
2716
2717 /**
2718 * Handle window resize events.
2719 *
2720 * @param {jQuery.Event} e Window resize event
2721 */
2722 OO.ui.WindowManager.prototype.afterWindowResize = function () {
2723 if ( this.currentWindow ) {
2724 this.updateWindowSize( this.currentWindow );
2725 }
2726 };
2727
2728 /**
2729 * Handle window mouse wheel events.
2730 *
2731 * @param {jQuery.Event} e Mouse wheel event
2732 */
2733 OO.ui.WindowManager.prototype.onWindowMouseWheel = function () {
2734 // Kill all events in the parent window if the child window is isolated
2735 return !this.shouldIsolate();
2736 };
2737
2738 /**
2739 * Handle document key down events.
2740 *
2741 * @param {jQuery.Event} e Key down event
2742 */
2743 OO.ui.WindowManager.prototype.onDocumentKeyDown = function ( e ) {
2744 switch ( e.which ) {
2745 case OO.ui.Keys.PAGEUP:
2746 case OO.ui.Keys.PAGEDOWN:
2747 case OO.ui.Keys.END:
2748 case OO.ui.Keys.HOME:
2749 case OO.ui.Keys.LEFT:
2750 case OO.ui.Keys.UP:
2751 case OO.ui.Keys.RIGHT:
2752 case OO.ui.Keys.DOWN:
2753 // Kill all events in the parent window if the child window is isolated
2754 return !this.shouldIsolate();
2755 }
2756 };
2757
2758 /**
2759 * Check if window is opening.
2760 *
2761 * @return {boolean} Window is opening
2762 */
2763 OO.ui.WindowManager.prototype.isOpening = function ( win ) {
2764 return win === this.currentWindow && !!this.opening && this.opening.state() === 'pending';
2765 };
2766
2767 /**
2768 * Check if window is closing.
2769 *
2770 * @return {boolean} Window is closing
2771 */
2772 OO.ui.WindowManager.prototype.isClosing = function ( win ) {
2773 return win === this.currentWindow && !!this.closing && this.closing.state() === 'pending';
2774 };
2775
2776 /**
2777 * Check if window is opened.
2778 *
2779 * @return {boolean} Window is opened
2780 */
2781 OO.ui.WindowManager.prototype.isOpened = function ( win ) {
2782 return win === this.currentWindow && !!this.opened && this.opened.state() === 'pending';
2783 };
2784
2785 /**
2786 * Check if window contents should be isolated.
2787 *
2788 * Window content isolation is done using inline frames.
2789 *
2790 * @return {boolean} Window contents should be isolated
2791 */
2792 OO.ui.WindowManager.prototype.shouldIsolate = function () {
2793 return this.isolate;
2794 };
2795
2796 /**
2797 * Check if a window is being managed.
2798 *
2799 * @param {OO.ui.Window} win Window to check
2800 * @return {boolean} Window is being managed
2801 */
2802 OO.ui.WindowManager.prototype.hasWindow = function ( win ) {
2803 var name;
2804
2805 for ( name in this.windows ) {
2806 if ( this.windows[name] === win ) {
2807 return true;
2808 }
2809 }
2810
2811 return false;
2812 };
2813
2814 /**
2815 * Get the number of milliseconds to wait between beginning opening and executing setup process.
2816 *
2817 * @param {OO.ui.Window} win Window being opened
2818 * @param {Object} [data] Window opening data
2819 * @return {number} Milliseconds to wait
2820 */
2821 OO.ui.WindowManager.prototype.getSetupDelay = function () {
2822 return 0;
2823 };
2824
2825 /**
2826 * Get the number of milliseconds to wait between finishing setup and executing ready process.
2827 *
2828 * @param {OO.ui.Window} win Window being opened
2829 * @param {Object} [data] Window opening data
2830 * @return {number} Milliseconds to wait
2831 */
2832 OO.ui.WindowManager.prototype.getReadyDelay = function () {
2833 return 0;
2834 };
2835
2836 /**
2837 * Get the number of milliseconds to wait between beginning closing and executing hold process.
2838 *
2839 * @param {OO.ui.Window} win Window being closed
2840 * @param {Object} [data] Window closing data
2841 * @return {number} Milliseconds to wait
2842 */
2843 OO.ui.WindowManager.prototype.getHoldDelay = function () {
2844 return 0;
2845 };
2846
2847 /**
2848 * Get the number of milliseconds to wait between finishing hold and executing teardown process.
2849 *
2850 * @param {OO.ui.Window} win Window being closed
2851 * @param {Object} [data] Window closing data
2852 * @return {number} Milliseconds to wait
2853 */
2854 OO.ui.WindowManager.prototype.getTeardownDelay = function () {
2855 return this.modal ? 250 : 0;
2856 };
2857
2858 /**
2859 * Get managed window by symbolic name.
2860 *
2861 * If window is not yet instantiated, it will be instantiated and added automatically.
2862 *
2863 * @param {string} name Symbolic window name
2864 * @return {jQuery.Promise} Promise resolved with matching window, or rejected with an OO.ui.Error
2865 * @throws {Error} If the symbolic name is unrecognized by the factory
2866 * @throws {Error} If the symbolic name unrecognized as a managed window
2867 */
2868 OO.ui.WindowManager.prototype.getWindow = function ( name ) {
2869 var deferred = $.Deferred(),
2870 win = this.windows[name];
2871
2872 if ( !( win instanceof OO.ui.Window ) ) {
2873 if ( this.factory ) {
2874 if ( !this.factory.lookup( name ) ) {
2875 deferred.reject( new OO.ui.Error(
2876 'Cannot auto-instantiate window: symbolic name is unrecognized by the factory'
2877 ) );
2878 } else {
2879 win = this.factory.create( name, this, { $: this.$ } );
2880 this.addWindows( [ win ] );
2881 deferred.resolve( win );
2882 }
2883 } else {
2884 deferred.reject( new OO.ui.Error(
2885 'Cannot get unmanaged window: symbolic name unrecognized as a managed window'
2886 ) );
2887 }
2888 } else {
2889 deferred.resolve( win );
2890 }
2891
2892 return deferred.promise();
2893 };
2894
2895 /**
2896 * Get current window.
2897 *
2898 * @return {OO.ui.Window|null} Currently opening/opened/closing window
2899 */
2900 OO.ui.WindowManager.prototype.getCurrentWindow = function () {
2901 return this.currentWindow;
2902 };
2903
2904 /**
2905 * Open a window.
2906 *
2907 * @param {OO.ui.Window|string} win Window object or symbolic name of window to open
2908 * @param {Object} [data] Window opening data
2909 * @return {jQuery.Promise} Promise resolved when window is done opening; see {@link #event-opening}
2910 * for more details about the `opening` promise
2911 * @fires opening
2912 */
2913 OO.ui.WindowManager.prototype.openWindow = function ( win, data ) {
2914 var manager = this,
2915 preparing = [],
2916 opening = $.Deferred();
2917
2918 // Argument handling
2919 if ( typeof win === 'string' ) {
2920 return this.getWindow( win ).then( function ( win ) {
2921 return manager.openWindow( win, data );
2922 } );
2923 }
2924
2925 // Error handling
2926 if ( !this.hasWindow( win ) ) {
2927 opening.reject( new OO.ui.Error(
2928 'Cannot open window: window is not attached to manager'
2929 ) );
2930 } else if ( this.preparingToOpen || this.opening || this.opened ) {
2931 opening.reject( new OO.ui.Error(
2932 'Cannot open window: another window is opening or open'
2933 ) );
2934 }
2935
2936 // Window opening
2937 if ( opening.state() !== 'rejected' ) {
2938 if ( !win.getManager() ) {
2939 win.setManager( this );
2940 }
2941 preparing.push( win.load() );
2942
2943 if ( this.closing ) {
2944 // If a window is currently closing, wait for it to complete
2945 preparing.push( this.closing );
2946 }
2947
2948 this.preparingToOpen = $.when.apply( $, preparing );
2949 // Ensure handlers get called after preparingToOpen is set
2950 this.preparingToOpen.done( function () {
2951 if ( manager.modal ) {
2952 manager.toggleGlobalEvents( true );
2953 manager.toggleAriaIsolation( true );
2954 }
2955 manager.currentWindow = win;
2956 manager.opening = opening;
2957 manager.preparingToOpen = null;
2958 manager.emit( 'opening', win, opening, data );
2959 setTimeout( function () {
2960 win.setup( data ).then( function () {
2961 manager.updateWindowSize( win );
2962 manager.opening.notify( { state: 'setup' } );
2963 setTimeout( function () {
2964 win.ready( data ).then( function () {
2965 manager.opening.notify( { state: 'ready' } );
2966 manager.opening = null;
2967 manager.opened = $.Deferred();
2968 opening.resolve( manager.opened.promise(), data );
2969 } );
2970 }, manager.getReadyDelay() );
2971 } );
2972 }, manager.getSetupDelay() );
2973 } );
2974 }
2975
2976 return opening.promise();
2977 };
2978
2979 /**
2980 * Close a window.
2981 *
2982 * @param {OO.ui.Window|string} win Window object or symbolic name of window to close
2983 * @param {Object} [data] Window closing data
2984 * @return {jQuery.Promise} Promise resolved when window is done closing; see {@link #event-closing}
2985 * for more details about the `closing` promise
2986 * @throws {Error} If no window by that name is being managed
2987 * @fires closing
2988 */
2989 OO.ui.WindowManager.prototype.closeWindow = function ( win, data ) {
2990 var manager = this,
2991 preparing = [],
2992 closing = $.Deferred(),
2993 opened;
2994
2995 // Argument handling
2996 if ( typeof win === 'string' ) {
2997 win = this.windows[win];
2998 } else if ( !this.hasWindow( win ) ) {
2999 win = null;
3000 }
3001
3002 // Error handling
3003 if ( !win ) {
3004 closing.reject( new OO.ui.Error(
3005 'Cannot close window: window is not attached to manager'
3006 ) );
3007 } else if ( win !== this.currentWindow ) {
3008 closing.reject( new OO.ui.Error(
3009 'Cannot close window: window already closed with different data'
3010 ) );
3011 } else if ( this.preparingToClose || this.closing ) {
3012 closing.reject( new OO.ui.Error(
3013 'Cannot close window: window already closing with different data'
3014 ) );
3015 }
3016
3017 // Window closing
3018 if ( closing.state() !== 'rejected' ) {
3019 if ( this.opening ) {
3020 // If the window is currently opening, close it when it's done
3021 preparing.push( this.opening );
3022 }
3023
3024 this.preparingToClose = $.when.apply( $, preparing );
3025 // Ensure handlers get called after preparingToClose is set
3026 this.preparingToClose.done( function () {
3027 manager.closing = closing;
3028 manager.preparingToClose = null;
3029 manager.emit( 'closing', win, closing, data );
3030 opened = manager.opened;
3031 manager.opened = null;
3032 opened.resolve( closing.promise(), data );
3033 setTimeout( function () {
3034 win.hold( data ).then( function () {
3035 closing.notify( { state: 'hold' } );
3036 setTimeout( function () {
3037 win.teardown( data ).then( function () {
3038 closing.notify( { state: 'teardown' } );
3039 if ( manager.modal ) {
3040 manager.toggleGlobalEvents( false );
3041 manager.toggleAriaIsolation( false );
3042 }
3043 manager.closing = null;
3044 manager.currentWindow = null;
3045 closing.resolve( data );
3046 } );
3047 }, manager.getTeardownDelay() );
3048 } );
3049 }, manager.getHoldDelay() );
3050 } );
3051 }
3052
3053 return closing.promise();
3054 };
3055
3056 /**
3057 * Add windows.
3058 *
3059 * @param {Object.<string,OO.ui.Window>|OO.ui.Window[]} windows Windows to add
3060 * @throws {Error} If one of the windows being added without an explicit symbolic name does not have
3061 * a statically configured symbolic name
3062 */
3063 OO.ui.WindowManager.prototype.addWindows = function ( windows ) {
3064 var i, len, win, name, list;
3065
3066 if ( $.isArray( windows ) ) {
3067 // Convert to map of windows by looking up symbolic names from static configuration
3068 list = {};
3069 for ( i = 0, len = windows.length; i < len; i++ ) {
3070 name = windows[i].constructor.static.name;
3071 if ( typeof name !== 'string' ) {
3072 throw new Error( 'Cannot add window' );
3073 }
3074 list[name] = windows[i];
3075 }
3076 } else if ( $.isPlainObject( windows ) ) {
3077 list = windows;
3078 }
3079
3080 // Add windows
3081 for ( name in list ) {
3082 win = list[name];
3083 this.windows[name] = win;
3084 this.$element.append( win.$element );
3085 }
3086 };
3087
3088 /**
3089 * Remove windows.
3090 *
3091 * Windows will be closed before they are removed.
3092 *
3093 * @param {string} name Symbolic name of window to remove
3094 * @return {jQuery.Promise} Promise resolved when window is closed and removed
3095 * @throws {Error} If windows being removed are not being managed
3096 */
3097 OO.ui.WindowManager.prototype.removeWindows = function ( names ) {
3098 var i, len, win, name, cleanupWindow,
3099 manager = this,
3100 promises = [],
3101 cleanup = function ( name, win ) {
3102 delete manager.windows[name];
3103 win.$element.detach();
3104 };
3105
3106 for ( i = 0, len = names.length; i < len; i++ ) {
3107 name = names[i];
3108 win = this.windows[name];
3109 if ( !win ) {
3110 throw new Error( 'Cannot remove window' );
3111 }
3112 cleanupWindow = cleanup.bind( null, name, win );
3113 promises.push( this.closeWindow( name ).then( cleanupWindow, cleanupWindow ) );
3114 }
3115
3116 return $.when.apply( $, promises );
3117 };
3118
3119 /**
3120 * Remove all windows.
3121 *
3122 * Windows will be closed before they are removed.
3123 *
3124 * @return {jQuery.Promise} Promise resolved when all windows are closed and removed
3125 */
3126 OO.ui.WindowManager.prototype.clearWindows = function () {
3127 return this.removeWindows( Object.keys( this.windows ) );
3128 };
3129
3130 /**
3131 * Set dialog size.
3132 *
3133 * Fullscreen mode will be used if the dialog is too wide to fit in the screen.
3134 *
3135 * @chainable
3136 */
3137 OO.ui.WindowManager.prototype.updateWindowSize = function ( win ) {
3138 // Bypass for non-current, and thus invisible, windows
3139 if ( win !== this.currentWindow ) {
3140 return;
3141 }
3142
3143 var viewport = OO.ui.Element.static.getDimensions( win.getElementWindow() ),
3144 sizes = this.constructor.static.sizes,
3145 size = win.getSize();
3146
3147 if ( !sizes[size] ) {
3148 size = this.constructor.static.defaultSize;
3149 }
3150 if ( size !== 'full' && viewport.rect.right - viewport.rect.left < sizes[size].width ) {
3151 size = 'full';
3152 }
3153
3154 this.$element.toggleClass( 'oo-ui-windowManager-fullscreen', size === 'full' );
3155 this.$element.toggleClass( 'oo-ui-windowManager-floating', size !== 'full' );
3156 win.setDimensions( sizes[size] );
3157
3158 this.emit( 'resize', win );
3159
3160 return this;
3161 };
3162
3163 /**
3164 * Bind or unbind global events for scrolling.
3165 *
3166 * @param {boolean} [on] Bind global events
3167 * @chainable
3168 */
3169 OO.ui.WindowManager.prototype.toggleGlobalEvents = function ( on ) {
3170 on = on === undefined ? !!this.globalEvents : !!on;
3171
3172 if ( on ) {
3173 if ( !this.globalEvents ) {
3174 this.$( this.getElementDocument() ).on( {
3175 // Prevent scrolling by keys in top-level window
3176 keydown: this.onDocumentKeyDownHandler
3177 } );
3178 this.$( this.getElementWindow() ).on( {
3179 // Prevent scrolling by wheel in top-level window
3180 mousewheel: this.onWindowMouseWheelHandler,
3181 // Start listening for top-level window dimension changes
3182 'orientationchange resize': this.onWindowResizeHandler
3183 } );
3184 // Disable window scrolling in isolated windows
3185 if ( !this.shouldIsolate() ) {
3186 $( this.getElementDocument().body ).css( 'overflow', 'hidden' );
3187 }
3188 this.globalEvents = true;
3189 }
3190 } else if ( this.globalEvents ) {
3191 // Unbind global events
3192 this.$( this.getElementDocument() ).off( {
3193 // Allow scrolling by keys in top-level window
3194 keydown: this.onDocumentKeyDownHandler
3195 } );
3196 this.$( this.getElementWindow() ).off( {
3197 // Allow scrolling by wheel in top-level window
3198 mousewheel: this.onWindowMouseWheelHandler,
3199 // Stop listening for top-level window dimension changes
3200 'orientationchange resize': this.onWindowResizeHandler
3201 } );
3202 if ( !this.shouldIsolate() ) {
3203 $( this.getElementDocument().body ).css( 'overflow', '' );
3204 }
3205 this.globalEvents = false;
3206 }
3207
3208 return this;
3209 };
3210
3211 /**
3212 * Toggle screen reader visibility of content other than the window manager.
3213 *
3214 * @param {boolean} [isolate] Make only the window manager visible to screen readers
3215 * @chainable
3216 */
3217 OO.ui.WindowManager.prototype.toggleAriaIsolation = function ( isolate ) {
3218 isolate = isolate === undefined ? !this.$ariaHidden : !!isolate;
3219
3220 if ( isolate ) {
3221 if ( !this.$ariaHidden ) {
3222 // Hide everything other than the window manager from screen readers
3223 this.$ariaHidden = $( 'body' )
3224 .children()
3225 .not( this.$element.parentsUntil( 'body' ).last() )
3226 .attr( 'aria-hidden', '' );
3227 }
3228 } else if ( this.$ariaHidden ) {
3229 // Restore screen reader visibility
3230 this.$ariaHidden.removeAttr( 'aria-hidden' );
3231 this.$ariaHidden = null;
3232 }
3233
3234 return this;
3235 };
3236
3237 /**
3238 * Destroy window manager.
3239 *
3240 * Windows will not be closed, only removed from the DOM.
3241 */
3242 OO.ui.WindowManager.prototype.destroy = function () {
3243 this.toggleGlobalEvents( false );
3244 this.toggleAriaIsolation( false );
3245 this.$element.remove();
3246 };
3247
3248 /**
3249 * @abstract
3250 * @class
3251 *
3252 * @constructor
3253 * @param {string|jQuery} message Description of error
3254 * @param {Object} [config] Configuration options
3255 * @cfg {boolean} [recoverable=true] Error is recoverable
3256 * @cfg {boolean} [warning=false] Whether this error is a warning or not.
3257 */
3258 OO.ui.Error = function OoUiElement( message, config ) {
3259 // Configuration initialization
3260 config = config || {};
3261
3262 // Properties
3263 this.message = message instanceof jQuery ? message : String( message );
3264 this.recoverable = config.recoverable === undefined || !!config.recoverable;
3265 this.warning = !!config.warning;
3266 };
3267
3268 /* Setup */
3269
3270 OO.initClass( OO.ui.Error );
3271
3272 /* Methods */
3273
3274 /**
3275 * Check if error can be recovered from.
3276 *
3277 * @return {boolean} Error is recoverable
3278 */
3279 OO.ui.Error.prototype.isRecoverable = function () {
3280 return this.recoverable;
3281 };
3282
3283 /**
3284 * Check if the error is a warning
3285 *
3286 * @return {boolean} Error is warning
3287 */
3288 OO.ui.Error.prototype.isWarning = function () {
3289 return this.warning;
3290 };
3291
3292 /**
3293 * Get error message as DOM nodes.
3294 *
3295 * @return {jQuery} Error message in DOM nodes
3296 */
3297 OO.ui.Error.prototype.getMessage = function () {
3298 return this.message instanceof jQuery ?
3299 this.message.clone() :
3300 $( '<div>' ).text( this.message ).contents();
3301 };
3302
3303 /**
3304 * Get error message as text.
3305 *
3306 * @return {string} Error message
3307 */
3308 OO.ui.Error.prototype.getMessageText = function () {
3309 return this.message instanceof jQuery ? this.message.text() : this.message;
3310 };
3311
3312 /**
3313 * A list of functions, called in sequence.
3314 *
3315 * If a function added to a process returns boolean false the process will stop; if it returns an
3316 * object with a `promise` method the process will use the promise to either continue to the next
3317 * step when the promise is resolved or stop when the promise is rejected.
3318 *
3319 * @class
3320 *
3321 * @constructor
3322 * @param {number|jQuery.Promise|Function} step Time to wait, promise to wait for or function to
3323 * call, see #createStep for more information
3324 * @param {Object} [context=null] Context to call the step function in, ignored if step is a number
3325 * or a promise
3326 * @return {Object} Step object, with `callback` and `context` properties
3327 */
3328 OO.ui.Process = function ( step, context ) {
3329 // Properties
3330 this.steps = [];
3331
3332 // Initialization
3333 if ( step !== undefined ) {
3334 this.next( step, context );
3335 }
3336 };
3337
3338 /* Setup */
3339
3340 OO.initClass( OO.ui.Process );
3341
3342 /* Methods */
3343
3344 /**
3345 * Start the process.
3346 *
3347 * @return {jQuery.Promise} Promise that is resolved when all steps have completed or rejected when
3348 * any of the steps return boolean false or a promise which gets rejected; upon stopping the
3349 * process, the remaining steps will not be taken
3350 */
3351 OO.ui.Process.prototype.execute = function () {
3352 var i, len, promise;
3353
3354 /**
3355 * Continue execution.
3356 *
3357 * @ignore
3358 * @param {Array} step A function and the context it should be called in
3359 * @return {Function} Function that continues the process
3360 */
3361 function proceed( step ) {
3362 return function () {
3363 // Execute step in the correct context
3364 var deferred,
3365 result = step.callback.call( step.context );
3366
3367 if ( result === false ) {
3368 // Use rejected promise for boolean false results
3369 return $.Deferred().reject( [] ).promise();
3370 }
3371 if ( typeof result === 'number' ) {
3372 if ( result < 0 ) {
3373 throw new Error( 'Cannot go back in time: flux capacitor is out of service' );
3374 }
3375 // Use a delayed promise for numbers, expecting them to be in milliseconds
3376 deferred = $.Deferred();
3377 setTimeout( deferred.resolve, result );
3378 return deferred.promise();
3379 }
3380 if ( result instanceof OO.ui.Error ) {
3381 // Use rejected promise for error
3382 return $.Deferred().reject( [ result ] ).promise();
3383 }
3384 if ( $.isArray( result ) && result.length && result[0] instanceof OO.ui.Error ) {
3385 // Use rejected promise for list of errors
3386 return $.Deferred().reject( result ).promise();
3387 }
3388 // Duck-type the object to see if it can produce a promise
3389 if ( result && $.isFunction( result.promise ) ) {
3390 // Use a promise generated from the result
3391 return result.promise();
3392 }
3393 // Use resolved promise for other results
3394 return $.Deferred().resolve().promise();
3395 };
3396 }
3397
3398 if ( this.steps.length ) {
3399 // Generate a chain reaction of promises
3400 promise = proceed( this.steps[0] )();
3401 for ( i = 1, len = this.steps.length; i < len; i++ ) {
3402 promise = promise.then( proceed( this.steps[i] ) );
3403 }
3404 } else {
3405 promise = $.Deferred().resolve().promise();
3406 }
3407
3408 return promise;
3409 };
3410
3411 /**
3412 * Create a process step.
3413 *
3414 * @private
3415 * @param {number|jQuery.Promise|Function} step
3416 *
3417 * - Number of milliseconds to wait; or
3418 * - Promise to wait to be resolved; or
3419 * - Function to execute
3420 * - If it returns boolean false the process will stop
3421 * - If it returns an object with a `promise` method the process will use the promise to either
3422 * continue to the next step when the promise is resolved or stop when the promise is rejected
3423 * - If it returns a number, the process will wait for that number of milliseconds before
3424 * proceeding
3425 * @param {Object} [context=null] Context to call the step function in, ignored if step is a number
3426 * or a promise
3427 * @return {Object} Step object, with `callback` and `context` properties
3428 */
3429 OO.ui.Process.prototype.createStep = function ( step, context ) {
3430 if ( typeof step === 'number' || $.isFunction( step.promise ) ) {
3431 return {
3432 callback: function () {
3433 return step;
3434 },
3435 context: null
3436 };
3437 }
3438 if ( $.isFunction( step ) ) {
3439 return {
3440 callback: step,
3441 context: context
3442 };
3443 }
3444 throw new Error( 'Cannot create process step: number, promise or function expected' );
3445 };
3446
3447 /**
3448 * Add step to the beginning of the process.
3449 *
3450 * @inheritdoc #createStep
3451 * @return {OO.ui.Process} this
3452 * @chainable
3453 */
3454 OO.ui.Process.prototype.first = function ( step, context ) {
3455 this.steps.unshift( this.createStep( step, context ) );
3456 return this;
3457 };
3458
3459 /**
3460 * Add step to the end of the process.
3461 *
3462 * @inheritdoc #createStep
3463 * @return {OO.ui.Process} this
3464 * @chainable
3465 */
3466 OO.ui.Process.prototype.next = function ( step, context ) {
3467 this.steps.push( this.createStep( step, context ) );
3468 return this;
3469 };
3470
3471 /**
3472 * Factory for tools.
3473 *
3474 * @class
3475 * @extends OO.Factory
3476 * @constructor
3477 */
3478 OO.ui.ToolFactory = function OoUiToolFactory() {
3479 // Parent constructor
3480 OO.ui.ToolFactory.super.call( this );
3481 };
3482
3483 /* Setup */
3484
3485 OO.inheritClass( OO.ui.ToolFactory, OO.Factory );
3486
3487 /* Methods */
3488
3489 /**
3490 * Get tools from the factory
3491 *
3492 * @param {Array} include Included tools
3493 * @param {Array} exclude Excluded tools
3494 * @param {Array} promote Promoted tools
3495 * @param {Array} demote Demoted tools
3496 * @return {string[]} List of tools
3497 */
3498 OO.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, demote ) {
3499 var i, len, included, promoted, demoted,
3500 auto = [],
3501 used = {};
3502
3503 // Collect included and not excluded tools
3504 included = OO.simpleArrayDifference( this.extract( include ), this.extract( exclude ) );
3505
3506 // Promotion
3507 promoted = this.extract( promote, used );
3508 demoted = this.extract( demote, used );
3509
3510 // Auto
3511 for ( i = 0, len = included.length; i < len; i++ ) {
3512 if ( !used[included[i]] ) {
3513 auto.push( included[i] );
3514 }
3515 }
3516
3517 return promoted.concat( auto ).concat( demoted );
3518 };
3519
3520 /**
3521 * Get a flat list of names from a list of names or groups.
3522 *
3523 * Tools can be specified in the following ways:
3524 *
3525 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
3526 * - All tools in a group: `{ group: 'group-name' }`
3527 * - All tools: `'*'`
3528 *
3529 * @private
3530 * @param {Array|string} collection List of tools
3531 * @param {Object} [used] Object with names that should be skipped as properties; extracted
3532 * names will be added as properties
3533 * @return {string[]} List of extracted names
3534 */
3535 OO.ui.ToolFactory.prototype.extract = function ( collection, used ) {
3536 var i, len, item, name, tool,
3537 names = [];
3538
3539 if ( collection === '*' ) {
3540 for ( name in this.registry ) {
3541 tool = this.registry[name];
3542 if (
3543 // Only add tools by group name when auto-add is enabled
3544 tool.static.autoAddToCatchall &&
3545 // Exclude already used tools
3546 ( !used || !used[name] )
3547 ) {
3548 names.push( name );
3549 if ( used ) {
3550 used[name] = true;
3551 }
3552 }
3553 }
3554 } else if ( $.isArray( collection ) ) {
3555 for ( i = 0, len = collection.length; i < len; i++ ) {
3556 item = collection[i];
3557 // Allow plain strings as shorthand for named tools
3558 if ( typeof item === 'string' ) {
3559 item = { name: item };
3560 }
3561 if ( OO.isPlainObject( item ) ) {
3562 if ( item.group ) {
3563 for ( name in this.registry ) {
3564 tool = this.registry[name];
3565 if (
3566 // Include tools with matching group
3567 tool.static.group === item.group &&
3568 // Only add tools by group name when auto-add is enabled
3569 tool.static.autoAddToGroup &&
3570 // Exclude already used tools
3571 ( !used || !used[name] )
3572 ) {
3573 names.push( name );
3574 if ( used ) {
3575 used[name] = true;
3576 }
3577 }
3578 }
3579 // Include tools with matching name and exclude already used tools
3580 } else if ( item.name && ( !used || !used[item.name] ) ) {
3581 names.push( item.name );
3582 if ( used ) {
3583 used[item.name] = true;
3584 }
3585 }
3586 }
3587 }
3588 }
3589 return names;
3590 };
3591
3592 /**
3593 * Factory for tool groups.
3594 *
3595 * @class
3596 * @extends OO.Factory
3597 * @constructor
3598 */
3599 OO.ui.ToolGroupFactory = function OoUiToolGroupFactory() {
3600 // Parent constructor
3601 OO.Factory.call( this );
3602
3603 var i, l,
3604 defaultClasses = this.constructor.static.getDefaultClasses();
3605
3606 // Register default toolgroups
3607 for ( i = 0, l = defaultClasses.length; i < l; i++ ) {
3608 this.register( defaultClasses[i] );
3609 }
3610 };
3611
3612 /* Setup */
3613
3614 OO.inheritClass( OO.ui.ToolGroupFactory, OO.Factory );
3615
3616 /* Static Methods */
3617
3618 /**
3619 * Get a default set of classes to be registered on construction
3620 *
3621 * @return {Function[]} Default classes
3622 */
3623 OO.ui.ToolGroupFactory.static.getDefaultClasses = function () {
3624 return [
3625 OO.ui.BarToolGroup,
3626 OO.ui.ListToolGroup,
3627 OO.ui.MenuToolGroup
3628 ];
3629 };
3630
3631 /**
3632 * Theme logic.
3633 *
3634 * @abstract
3635 * @class
3636 *
3637 * @constructor
3638 * @param {Object} [config] Configuration options
3639 */
3640 OO.ui.Theme = function OoUiTheme( config ) {
3641 // Configuration initialization
3642 config = config || {};
3643 };
3644
3645 /* Setup */
3646
3647 OO.initClass( OO.ui.Theme );
3648
3649 /* Methods */
3650
3651 /**
3652 * Get a list of classes to be applied to a widget.
3653 *
3654 * The 'on' and 'off' lists combined MUST contain keys for all classes the theme adds or removes,
3655 * otherwise state transitions will not work properly.
3656 *
3657 * @param {OO.ui.Element} element Element for which to get classes
3658 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
3659 */
3660 OO.ui.Theme.prototype.getElementClasses = function ( /* element */ ) {
3661 return { on: [], off: [] };
3662 };
3663
3664 /**
3665 * Update CSS classes provided by the theme.
3666 *
3667 * For elements with theme logic hooks, this should be called any time there's a state change.
3668 *
3669 * @param {OO.ui.Element} element Element for which to update classes
3670 * @return {Object.<string,string[]>} Categorized class names with `on` and `off` lists
3671 */
3672 OO.ui.Theme.prototype.updateElementClasses = function ( element ) {
3673 var classes = this.getElementClasses( element );
3674
3675 element.$element
3676 .removeClass( classes.off.join( ' ' ) )
3677 .addClass( classes.on.join( ' ' ) );
3678 };
3679
3680 /**
3681 * Element with a button.
3682 *
3683 * Buttons are used for controls which can be clicked. They can be configured to use tab indexing
3684 * and access keys for accessibility purposes.
3685 *
3686 * @abstract
3687 * @class
3688 *
3689 * @constructor
3690 * @param {Object} [config] Configuration options
3691 * @cfg {jQuery} [$button] Button node, assigned to #$button, omit to use a generated `<a>`
3692 * @cfg {boolean} [framed=true] Render button with a frame
3693 * @cfg {number} [tabIndex=0] Button's tab index. Use 0 to use default ordering, use -1 to prevent
3694 * tab focusing.
3695 * @cfg {string} [accessKey] Button's access key
3696 */
3697 OO.ui.ButtonElement = function OoUiButtonElement( config ) {
3698 // Configuration initialization
3699 config = config || {};
3700
3701 // Properties
3702 this.$button = null;
3703 this.framed = null;
3704 this.tabIndex = null;
3705 this.accessKey = null;
3706 this.active = false;
3707 this.onMouseUpHandler = this.onMouseUp.bind( this );
3708 this.onMouseDownHandler = this.onMouseDown.bind( this );
3709
3710 // Initialization
3711 this.$element.addClass( 'oo-ui-buttonElement' );
3712 this.toggleFramed( config.framed === undefined || config.framed );
3713 this.setTabIndex( config.tabIndex || 0 );
3714 this.setAccessKey( config.accessKey );
3715 this.setButtonElement( config.$button || this.$( '<a>' ) );
3716 };
3717
3718 /* Setup */
3719
3720 OO.initClass( OO.ui.ButtonElement );
3721
3722 /* Static Properties */
3723
3724 /**
3725 * Cancel mouse down events.
3726 *
3727 * @static
3728 * @inheritable
3729 * @property {boolean}
3730 */
3731 OO.ui.ButtonElement.static.cancelButtonMouseDownEvents = true;
3732
3733 /* Methods */
3734
3735 /**
3736 * Set the button element.
3737 *
3738 * If an element is already set, it will be cleaned up before setting up the new element.
3739 *
3740 * @param {jQuery} $button Element to use as button
3741 */
3742 OO.ui.ButtonElement.prototype.setButtonElement = function ( $button ) {
3743 if ( this.$button ) {
3744 this.$button
3745 .removeClass( 'oo-ui-buttonElement-button' )
3746 .removeAttr( 'role accesskey tabindex' )
3747 .off( 'mousedown', this.onMouseDownHandler );
3748 }
3749
3750 this.$button = $button
3751 .addClass( 'oo-ui-buttonElement-button' )
3752 .attr( { role: 'button', accesskey: this.accessKey, tabindex: this.tabIndex } )
3753 .on( 'mousedown', this.onMouseDownHandler );
3754 };
3755
3756 /**
3757 * Handles mouse down events.
3758 *
3759 * @param {jQuery.Event} e Mouse down event
3760 */
3761 OO.ui.ButtonElement.prototype.onMouseDown = function ( e ) {
3762 if ( this.isDisabled() || e.which !== 1 ) {
3763 return false;
3764 }
3765 // Remove the tab-index while the button is down to prevent the button from stealing focus
3766 this.$button.removeAttr( 'tabindex' );
3767 this.$element.addClass( 'oo-ui-buttonElement-pressed' );
3768 // Run the mouseup handler no matter where the mouse is when the button is let go, so we can
3769 // reliably reapply the tabindex and remove the pressed class
3770 this.getElementDocument().addEventListener( 'mouseup', this.onMouseUpHandler, true );
3771 // Prevent change of focus unless specifically configured otherwise
3772 if ( this.constructor.static.cancelButtonMouseDownEvents ) {
3773 return false;
3774 }
3775 };
3776
3777 /**
3778 * Handles mouse up events.
3779 *
3780 * @param {jQuery.Event} e Mouse up event
3781 */
3782 OO.ui.ButtonElement.prototype.onMouseUp = function ( e ) {
3783 if ( this.isDisabled() || e.which !== 1 ) {
3784 return false;
3785 }
3786 // Restore the tab-index after the button is up to restore the button's accessibility
3787 this.$button.attr( 'tabindex', this.tabIndex );
3788 this.$element.removeClass( 'oo-ui-buttonElement-pressed' );
3789 // Stop listening for mouseup, since we only needed this once
3790 this.getElementDocument().removeEventListener( 'mouseup', this.onMouseUpHandler, true );
3791 };
3792
3793 /**
3794 * Check if button has a frame.
3795 *
3796 * @return {boolean} Button is framed
3797 */
3798 OO.ui.ButtonElement.prototype.isFramed = function () {
3799 return this.framed;
3800 };
3801
3802 /**
3803 * Toggle frame.
3804 *
3805 * @param {boolean} [framed] Make button framed, omit to toggle
3806 * @chainable
3807 */
3808 OO.ui.ButtonElement.prototype.toggleFramed = function ( framed ) {
3809 framed = framed === undefined ? !this.framed : !!framed;
3810 if ( framed !== this.framed ) {
3811 this.framed = framed;
3812 this.$element
3813 .toggleClass( 'oo-ui-buttonElement-frameless', !framed )
3814 .toggleClass( 'oo-ui-buttonElement-framed', framed );
3815 this.updateThemeClasses();
3816 }
3817
3818 return this;
3819 };
3820
3821 /**
3822 * Set tab index.
3823 *
3824 * @param {number|null} tabIndex Button's tab index, use null to remove
3825 * @chainable
3826 */
3827 OO.ui.ButtonElement.prototype.setTabIndex = function ( tabIndex ) {
3828 tabIndex = typeof tabIndex === 'number' && tabIndex >= 0 ? tabIndex : null;
3829
3830 if ( this.tabIndex !== tabIndex ) {
3831 if ( this.$button ) {
3832 if ( tabIndex !== null ) {
3833 this.$button.attr( 'tabindex', tabIndex );
3834 } else {
3835 this.$button.removeAttr( 'tabindex' );
3836 }
3837 }
3838 this.tabIndex = tabIndex;
3839 }
3840
3841 return this;
3842 };
3843
3844 /**
3845 * Set access key.
3846 *
3847 * @param {string} accessKey Button's access key, use empty string to remove
3848 * @chainable
3849 */
3850 OO.ui.ButtonElement.prototype.setAccessKey = function ( accessKey ) {
3851 accessKey = typeof accessKey === 'string' && accessKey.length ? accessKey : null;
3852
3853 if ( this.accessKey !== accessKey ) {
3854 if ( this.$button ) {
3855 if ( accessKey !== null ) {
3856 this.$button.attr( 'accesskey', accessKey );
3857 } else {
3858 this.$button.removeAttr( 'accesskey' );
3859 }
3860 }
3861 this.accessKey = accessKey;
3862 }
3863
3864 return this;
3865 };
3866
3867 /**
3868 * Set active state.
3869 *
3870 * @param {boolean} [value] Make button active
3871 * @chainable
3872 */
3873 OO.ui.ButtonElement.prototype.setActive = function ( value ) {
3874 this.$element.toggleClass( 'oo-ui-buttonElement-active', !!value );
3875 return this;
3876 };
3877
3878 /**
3879 * Element containing a sequence of child elements.
3880 *
3881 * @abstract
3882 * @class
3883 *
3884 * @constructor
3885 * @param {Object} [config] Configuration options
3886 * @cfg {jQuery} [$group] Container node, assigned to #$group, omit to use a generated `<div>`
3887 */
3888 OO.ui.GroupElement = function OoUiGroupElement( config ) {
3889 // Configuration initialization
3890 config = config || {};
3891
3892 // Properties
3893 this.$group = null;
3894 this.items = [];
3895 this.aggregateItemEvents = {};
3896
3897 // Initialization
3898 this.setGroupElement( config.$group || this.$( '<div>' ) );
3899 };
3900
3901 /* Methods */
3902
3903 /**
3904 * Set the group element.
3905 *
3906 * If an element is already set, items will be moved to the new element.
3907 *
3908 * @param {jQuery} $group Element to use as group
3909 */
3910 OO.ui.GroupElement.prototype.setGroupElement = function ( $group ) {
3911 var i, len;
3912
3913 this.$group = $group;
3914 for ( i = 0, len = this.items.length; i < len; i++ ) {
3915 this.$group.append( this.items[i].$element );
3916 }
3917 };
3918
3919 /**
3920 * Check if there are no items.
3921 *
3922 * @return {boolean} Group is empty
3923 */
3924 OO.ui.GroupElement.prototype.isEmpty = function () {
3925 return !this.items.length;
3926 };
3927
3928 /**
3929 * Get items.
3930 *
3931 * @return {OO.ui.Element[]} Items
3932 */
3933 OO.ui.GroupElement.prototype.getItems = function () {
3934 return this.items.slice( 0 );
3935 };
3936
3937 /**
3938 * Get an item by its data.
3939 *
3940 * Data is compared by a hash of its value. Only the first item with matching data will be returned.
3941 *
3942 * @param {Object} data Item data to search for
3943 * @return {OO.ui.Element|null} Item with equivalent data, `null` if none exists
3944 */
3945 OO.ui.GroupElement.prototype.getItemFromData = function ( data ) {
3946 var i, len, item,
3947 hash = OO.getHash( data );
3948
3949 for ( i = 0, len = this.items.length; i < len; i++ ) {
3950 item = this.items[i];
3951 if ( hash === OO.getHash( item.getData() ) ) {
3952 return item;
3953 }
3954 }
3955
3956 return null;
3957 };
3958
3959 /**
3960 * Get items by their data.
3961 *
3962 * Data is compared by a hash of its value. All items with matching data will be returned.
3963 *
3964 * @param {Object} data Item data to search for
3965 * @return {OO.ui.Element[]} Items with equivalent data
3966 */
3967 OO.ui.GroupElement.prototype.getItemsFromData = function ( data ) {
3968 var i, len, item,
3969 hash = OO.getHash( data ),
3970 items = [];
3971
3972 for ( i = 0, len = this.items.length; i < len; i++ ) {
3973 item = this.items[i];
3974 if ( hash === OO.getHash( item.getData() ) ) {
3975 items.push( item );
3976 }
3977 }
3978
3979 return items;
3980 };
3981
3982 /**
3983 * Add an aggregate item event.
3984 *
3985 * Aggregated events are listened to on each item and then emitted by the group under a new name,
3986 * and with an additional leading parameter containing the item that emitted the original event.
3987 * Other arguments that were emitted from the original event are passed through.
3988 *
3989 * @param {Object.<string,string|null>} events Aggregate events emitted by group, keyed by item
3990 * event, use null value to remove aggregation
3991 * @throws {Error} If aggregation already exists
3992 */
3993 OO.ui.GroupElement.prototype.aggregate = function ( events ) {
3994 var i, len, item, add, remove, itemEvent, groupEvent;
3995
3996 for ( itemEvent in events ) {
3997 groupEvent = events[itemEvent];
3998
3999 // Remove existing aggregated event
4000 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4001 // Don't allow duplicate aggregations
4002 if ( groupEvent ) {
4003 throw new Error( 'Duplicate item event aggregation for ' + itemEvent );
4004 }
4005 // Remove event aggregation from existing items
4006 for ( i = 0, len = this.items.length; i < len; i++ ) {
4007 item = this.items[i];
4008 if ( item.connect && item.disconnect ) {
4009 remove = {};
4010 remove[itemEvent] = [ 'emit', groupEvent, item ];
4011 item.disconnect( this, remove );
4012 }
4013 }
4014 // Prevent future items from aggregating event
4015 delete this.aggregateItemEvents[itemEvent];
4016 }
4017
4018 // Add new aggregate event
4019 if ( groupEvent ) {
4020 // Make future items aggregate event
4021 this.aggregateItemEvents[itemEvent] = groupEvent;
4022 // Add event aggregation to existing items
4023 for ( i = 0, len = this.items.length; i < len; i++ ) {
4024 item = this.items[i];
4025 if ( item.connect && item.disconnect ) {
4026 add = {};
4027 add[itemEvent] = [ 'emit', groupEvent, item ];
4028 item.connect( this, add );
4029 }
4030 }
4031 }
4032 }
4033 };
4034
4035 /**
4036 * Add items.
4037 *
4038 * Adding an existing item will move it.
4039 *
4040 * @param {OO.ui.Element[]} items Items
4041 * @param {number} [index] Index to insert items at
4042 * @chainable
4043 */
4044 OO.ui.GroupElement.prototype.addItems = function ( items, index ) {
4045 var i, len, item, event, events, currentIndex,
4046 itemElements = [];
4047
4048 for ( i = 0, len = items.length; i < len; i++ ) {
4049 item = items[i];
4050
4051 // Check if item exists then remove it first, effectively "moving" it
4052 currentIndex = $.inArray( item, this.items );
4053 if ( currentIndex >= 0 ) {
4054 this.removeItems( [ item ] );
4055 // Adjust index to compensate for removal
4056 if ( currentIndex < index ) {
4057 index--;
4058 }
4059 }
4060 // Add the item
4061 if ( item.connect && item.disconnect && !$.isEmptyObject( this.aggregateItemEvents ) ) {
4062 events = {};
4063 for ( event in this.aggregateItemEvents ) {
4064 events[event] = [ 'emit', this.aggregateItemEvents[event], item ];
4065 }
4066 item.connect( this, events );
4067 }
4068 item.setElementGroup( this );
4069 itemElements.push( item.$element.get( 0 ) );
4070 }
4071
4072 if ( index === undefined || index < 0 || index >= this.items.length ) {
4073 this.$group.append( itemElements );
4074 this.items.push.apply( this.items, items );
4075 } else if ( index === 0 ) {
4076 this.$group.prepend( itemElements );
4077 this.items.unshift.apply( this.items, items );
4078 } else {
4079 this.items[index].$element.before( itemElements );
4080 this.items.splice.apply( this.items, [ index, 0 ].concat( items ) );
4081 }
4082
4083 return this;
4084 };
4085
4086 /**
4087 * Remove items.
4088 *
4089 * Items will be detached, not removed, so they can be used later.
4090 *
4091 * @param {OO.ui.Element[]} items Items to remove
4092 * @chainable
4093 */
4094 OO.ui.GroupElement.prototype.removeItems = function ( items ) {
4095 var i, len, item, index, remove, itemEvent;
4096
4097 // Remove specific items
4098 for ( i = 0, len = items.length; i < len; i++ ) {
4099 item = items[i];
4100 index = $.inArray( item, this.items );
4101 if ( index !== -1 ) {
4102 if (
4103 item.connect && item.disconnect &&
4104 !$.isEmptyObject( this.aggregateItemEvents )
4105 ) {
4106 remove = {};
4107 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4108 remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ];
4109 }
4110 item.disconnect( this, remove );
4111 }
4112 item.setElementGroup( null );
4113 this.items.splice( index, 1 );
4114 item.$element.detach();
4115 }
4116 }
4117
4118 return this;
4119 };
4120
4121 /**
4122 * Clear all items.
4123 *
4124 * Items will be detached, not removed, so they can be used later.
4125 *
4126 * @chainable
4127 */
4128 OO.ui.GroupElement.prototype.clearItems = function () {
4129 var i, len, item, remove, itemEvent;
4130
4131 // Remove all items
4132 for ( i = 0, len = this.items.length; i < len; i++ ) {
4133 item = this.items[i];
4134 if (
4135 item.connect && item.disconnect &&
4136 !$.isEmptyObject( this.aggregateItemEvents )
4137 ) {
4138 remove = {};
4139 if ( Object.prototype.hasOwnProperty.call( this.aggregateItemEvents, itemEvent ) ) {
4140 remove[itemEvent] = [ 'emit', this.aggregateItemEvents[itemEvent], item ];
4141 }
4142 item.disconnect( this, remove );
4143 }
4144 item.setElementGroup( null );
4145 item.$element.detach();
4146 }
4147
4148 this.items = [];
4149 return this;
4150 };
4151
4152 /**
4153 * A mixin for an element that can be dragged and dropped.
4154 * Use in conjunction with DragGroupWidget
4155 *
4156 * @abstract
4157 * @class
4158 *
4159 * @constructor
4160 */
4161 OO.ui.DraggableElement = function OoUiDraggableElement() {
4162 // Properties
4163 this.index = null;
4164
4165 // Initialize and events
4166 this.$element
4167 .attr( 'draggable', true )
4168 .addClass( 'oo-ui-draggableElement' )
4169 .on( {
4170 dragstart: this.onDragStart.bind( this ),
4171 dragover: this.onDragOver.bind( this ),
4172 dragend: this.onDragEnd.bind( this ),
4173 drop: this.onDrop.bind( this )
4174 } );
4175 };
4176
4177 /* Events */
4178
4179 /**
4180 * @event dragstart
4181 * @param {OO.ui.DraggableElement} item Dragging item
4182 */
4183
4184 /**
4185 * @event dragend
4186 */
4187
4188 /**
4189 * @event drop
4190 */
4191
4192 /* Methods */
4193
4194 /**
4195 * Respond to dragstart event.
4196 * @param {jQuery.Event} event jQuery event
4197 * @fires dragstart
4198 */
4199 OO.ui.DraggableElement.prototype.onDragStart = function ( e ) {
4200 var dataTransfer = e.originalEvent.dataTransfer;
4201 // Define drop effect
4202 dataTransfer.dropEffect = 'none';
4203 dataTransfer.effectAllowed = 'move';
4204 // We must set up a dataTransfer data property or Firefox seems to
4205 // ignore the fact the element is draggable.
4206 try {
4207 dataTransfer.setData( 'application-x/OOjs-UI-draggable', this.getIndex() );
4208 } catch ( err ) {
4209 // The above is only for firefox. No need to set a catch clause
4210 // if it fails, move on.
4211 }
4212 // Add dragging class
4213 this.$element.addClass( 'oo-ui-draggableElement-dragging' );
4214 // Emit event
4215 this.emit( 'dragstart', this );
4216 return true;
4217 };
4218
4219 /**
4220 * Respond to dragend event.
4221 * @fires dragend
4222 */
4223 OO.ui.DraggableElement.prototype.onDragEnd = function () {
4224 this.$element.removeClass( 'oo-ui-draggableElement-dragging' );
4225 this.emit( 'dragend' );
4226 };
4227
4228 /**
4229 * Handle drop event.
4230 * @param {jQuery.Event} event jQuery event
4231 * @fires drop
4232 */
4233 OO.ui.DraggableElement.prototype.onDrop = function ( e ) {
4234 e.preventDefault();
4235 this.emit( 'drop', e );
4236 };
4237
4238 /**
4239 * In order for drag/drop to work, the dragover event must
4240 * return false and stop propogation.
4241 */
4242 OO.ui.DraggableElement.prototype.onDragOver = function ( e ) {
4243 e.preventDefault();
4244 };
4245
4246 /**
4247 * Set item index.
4248 * Store it in the DOM so we can access from the widget drag event
4249 * @param {number} Item index
4250 */
4251 OO.ui.DraggableElement.prototype.setIndex = function ( index ) {
4252 if ( this.index !== index ) {
4253 this.index = index;
4254 this.$element.data( 'index', index );
4255 }
4256 };
4257
4258 /**
4259 * Get item index
4260 * @return {number} Item index
4261 */
4262 OO.ui.DraggableElement.prototype.getIndex = function () {
4263 return this.index;
4264 };
4265
4266 /**
4267 * Element containing a sequence of child elements that can be dragged
4268 * and dropped.
4269 *
4270 * @abstract
4271 * @class
4272 *
4273 * @constructor
4274 * @param {Object} [config] Configuration options
4275 * @cfg {jQuery} [$group] Container node, assigned to #$group, omit to use a generated `<div>`
4276 * @cfg {string} [orientation] Item orientation, 'horizontal' or 'vertical'. Defaults to 'vertical'
4277 */
4278 OO.ui.DraggableGroupElement = function OoUiDraggableGroupElement( config ) {
4279 // Configuration initialization
4280 config = config || {};
4281
4282 // Parent constructor
4283 OO.ui.GroupElement.call( this, config );
4284
4285 // Properties
4286 this.orientation = config.orientation || 'vertical';
4287 this.dragItem = null;
4288 this.itemDragOver = null;
4289 this.itemKeys = {};
4290 this.sideInsertion = '';
4291
4292 // Events
4293 this.aggregate( {
4294 dragstart: 'itemDragStart',
4295 dragend: 'itemDragEnd',
4296 drop: 'itemDrop'
4297 } );
4298 this.connect( this, {
4299 itemDragStart: 'onItemDragStart',
4300 itemDrop: 'onItemDrop',
4301 itemDragEnd: 'onItemDragEnd'
4302 } );
4303 this.$element.on( {
4304 dragover: $.proxy( this.onDragOver, this ),
4305 dragleave: $.proxy( this.onDragLeave, this )
4306 } );
4307
4308 // Initialize
4309 if ( $.isArray( config.items ) ) {
4310 this.addItems( config.items );
4311 }
4312 this.$placeholder = $( '<div>' )
4313 .addClass( 'oo-ui-draggableGroupElement-placeholder' );
4314 this.$element
4315 .addClass( 'oo-ui-draggableGroupElement' )
4316 .append( this.$status )
4317 .toggleClass( 'oo-ui-draggableGroupElement-horizontal', this.orientation === 'horizontal' )
4318 .prepend( this.$placeholder );
4319 };
4320
4321 /* Setup */
4322 OO.mixinClass( OO.ui.DraggableGroupElement, OO.ui.GroupElement );
4323
4324 /* Events */
4325
4326 /**
4327 * @event reorder
4328 * @param {OO.ui.DraggableElement} item Reordered item
4329 * @param {number} [newIndex] New index for the item
4330 */
4331
4332 /* Methods */
4333
4334 /**
4335 * Respond to item drag start event
4336 * @param {OO.ui.DraggableElement} item Dragged item
4337 */
4338 OO.ui.DraggableGroupElement.prototype.onItemDragStart = function ( item ) {
4339 var i, len;
4340
4341 // Map the index of each object
4342 for ( i = 0, len = this.items.length; i < len; i++ ) {
4343 this.items[i].setIndex( i );
4344 }
4345
4346 if ( this.orientation === 'horizontal' ) {
4347 // Set the height of the indicator
4348 this.$placeholder.css( {
4349 height: item.$element.outerHeight(),
4350 width: 2
4351 } );
4352 } else {
4353 // Set the width of the indicator
4354 this.$placeholder.css( {
4355 height: 2,
4356 width: item.$element.outerWidth()
4357 } );
4358 }
4359 this.setDragItem( item );
4360 };
4361
4362 /**
4363 * Respond to item drag end event
4364 */
4365 OO.ui.DraggableGroupElement.prototype.onItemDragEnd = function () {
4366 this.unsetDragItem();
4367 return false;
4368 };
4369
4370 /**
4371 * Handle drop event and switch the order of the items accordingly
4372 * @param {OO.ui.DraggableElement} item Dropped item
4373 * @fires reorder
4374 */
4375 OO.ui.DraggableGroupElement.prototype.onItemDrop = function ( item ) {
4376 var toIndex = item.getIndex();
4377 // Check if the dropped item is from the current group
4378 // TODO: Figure out a way to configure a list of legally droppable
4379 // elements even if they are not yet in the list
4380 if ( this.getDragItem() ) {
4381 // If the insertion point is 'after', the insertion index
4382 // is shifted to the right (or to the left in RTL, hence 'after')
4383 if ( this.sideInsertion === 'after' ) {
4384 toIndex++;
4385 }
4386 // Emit change event
4387 this.emit( 'reorder', this.getDragItem(), toIndex );
4388 }
4389 // Return false to prevent propogation
4390 return false;
4391 };
4392
4393 /**
4394 * Handle dragleave event.
4395 */
4396 OO.ui.DraggableGroupElement.prototype.onDragLeave = function () {
4397 // This means the item was dragged outside the widget
4398 this.$placeholder
4399 .css( 'left', 0 )
4400 .hide();
4401 };
4402
4403 /**
4404 * Respond to dragover event
4405 * @param {jQuery.Event} event Event details
4406 */
4407 OO.ui.DraggableGroupElement.prototype.onDragOver = function ( e ) {
4408 var dragOverObj, $optionWidget, itemOffset, itemMidpoint, itemBoundingRect,
4409 itemSize, cssOutput, dragPosition, itemIndex, itemPosition,
4410 clientX = e.originalEvent.clientX,
4411 clientY = e.originalEvent.clientY;
4412
4413 // Get the OptionWidget item we are dragging over
4414 dragOverObj = this.getElementDocument().elementFromPoint( clientX, clientY );
4415 $optionWidget = $( dragOverObj ).closest( '.oo-ui-draggableElement' );
4416 if ( $optionWidget[0] ) {
4417 itemOffset = $optionWidget.offset();
4418 itemBoundingRect = $optionWidget[0].getBoundingClientRect();
4419 itemPosition = $optionWidget.position();
4420 itemIndex = $optionWidget.data( 'index' );
4421 }
4422
4423 if (
4424 itemOffset &&
4425 this.isDragging() &&
4426 itemIndex !== this.getDragItem().getIndex()
4427 ) {
4428 if ( this.orientation === 'horizontal' ) {
4429 // Calculate where the mouse is relative to the item width
4430 itemSize = itemBoundingRect.width;
4431 itemMidpoint = itemBoundingRect.left + itemSize / 2;
4432 dragPosition = clientX;
4433 // Which side of the item we hover over will dictate
4434 // where the placeholder will appear, on the left or
4435 // on the right
4436 cssOutput = {
4437 left: dragPosition < itemMidpoint ? itemPosition.left : itemPosition.left + itemSize,
4438 top: itemPosition.top
4439 };
4440 } else {
4441 // Calculate where the mouse is relative to the item height
4442 itemSize = itemBoundingRect.height;
4443 itemMidpoint = itemBoundingRect.top + itemSize / 2;
4444 dragPosition = clientY;
4445 // Which side of the item we hover over will dictate
4446 // where the placeholder will appear, on the top or
4447 // on the bottom
4448 cssOutput = {
4449 top: dragPosition < itemMidpoint ? itemPosition.top : itemPosition.top + itemSize,
4450 left: itemPosition.left
4451 };
4452 }
4453 // Store whether we are before or after an item to rearrange
4454 // For horizontal layout, we need to account for RTL, as this is flipped
4455 if ( this.orientation === 'horizontal' && this.$element.css( 'direction' ) === 'rtl' ) {
4456 this.sideInsertion = dragPosition < itemMidpoint ? 'after' : 'before';
4457 } else {
4458 this.sideInsertion = dragPosition < itemMidpoint ? 'before' : 'after';
4459 }
4460 // Add drop indicator between objects
4461 if ( this.sideInsertion ) {
4462 this.$placeholder
4463 .css( cssOutput )
4464 .show();
4465 } else {
4466 this.$placeholder
4467 .css( {
4468 left: 0,
4469 top: 0
4470 } )
4471 .hide();
4472 }
4473 } else {
4474 // This means the item was dragged outside the widget
4475 this.$placeholder
4476 .css( 'left', 0 )
4477 .hide();
4478 }
4479 // Prevent default
4480 e.preventDefault();
4481 };
4482
4483 /**
4484 * Set a dragged item
4485 * @param {OO.ui.DraggableElement} item Dragged item
4486 */
4487 OO.ui.DraggableGroupElement.prototype.setDragItem = function ( item ) {
4488 this.dragItem = item;
4489 };
4490
4491 /**
4492 * Unset the current dragged item
4493 */
4494 OO.ui.DraggableGroupElement.prototype.unsetDragItem = function () {
4495 this.dragItem = null;
4496 this.itemDragOver = null;
4497 this.$placeholder.hide();
4498 this.sideInsertion = '';
4499 };
4500
4501 /**
4502 * Get the current dragged item
4503 * @return {OO.ui.DraggableElement|null} item Dragged item or null if no item is dragged
4504 */
4505 OO.ui.DraggableGroupElement.prototype.getDragItem = function () {
4506 return this.dragItem;
4507 };
4508
4509 /**
4510 * Check if there's an item being dragged.
4511 * @return {Boolean} Item is being dragged
4512 */
4513 OO.ui.DraggableGroupElement.prototype.isDragging = function () {
4514 return this.getDragItem() !== null;
4515 };
4516
4517 /**
4518 * Element containing an icon.
4519 *
4520 * Icons are graphics, about the size of normal text. They can be used to aid the user in locating
4521 * a control or convey information in a more space efficient way. Icons should rarely be used
4522 * without labels; such as in a toolbar where space is at a premium or within a context where the
4523 * meaning is very clear to the user.
4524 *
4525 * @abstract
4526 * @class
4527 *
4528 * @constructor
4529 * @param {Object} [config] Configuration options
4530 * @cfg {jQuery} [$icon] Icon node, assigned to #$icon, omit to use a generated `<span>`
4531 * @cfg {Object|string} [icon=''] Symbolic icon name, or map of icon names keyed by language ID;
4532 * use the 'default' key to specify the icon to be used when there is no icon in the user's
4533 * language
4534 * @cfg {string} [iconTitle] Icon title text or a function that returns text
4535 */
4536 OO.ui.IconElement = function OoUiIconElement( config ) {
4537 // Configuration initialization
4538 config = config || {};
4539
4540 // Properties
4541 this.$icon = null;
4542 this.icon = null;
4543 this.iconTitle = null;
4544
4545 // Initialization
4546 this.setIcon( config.icon || this.constructor.static.icon );
4547 this.setIconTitle( config.iconTitle || this.constructor.static.iconTitle );
4548 this.setIconElement( config.$icon || this.$( '<span>' ) );
4549 };
4550
4551 /* Setup */
4552
4553 OO.initClass( OO.ui.IconElement );
4554
4555 /* Static Properties */
4556
4557 /**
4558 * Icon.
4559 *
4560 * Value should be the unique portion of an icon CSS class name, such as 'up' for 'oo-ui-icon-up'.
4561 *
4562 * For i18n purposes, this property can be an object containing a `default` icon name property and
4563 * additional icon names keyed by language code.
4564 *
4565 * Example of i18n icon definition:
4566 * { default: 'bold-a', en: 'bold-b', de: 'bold-f' }
4567 *
4568 * @static
4569 * @inheritable
4570 * @property {Object|string} Symbolic icon name, or map of icon names keyed by language ID;
4571 * use the 'default' key to specify the icon to be used when there is no icon in the user's
4572 * language
4573 */
4574 OO.ui.IconElement.static.icon = null;
4575
4576 /**
4577 * Icon title.
4578 *
4579 * @static
4580 * @inheritable
4581 * @property {string|Function|null} Icon title text, a function that returns text or null for no
4582 * icon title
4583 */
4584 OO.ui.IconElement.static.iconTitle = null;
4585
4586 /* Methods */
4587
4588 /**
4589 * Set the icon element.
4590 *
4591 * If an element is already set, it will be cleaned up before setting up the new element.
4592 *
4593 * @param {jQuery} $icon Element to use as icon
4594 */
4595 OO.ui.IconElement.prototype.setIconElement = function ( $icon ) {
4596 if ( this.$icon ) {
4597 this.$icon
4598 .removeClass( 'oo-ui-iconElement-icon oo-ui-icon-' + this.icon )
4599 .removeAttr( 'title' );
4600 }
4601
4602 this.$icon = $icon
4603 .addClass( 'oo-ui-iconElement-icon' )
4604 .toggleClass( 'oo-ui-icon-' + this.icon, !!this.icon );
4605 if ( this.iconTitle !== null ) {
4606 this.$icon.attr( 'title', this.iconTitle );
4607 }
4608 };
4609
4610 /**
4611 * Set icon name.
4612 *
4613 * @param {Object|string|null} icon Symbolic icon name, or map of icon names keyed by language ID;
4614 * use the 'default' key to specify the icon to be used when there is no icon in the user's
4615 * language, use null to remove icon
4616 * @chainable
4617 */
4618 OO.ui.IconElement.prototype.setIcon = function ( icon ) {
4619 icon = OO.isPlainObject( icon ) ? OO.ui.getLocalValue( icon, null, 'default' ) : icon;
4620 icon = typeof icon === 'string' && icon.trim().length ? icon.trim() : null;
4621
4622 if ( this.icon !== icon ) {
4623 if ( this.$icon ) {
4624 if ( this.icon !== null ) {
4625 this.$icon.removeClass( 'oo-ui-icon-' + this.icon );
4626 }
4627 if ( icon !== null ) {
4628 this.$icon.addClass( 'oo-ui-icon-' + icon );
4629 }
4630 }
4631 this.icon = icon;
4632 }
4633
4634 this.$element.toggleClass( 'oo-ui-iconElement', !!this.icon );
4635 this.updateThemeClasses();
4636
4637 return this;
4638 };
4639
4640 /**
4641 * Set icon title.
4642 *
4643 * @param {string|Function|null} icon Icon title text, a function that returns text or null
4644 * for no icon title
4645 * @chainable
4646 */
4647 OO.ui.IconElement.prototype.setIconTitle = function ( iconTitle ) {
4648 iconTitle = typeof iconTitle === 'function' ||
4649 ( typeof iconTitle === 'string' && iconTitle.length ) ?
4650 OO.ui.resolveMsg( iconTitle ) : null;
4651
4652 if ( this.iconTitle !== iconTitle ) {
4653 this.iconTitle = iconTitle;
4654 if ( this.$icon ) {
4655 if ( this.iconTitle !== null ) {
4656 this.$icon.attr( 'title', iconTitle );
4657 } else {
4658 this.$icon.removeAttr( 'title' );
4659 }
4660 }
4661 }
4662
4663 return this;
4664 };
4665
4666 /**
4667 * Get icon name.
4668 *
4669 * @return {string} Icon name
4670 */
4671 OO.ui.IconElement.prototype.getIcon = function () {
4672 return this.icon;
4673 };
4674
4675 /**
4676 * Get icon title.
4677 *
4678 * @return {string} Icon title text
4679 */
4680 OO.ui.IconElement.prototype.getIconTitle = function () {
4681 return this.iconTitle;
4682 };
4683
4684 /**
4685 * Element containing an indicator.
4686 *
4687 * Indicators are graphics, smaller than normal text. They can be used to describe unique status or
4688 * behavior. Indicators should only be used in exceptional cases; such as a button that opens a menu
4689 * instead of performing an action directly, or an item in a list which has errors that need to be
4690 * resolved.
4691 *
4692 * @abstract
4693 * @class
4694 *
4695 * @constructor
4696 * @param {Object} [config] Configuration options
4697 * @cfg {jQuery} [$indicator] Indicator node, assigned to #$indicator, omit to use a generated
4698 * `<span>`
4699 * @cfg {string} [indicator] Symbolic indicator name
4700 * @cfg {string} [indicatorTitle] Indicator title text or a function that returns text
4701 */
4702 OO.ui.IndicatorElement = function OoUiIndicatorElement( config ) {
4703 // Configuration initialization
4704 config = config || {};
4705
4706 // Properties
4707 this.$indicator = null;
4708 this.indicator = null;
4709 this.indicatorTitle = null;
4710
4711 // Initialization
4712 this.setIndicator( config.indicator || this.constructor.static.indicator );
4713 this.setIndicatorTitle( config.indicatorTitle || this.constructor.static.indicatorTitle );
4714 this.setIndicatorElement( config.$indicator || this.$( '<span>' ) );
4715 };
4716
4717 /* Setup */
4718
4719 OO.initClass( OO.ui.IndicatorElement );
4720
4721 /* Static Properties */
4722
4723 /**
4724 * indicator.
4725 *
4726 * @static
4727 * @inheritable
4728 * @property {string|null} Symbolic indicator name
4729 */
4730 OO.ui.IndicatorElement.static.indicator = null;
4731
4732 /**
4733 * Indicator title.
4734 *
4735 * @static
4736 * @inheritable
4737 * @property {string|Function|null} Indicator title text, a function that returns text or null for no
4738 * indicator title
4739 */
4740 OO.ui.IndicatorElement.static.indicatorTitle = null;
4741
4742 /* Methods */
4743
4744 /**
4745 * Set the indicator element.
4746 *
4747 * If an element is already set, it will be cleaned up before setting up the new element.
4748 *
4749 * @param {jQuery} $indicator Element to use as indicator
4750 */
4751 OO.ui.IndicatorElement.prototype.setIndicatorElement = function ( $indicator ) {
4752 if ( this.$indicator ) {
4753 this.$indicator
4754 .removeClass( 'oo-ui-indicatorElement-indicator oo-ui-indicator-' + this.indicator )
4755 .removeAttr( 'title' );
4756 }
4757
4758 this.$indicator = $indicator
4759 .addClass( 'oo-ui-indicatorElement-indicator' )
4760 .toggleClass( 'oo-ui-indicator-' + this.indicator, !!this.indicator );
4761 if ( this.indicatorTitle !== null ) {
4762 this.$indicatorTitle.attr( 'title', this.indicatorTitle );
4763 }
4764 };
4765
4766 /**
4767 * Set indicator name.
4768 *
4769 * @param {string|null} indicator Symbolic name of indicator to use or null for no indicator
4770 * @chainable
4771 */
4772 OO.ui.IndicatorElement.prototype.setIndicator = function ( indicator ) {
4773 indicator = typeof indicator === 'string' && indicator.length ? indicator.trim() : null;
4774
4775 if ( this.indicator !== indicator ) {
4776 if ( this.$indicator ) {
4777 if ( this.indicator !== null ) {
4778 this.$indicator.removeClass( 'oo-ui-indicator-' + this.indicator );
4779 }
4780 if ( indicator !== null ) {
4781 this.$indicator.addClass( 'oo-ui-indicator-' + indicator );
4782 }
4783 }
4784 this.indicator = indicator;
4785 }
4786
4787 this.$element.toggleClass( 'oo-ui-indicatorElement', !!this.indicator );
4788 this.updateThemeClasses();
4789
4790 return this;
4791 };
4792
4793 /**
4794 * Set indicator title.
4795 *
4796 * @param {string|Function|null} indicator Indicator title text, a function that returns text or
4797 * null for no indicator title
4798 * @chainable
4799 */
4800 OO.ui.IndicatorElement.prototype.setIndicatorTitle = function ( indicatorTitle ) {
4801 indicatorTitle = typeof indicatorTitle === 'function' ||
4802 ( typeof indicatorTitle === 'string' && indicatorTitle.length ) ?
4803 OO.ui.resolveMsg( indicatorTitle ) : null;
4804
4805 if ( this.indicatorTitle !== indicatorTitle ) {
4806 this.indicatorTitle = indicatorTitle;
4807 if ( this.$indicator ) {
4808 if ( this.indicatorTitle !== null ) {
4809 this.$indicator.attr( 'title', indicatorTitle );
4810 } else {
4811 this.$indicator.removeAttr( 'title' );
4812 }
4813 }
4814 }
4815
4816 return this;
4817 };
4818
4819 /**
4820 * Get indicator name.
4821 *
4822 * @return {string} Symbolic name of indicator
4823 */
4824 OO.ui.IndicatorElement.prototype.getIndicator = function () {
4825 return this.indicator;
4826 };
4827
4828 /**
4829 * Get indicator title.
4830 *
4831 * @return {string} Indicator title text
4832 */
4833 OO.ui.IndicatorElement.prototype.getIndicatorTitle = function () {
4834 return this.indicatorTitle;
4835 };
4836
4837 /**
4838 * Element containing a label.
4839 *
4840 * @abstract
4841 * @class
4842 *
4843 * @constructor
4844 * @param {Object} [config] Configuration options
4845 * @cfg {jQuery} [$label] Label node, assigned to #$label, omit to use a generated `<span>`
4846 * @cfg {jQuery|string|Function} [label] Label nodes, text or a function that returns nodes or text
4847 * @cfg {boolean} [autoFitLabel=true] Whether to fit the label or not.
4848 */
4849 OO.ui.LabelElement = function OoUiLabelElement( config ) {
4850 // Configuration initialization
4851 config = config || {};
4852
4853 // Properties
4854 this.$label = null;
4855 this.label = null;
4856 this.autoFitLabel = config.autoFitLabel === undefined || !!config.autoFitLabel;
4857
4858 // Initialization
4859 this.setLabel( config.label || this.constructor.static.label );
4860 this.setLabelElement( config.$label || this.$( '<span>' ) );
4861 };
4862
4863 /* Setup */
4864
4865 OO.initClass( OO.ui.LabelElement );
4866
4867 /* Static Properties */
4868
4869 /**
4870 * Label.
4871 *
4872 * @static
4873 * @inheritable
4874 * @property {string|Function|null} Label text; a function that returns nodes or text; or null for
4875 * no label
4876 */
4877 OO.ui.LabelElement.static.label = null;
4878
4879 /* Methods */
4880
4881 /**
4882 * Set the label element.
4883 *
4884 * If an element is already set, it will be cleaned up before setting up the new element.
4885 *
4886 * @param {jQuery} $label Element to use as label
4887 */
4888 OO.ui.LabelElement.prototype.setLabelElement = function ( $label ) {
4889 if ( this.$label ) {
4890 this.$label.removeClass( 'oo-ui-labelElement-label' ).empty();
4891 }
4892
4893 this.$label = $label.addClass( 'oo-ui-labelElement-label' );
4894 this.setLabelContent( this.label );
4895 };
4896
4897 /**
4898 * Set the label.
4899 *
4900 * An empty string will result in the label being hidden. A string containing only whitespace will
4901 * be converted to a single `&nbsp;`.
4902 *
4903 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
4904 * text; or null for no label
4905 * @chainable
4906 */
4907 OO.ui.LabelElement.prototype.setLabel = function ( label ) {
4908 label = typeof label === 'function' ? OO.ui.resolveMsg( label ) : label;
4909 label = ( typeof label === 'string' && label.length ) || label instanceof jQuery ? label : null;
4910
4911 if ( this.label !== label ) {
4912 if ( this.$label ) {
4913 this.setLabelContent( label );
4914 }
4915 this.label = label;
4916 }
4917
4918 this.$element.toggleClass( 'oo-ui-labelElement', !!this.label );
4919
4920 return this;
4921 };
4922
4923 /**
4924 * Get the label.
4925 *
4926 * @return {jQuery|string|Function|null} Label nodes; text; a function that returns nodes or
4927 * text; or null for no label
4928 */
4929 OO.ui.LabelElement.prototype.getLabel = function () {
4930 return this.label;
4931 };
4932
4933 /**
4934 * Fit the label.
4935 *
4936 * @chainable
4937 */
4938 OO.ui.LabelElement.prototype.fitLabel = function () {
4939 if ( this.$label && this.$label.autoEllipsis && this.autoFitLabel ) {
4940 this.$label.autoEllipsis( { hasSpan: false, tooltip: true } );
4941 }
4942
4943 return this;
4944 };
4945
4946 /**
4947 * Set the content of the label.
4948 *
4949 * Do not call this method until after the label element has been set by #setLabelElement.
4950 *
4951 * @private
4952 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
4953 * text; or null for no label
4954 */
4955 OO.ui.LabelElement.prototype.setLabelContent = function ( label ) {
4956 if ( typeof label === 'string' ) {
4957 if ( label.match( /^\s*$/ ) ) {
4958 // Convert whitespace only string to a single non-breaking space
4959 this.$label.html( '&nbsp;' );
4960 } else {
4961 this.$label.text( label );
4962 }
4963 } else if ( label instanceof jQuery ) {
4964 this.$label.empty().append( label );
4965 } else {
4966 this.$label.empty();
4967 }
4968 };
4969
4970 /**
4971 * Element containing an OO.ui.PopupWidget object.
4972 *
4973 * @abstract
4974 * @class
4975 *
4976 * @constructor
4977 * @param {Object} [config] Configuration options
4978 * @cfg {Object} [popup] Configuration to pass to popup
4979 * @cfg {boolean} [autoClose=true] Popup auto-closes when it loses focus
4980 */
4981 OO.ui.PopupElement = function OoUiPopupElement( config ) {
4982 // Configuration initialization
4983 config = config || {};
4984
4985 // Properties
4986 this.popup = new OO.ui.PopupWidget( $.extend(
4987 { autoClose: true },
4988 config.popup,
4989 { $: this.$, $autoCloseIgnore: this.$element }
4990 ) );
4991 };
4992
4993 /* Methods */
4994
4995 /**
4996 * Get popup.
4997 *
4998 * @return {OO.ui.PopupWidget} Popup widget
4999 */
5000 OO.ui.PopupElement.prototype.getPopup = function () {
5001 return this.popup;
5002 };
5003
5004 /**
5005 * Element with named flags that can be added, removed, listed and checked.
5006 *
5007 * A flag, when set, adds a CSS class on the `$element` by combining `oo-ui-flaggedElement-` with
5008 * the flag name. Flags are primarily useful for styling.
5009 *
5010 * @abstract
5011 * @class
5012 *
5013 * @constructor
5014 * @param {Object} [config] Configuration options
5015 * @cfg {string|string[]} [flags] Flags describing importance and functionality, e.g. 'primary',
5016 * 'safe', 'progressive', 'destructive' or 'constructive'
5017 * @cfg {jQuery} [$flagged] Flagged node, assigned to #$flagged, omit to use #$element
5018 */
5019 OO.ui.FlaggedElement = function OoUiFlaggedElement( config ) {
5020 // Configuration initialization
5021 config = config || {};
5022
5023 // Properties
5024 this.flags = {};
5025 this.$flagged = null;
5026
5027 // Initialization
5028 this.setFlags( config.flags );
5029 this.setFlaggedElement( config.$flagged || this.$element );
5030 };
5031
5032 /* Events */
5033
5034 /**
5035 * @event flag
5036 * @param {Object.<string,boolean>} changes Object keyed by flag name containing boolean
5037 * added/removed properties
5038 */
5039
5040 /* Methods */
5041
5042 /**
5043 * Set the flagged element.
5044 *
5045 * If an element is already set, it will be cleaned up before setting up the new element.
5046 *
5047 * @param {jQuery} $flagged Element to add flags to
5048 */
5049 OO.ui.FlaggedElement.prototype.setFlaggedElement = function ( $flagged ) {
5050 var classNames = Object.keys( this.flags ).map( function ( flag ) {
5051 return 'oo-ui-flaggedElement-' + flag;
5052 } ).join( ' ' );
5053
5054 if ( this.$flagged ) {
5055 this.$flagged.removeClass( classNames );
5056 }
5057
5058 this.$flagged = $flagged.addClass( classNames );
5059 };
5060
5061 /**
5062 * Check if a flag is set.
5063 *
5064 * @param {string} flag Name of flag
5065 * @return {boolean} Has flag
5066 */
5067 OO.ui.FlaggedElement.prototype.hasFlag = function ( flag ) {
5068 return flag in this.flags;
5069 };
5070
5071 /**
5072 * Get the names of all flags set.
5073 *
5074 * @return {string[]} Flag names
5075 */
5076 OO.ui.FlaggedElement.prototype.getFlags = function () {
5077 return Object.keys( this.flags );
5078 };
5079
5080 /**
5081 * Clear all flags.
5082 *
5083 * @chainable
5084 * @fires flag
5085 */
5086 OO.ui.FlaggedElement.prototype.clearFlags = function () {
5087 var flag, className,
5088 changes = {},
5089 remove = [],
5090 classPrefix = 'oo-ui-flaggedElement-';
5091
5092 for ( flag in this.flags ) {
5093 className = classPrefix + flag;
5094 changes[flag] = false;
5095 delete this.flags[flag];
5096 remove.push( className );
5097 }
5098
5099 if ( this.$flagged ) {
5100 this.$flagged.removeClass( remove.join( ' ' ) );
5101 }
5102
5103 this.updateThemeClasses();
5104 this.emit( 'flag', changes );
5105
5106 return this;
5107 };
5108
5109 /**
5110 * Add one or more flags.
5111 *
5112 * @param {string|string[]|Object.<string, boolean>} flags One or more flags to add, or an object
5113 * keyed by flag name containing boolean set/remove instructions.
5114 * @chainable
5115 * @fires flag
5116 */
5117 OO.ui.FlaggedElement.prototype.setFlags = function ( flags ) {
5118 var i, len, flag, className,
5119 changes = {},
5120 add = [],
5121 remove = [],
5122 classPrefix = 'oo-ui-flaggedElement-';
5123
5124 if ( typeof flags === 'string' ) {
5125 className = classPrefix + flags;
5126 // Set
5127 if ( !this.flags[flags] ) {
5128 this.flags[flags] = true;
5129 add.push( className );
5130 }
5131 } else if ( $.isArray( flags ) ) {
5132 for ( i = 0, len = flags.length; i < len; i++ ) {
5133 flag = flags[i];
5134 className = classPrefix + flag;
5135 // Set
5136 if ( !this.flags[flag] ) {
5137 changes[flag] = true;
5138 this.flags[flag] = true;
5139 add.push( className );
5140 }
5141 }
5142 } else if ( OO.isPlainObject( flags ) ) {
5143 for ( flag in flags ) {
5144 className = classPrefix + flag;
5145 if ( flags[flag] ) {
5146 // Set
5147 if ( !this.flags[flag] ) {
5148 changes[flag] = true;
5149 this.flags[flag] = true;
5150 add.push( className );
5151 }
5152 } else {
5153 // Remove
5154 if ( this.flags[flag] ) {
5155 changes[flag] = false;
5156 delete this.flags[flag];
5157 remove.push( className );
5158 }
5159 }
5160 }
5161 }
5162
5163 if ( this.$flagged ) {
5164 this.$flagged
5165 .addClass( add.join( ' ' ) )
5166 .removeClass( remove.join( ' ' ) );
5167 }
5168
5169 this.updateThemeClasses();
5170 this.emit( 'flag', changes );
5171
5172 return this;
5173 };
5174
5175 /**
5176 * Element with a title.
5177 *
5178 * Titles are rendered by the browser and are made visible when hovering the element. Titles are
5179 * not visible on touch devices.
5180 *
5181 * @abstract
5182 * @class
5183 *
5184 * @constructor
5185 * @param {Object} [config] Configuration options
5186 * @cfg {jQuery} [$titled] Titled node, assigned to #$titled, omit to use #$element
5187 * @cfg {string|Function} [title] Title text or a function that returns text. If not provided, the
5188 * static property 'title' is used.
5189 */
5190 OO.ui.TitledElement = function OoUiTitledElement( config ) {
5191 // Configuration initialization
5192 config = config || {};
5193
5194 // Properties
5195 this.$titled = null;
5196 this.title = null;
5197
5198 // Initialization
5199 this.setTitle( config.title || this.constructor.static.title );
5200 this.setTitledElement( config.$titled || this.$element );
5201 };
5202
5203 /* Setup */
5204
5205 OO.initClass( OO.ui.TitledElement );
5206
5207 /* Static Properties */
5208
5209 /**
5210 * Title.
5211 *
5212 * @static
5213 * @inheritable
5214 * @property {string|Function} Title text or a function that returns text
5215 */
5216 OO.ui.TitledElement.static.title = null;
5217
5218 /* Methods */
5219
5220 /**
5221 * Set the titled element.
5222 *
5223 * If an element is already set, it will be cleaned up before setting up the new element.
5224 *
5225 * @param {jQuery} $titled Element to set title on
5226 */
5227 OO.ui.TitledElement.prototype.setTitledElement = function ( $titled ) {
5228 if ( this.$titled ) {
5229 this.$titled.removeAttr( 'title' );
5230 }
5231
5232 this.$titled = $titled;
5233 if ( this.title ) {
5234 this.$titled.attr( 'title', this.title );
5235 }
5236 };
5237
5238 /**
5239 * Set title.
5240 *
5241 * @param {string|Function|null} title Title text, a function that returns text or null for no title
5242 * @chainable
5243 */
5244 OO.ui.TitledElement.prototype.setTitle = function ( title ) {
5245 title = typeof title === 'string' ? OO.ui.resolveMsg( title ) : null;
5246
5247 if ( this.title !== title ) {
5248 if ( this.$titled ) {
5249 if ( title !== null ) {
5250 this.$titled.attr( 'title', title );
5251 } else {
5252 this.$titled.removeAttr( 'title' );
5253 }
5254 }
5255 this.title = title;
5256 }
5257
5258 return this;
5259 };
5260
5261 /**
5262 * Get title.
5263 *
5264 * @return {string} Title string
5265 */
5266 OO.ui.TitledElement.prototype.getTitle = function () {
5267 return this.title;
5268 };
5269
5270 /**
5271 * Element that can be automatically clipped to visible boundaries.
5272 *
5273 * Whenever the element's natural height changes, you have to call
5274 * #clip to make sure it's still clipping correctly.
5275 *
5276 * @abstract
5277 * @class
5278 *
5279 * @constructor
5280 * @param {Object} [config] Configuration options
5281 * @cfg {jQuery} [$clippable] Nodes to clip, assigned to #$clippable, omit to use #$element
5282 */
5283 OO.ui.ClippableElement = function OoUiClippableElement( config ) {
5284 // Configuration initialization
5285 config = config || {};
5286
5287 // Properties
5288 this.$clippable = null;
5289 this.clipping = false;
5290 this.clippedHorizontally = false;
5291 this.clippedVertically = false;
5292 this.$clippableContainer = null;
5293 this.$clippableScroller = null;
5294 this.$clippableWindow = null;
5295 this.idealWidth = null;
5296 this.idealHeight = null;
5297 this.onClippableContainerScrollHandler = this.clip.bind( this );
5298 this.onClippableWindowResizeHandler = this.clip.bind( this );
5299
5300 // Initialization
5301 this.setClippableElement( config.$clippable || this.$element );
5302 };
5303
5304 /* Methods */
5305
5306 /**
5307 * Set clippable element.
5308 *
5309 * If an element is already set, it will be cleaned up before setting up the new element.
5310 *
5311 * @param {jQuery} $clippable Element to make clippable
5312 */
5313 OO.ui.ClippableElement.prototype.setClippableElement = function ( $clippable ) {
5314 if ( this.$clippable ) {
5315 this.$clippable.removeClass( 'oo-ui-clippableElement-clippable' );
5316 this.$clippable.css( { width: '', height: '' } );
5317 this.$clippable.width(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
5318 this.$clippable.css( { overflowX: '', overflowY: '' } );
5319 }
5320
5321 this.$clippable = $clippable.addClass( 'oo-ui-clippableElement-clippable' );
5322 this.clip();
5323 };
5324
5325 /**
5326 * Toggle clipping.
5327 *
5328 * Do not turn clipping on until after the element is attached to the DOM and visible.
5329 *
5330 * @param {boolean} [clipping] Enable clipping, omit to toggle
5331 * @chainable
5332 */
5333 OO.ui.ClippableElement.prototype.toggleClipping = function ( clipping ) {
5334 clipping = clipping === undefined ? !this.clipping : !!clipping;
5335
5336 if ( this.clipping !== clipping ) {
5337 this.clipping = clipping;
5338 if ( clipping ) {
5339 this.$clippableContainer = this.$( this.getClosestScrollableElementContainer() );
5340 // If the clippable container is the root, we have to listen to scroll events and check
5341 // jQuery.scrollTop on the window because of browser inconsistencies
5342 this.$clippableScroller = this.$clippableContainer.is( 'html, body' ) ?
5343 this.$( OO.ui.Element.static.getWindow( this.$clippableContainer ) ) :
5344 this.$clippableContainer;
5345 this.$clippableScroller.on( 'scroll', this.onClippableContainerScrollHandler );
5346 this.$clippableWindow = this.$( this.getElementWindow() )
5347 .on( 'resize', this.onClippableWindowResizeHandler );
5348 // Initial clip after visible
5349 this.clip();
5350 } else {
5351 this.$clippable.css( { width: '', height: '' } );
5352 this.$clippable.width(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
5353 this.$clippable.css( { overflowX: '', overflowY: '' } );
5354
5355 this.$clippableContainer = null;
5356 this.$clippableScroller.off( 'scroll', this.onClippableContainerScrollHandler );
5357 this.$clippableScroller = null;
5358 this.$clippableWindow.off( 'resize', this.onClippableWindowResizeHandler );
5359 this.$clippableWindow = null;
5360 }
5361 }
5362
5363 return this;
5364 };
5365
5366 /**
5367 * Check if the element will be clipped to fit the visible area of the nearest scrollable container.
5368 *
5369 * @return {boolean} Element will be clipped to the visible area
5370 */
5371 OO.ui.ClippableElement.prototype.isClipping = function () {
5372 return this.clipping;
5373 };
5374
5375 /**
5376 * Check if the bottom or right of the element is being clipped by the nearest scrollable container.
5377 *
5378 * @return {boolean} Part of the element is being clipped
5379 */
5380 OO.ui.ClippableElement.prototype.isClipped = function () {
5381 return this.clippedHorizontally || this.clippedVertically;
5382 };
5383
5384 /**
5385 * Check if the right of the element is being clipped by the nearest scrollable container.
5386 *
5387 * @return {boolean} Part of the element is being clipped
5388 */
5389 OO.ui.ClippableElement.prototype.isClippedHorizontally = function () {
5390 return this.clippedHorizontally;
5391 };
5392
5393 /**
5394 * Check if the bottom of the element is being clipped by the nearest scrollable container.
5395 *
5396 * @return {boolean} Part of the element is being clipped
5397 */
5398 OO.ui.ClippableElement.prototype.isClippedVertically = function () {
5399 return this.clippedVertically;
5400 };
5401
5402 /**
5403 * Set the ideal size. These are the dimensions the element will have when it's not being clipped.
5404 *
5405 * @param {number|string} [width] Width as a number of pixels or CSS string with unit suffix
5406 * @param {number|string} [height] Height as a number of pixels or CSS string with unit suffix
5407 */
5408 OO.ui.ClippableElement.prototype.setIdealSize = function ( width, height ) {
5409 this.idealWidth = width;
5410 this.idealHeight = height;
5411
5412 if ( !this.clipping ) {
5413 // Update dimensions
5414 this.$clippable.css( { width: width, height: height } );
5415 }
5416 // While clipping, idealWidth and idealHeight are not considered
5417 };
5418
5419 /**
5420 * Clip element to visible boundaries and allow scrolling when needed. Call this method when
5421 * the element's natural height changes.
5422 *
5423 * Element will be clipped the bottom or right of the element is within 10px of the edge of, or
5424 * overlapped by, the visible area of the nearest scrollable container.
5425 *
5426 * @chainable
5427 */
5428 OO.ui.ClippableElement.prototype.clip = function () {
5429 if ( !this.clipping ) {
5430 // this.$clippableContainer and this.$clippableWindow are null, so the below will fail
5431 return this;
5432 }
5433
5434 var buffer = 7, // Chosen by fair dice roll
5435 cOffset = this.$clippable.offset(),
5436 $container = this.$clippableContainer.is( 'html, body' ) ?
5437 this.$clippableWindow : this.$clippableContainer,
5438 ccOffset = $container.offset() || { top: 0, left: 0 },
5439 ccHeight = $container.innerHeight() - buffer,
5440 ccWidth = $container.innerWidth() - buffer,
5441 cHeight = this.$clippable.outerHeight() + buffer,
5442 cWidth = this.$clippable.outerWidth() + buffer,
5443 scrollTop = this.$clippableScroller.scrollTop(),
5444 scrollLeft = this.$clippableScroller.scrollLeft(),
5445 desiredWidth = cOffset.left < 0 ?
5446 cWidth + cOffset.left :
5447 ( ccOffset.left + scrollLeft + ccWidth ) - cOffset.left,
5448 desiredHeight = cOffset.top < 0 ?
5449 cHeight + cOffset.top :
5450 ( ccOffset.top + scrollTop + ccHeight ) - cOffset.top,
5451 naturalWidth = this.$clippable.prop( 'scrollWidth' ),
5452 naturalHeight = this.$clippable.prop( 'scrollHeight' ),
5453 clipWidth = desiredWidth < naturalWidth,
5454 clipHeight = desiredHeight < naturalHeight;
5455
5456 if ( clipWidth ) {
5457 this.$clippable.css( { overflowX: 'scroll', width: desiredWidth } );
5458 } else {
5459 this.$clippable.css( 'width', this.idealWidth || '' );
5460 this.$clippable.width(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
5461 this.$clippable.css( 'overflowX', '' );
5462 }
5463 if ( clipHeight ) {
5464 this.$clippable.css( { overflowY: 'scroll', height: desiredHeight } );
5465 } else {
5466 this.$clippable.css( 'height', this.idealHeight || '' );
5467 this.$clippable.height(); // Force reflow for https://code.google.com/p/chromium/issues/detail?id=387290
5468 this.$clippable.css( 'overflowY', '' );
5469 }
5470
5471 this.clippedHorizontally = clipWidth;
5472 this.clippedVertically = clipHeight;
5473
5474 return this;
5475 };
5476
5477 /**
5478 * Generic toolbar tool.
5479 *
5480 * @abstract
5481 * @class
5482 * @extends OO.ui.Widget
5483 * @mixins OO.ui.IconElement
5484 * @mixins OO.ui.FlaggedElement
5485 *
5486 * @constructor
5487 * @param {OO.ui.ToolGroup} toolGroup
5488 * @param {Object} [config] Configuration options
5489 * @cfg {string|Function} [title] Title text or a function that returns text
5490 */
5491 OO.ui.Tool = function OoUiTool( toolGroup, config ) {
5492 // Configuration initialization
5493 config = config || {};
5494
5495 // Parent constructor
5496 OO.ui.Tool.super.call( this, config );
5497
5498 // Mixin constructors
5499 OO.ui.IconElement.call( this, config );
5500 OO.ui.FlaggedElement.call( this, config );
5501
5502 // Properties
5503 this.toolGroup = toolGroup;
5504 this.toolbar = this.toolGroup.getToolbar();
5505 this.active = false;
5506 this.$title = this.$( '<span>' );
5507 this.$accel = this.$( '<span>' );
5508 this.$link = this.$( '<a>' );
5509 this.title = null;
5510
5511 // Events
5512 this.toolbar.connect( this, { updateState: 'onUpdateState' } );
5513
5514 // Initialization
5515 this.$title.addClass( 'oo-ui-tool-title' );
5516 this.$accel
5517 .addClass( 'oo-ui-tool-accel' )
5518 .prop( {
5519 // This may need to be changed if the key names are ever localized,
5520 // but for now they are essentially written in English
5521 dir: 'ltr',
5522 lang: 'en'
5523 } );
5524 this.$link
5525 .addClass( 'oo-ui-tool-link' )
5526 .append( this.$icon, this.$title, this.$accel )
5527 .prop( 'tabIndex', 0 )
5528 .attr( 'role', 'button' );
5529 this.$element
5530 .data( 'oo-ui-tool', this )
5531 .addClass(
5532 'oo-ui-tool ' + 'oo-ui-tool-name-' +
5533 this.constructor.static.name.replace( /^([^\/]+)\/([^\/]+).*$/, '$1-$2' )
5534 )
5535 .append( this.$link );
5536 this.setTitle( config.title || this.constructor.static.title );
5537 };
5538
5539 /* Setup */
5540
5541 OO.inheritClass( OO.ui.Tool, OO.ui.Widget );
5542 OO.mixinClass( OO.ui.Tool, OO.ui.IconElement );
5543 OO.mixinClass( OO.ui.Tool, OO.ui.FlaggedElement );
5544
5545 /* Events */
5546
5547 /**
5548 * @event select
5549 */
5550
5551 /* Static Properties */
5552
5553 /**
5554 * @static
5555 * @inheritdoc
5556 */
5557 OO.ui.Tool.static.tagName = 'span';
5558
5559 /**
5560 * Symbolic name of tool.
5561 *
5562 * @abstract
5563 * @static
5564 * @inheritable
5565 * @property {string}
5566 */
5567 OO.ui.Tool.static.name = '';
5568
5569 /**
5570 * Tool group.
5571 *
5572 * @abstract
5573 * @static
5574 * @inheritable
5575 * @property {string}
5576 */
5577 OO.ui.Tool.static.group = '';
5578
5579 /**
5580 * Tool title.
5581 *
5582 * Title is used as a tooltip when the tool is part of a bar tool group, or a label when the tool
5583 * is part of a list or menu tool group. If a trigger is associated with an action by the same name
5584 * as the tool, a description of its keyboard shortcut for the appropriate platform will be
5585 * appended to the title if the tool is part of a bar tool group.
5586 *
5587 * @abstract
5588 * @static
5589 * @inheritable
5590 * @property {string|Function} Title text or a function that returns text
5591 */
5592 OO.ui.Tool.static.title = '';
5593
5594 /**
5595 * Tool can be automatically added to catch-all groups.
5596 *
5597 * @static
5598 * @inheritable
5599 * @property {boolean}
5600 */
5601 OO.ui.Tool.static.autoAddToCatchall = true;
5602
5603 /**
5604 * Tool can be automatically added to named groups.
5605 *
5606 * @static
5607 * @property {boolean}
5608 * @inheritable
5609 */
5610 OO.ui.Tool.static.autoAddToGroup = true;
5611
5612 /**
5613 * Check if this tool is compatible with given data.
5614 *
5615 * @static
5616 * @inheritable
5617 * @param {Mixed} data Data to check
5618 * @return {boolean} Tool can be used with data
5619 */
5620 OO.ui.Tool.static.isCompatibleWith = function () {
5621 return false;
5622 };
5623
5624 /* Methods */
5625
5626 /**
5627 * Handle the toolbar state being updated.
5628 *
5629 * This is an abstract method that must be overridden in a concrete subclass.
5630 *
5631 * @abstract
5632 */
5633 OO.ui.Tool.prototype.onUpdateState = function () {
5634 throw new Error(
5635 'OO.ui.Tool.onUpdateState not implemented in this subclass:' + this.constructor
5636 );
5637 };
5638
5639 /**
5640 * Handle the tool being selected.
5641 *
5642 * This is an abstract method that must be overridden in a concrete subclass.
5643 *
5644 * @abstract
5645 */
5646 OO.ui.Tool.prototype.onSelect = function () {
5647 throw new Error(
5648 'OO.ui.Tool.onSelect not implemented in this subclass:' + this.constructor
5649 );
5650 };
5651
5652 /**
5653 * Check if the button is active.
5654 *
5655 * @return {boolean} Button is active
5656 */
5657 OO.ui.Tool.prototype.isActive = function () {
5658 return this.active;
5659 };
5660
5661 /**
5662 * Make the button appear active or inactive.
5663 *
5664 * @param {boolean} state Make button appear active
5665 */
5666 OO.ui.Tool.prototype.setActive = function ( state ) {
5667 this.active = !!state;
5668 if ( this.active ) {
5669 this.$element.addClass( 'oo-ui-tool-active' );
5670 } else {
5671 this.$element.removeClass( 'oo-ui-tool-active' );
5672 }
5673 };
5674
5675 /**
5676 * Get the tool title.
5677 *
5678 * @param {string|Function} title Title text or a function that returns text
5679 * @chainable
5680 */
5681 OO.ui.Tool.prototype.setTitle = function ( title ) {
5682 this.title = OO.ui.resolveMsg( title );
5683 this.updateTitle();
5684 return this;
5685 };
5686
5687 /**
5688 * Get the tool title.
5689 *
5690 * @return {string} Title text
5691 */
5692 OO.ui.Tool.prototype.getTitle = function () {
5693 return this.title;
5694 };
5695
5696 /**
5697 * Get the tool's symbolic name.
5698 *
5699 * @return {string} Symbolic name of tool
5700 */
5701 OO.ui.Tool.prototype.getName = function () {
5702 return this.constructor.static.name;
5703 };
5704
5705 /**
5706 * Update the title.
5707 */
5708 OO.ui.Tool.prototype.updateTitle = function () {
5709 var titleTooltips = this.toolGroup.constructor.static.titleTooltips,
5710 accelTooltips = this.toolGroup.constructor.static.accelTooltips,
5711 accel = this.toolbar.getToolAccelerator( this.constructor.static.name ),
5712 tooltipParts = [];
5713
5714 this.$title.text( this.title );
5715 this.$accel.text( accel );
5716
5717 if ( titleTooltips && typeof this.title === 'string' && this.title.length ) {
5718 tooltipParts.push( this.title );
5719 }
5720 if ( accelTooltips && typeof accel === 'string' && accel.length ) {
5721 tooltipParts.push( accel );
5722 }
5723 if ( tooltipParts.length ) {
5724 this.$link.attr( 'title', tooltipParts.join( ' ' ) );
5725 } else {
5726 this.$link.removeAttr( 'title' );
5727 }
5728 };
5729
5730 /**
5731 * Destroy tool.
5732 */
5733 OO.ui.Tool.prototype.destroy = function () {
5734 this.toolbar.disconnect( this );
5735 this.$element.remove();
5736 };
5737
5738 /**
5739 * Collection of tool groups.
5740 *
5741 * @class
5742 * @extends OO.ui.Element
5743 * @mixins OO.EventEmitter
5744 * @mixins OO.ui.GroupElement
5745 *
5746 * @constructor
5747 * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
5748 * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating tool groups
5749 * @param {Object} [config] Configuration options
5750 * @cfg {boolean} [actions] Add an actions section opposite to the tools
5751 * @cfg {boolean} [shadow] Add a shadow below the toolbar
5752 */
5753 OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
5754 // Configuration initialization
5755 config = config || {};
5756
5757 // Parent constructor
5758 OO.ui.Toolbar.super.call( this, config );
5759
5760 // Mixin constructors
5761 OO.EventEmitter.call( this );
5762 OO.ui.GroupElement.call( this, config );
5763
5764 // Properties
5765 this.toolFactory = toolFactory;
5766 this.toolGroupFactory = toolGroupFactory;
5767 this.groups = [];
5768 this.tools = {};
5769 this.$bar = this.$( '<div>' );
5770 this.$actions = this.$( '<div>' );
5771 this.initialized = false;
5772
5773 // Events
5774 this.$element
5775 .add( this.$bar ).add( this.$group ).add( this.$actions )
5776 .on( 'mousedown touchstart', this.onPointerDown.bind( this ) );
5777
5778 // Initialization
5779 this.$group.addClass( 'oo-ui-toolbar-tools' );
5780 if ( config.actions ) {
5781 this.$bar.append( this.$actions.addClass( 'oo-ui-toolbar-actions' ) );
5782 }
5783 this.$bar
5784 .addClass( 'oo-ui-toolbar-bar' )
5785 .append( this.$group, '<div style="clear:both"></div>' );
5786 if ( config.shadow ) {
5787 this.$bar.append( '<div class="oo-ui-toolbar-shadow"></div>' );
5788 }
5789 this.$element.addClass( 'oo-ui-toolbar' ).append( this.$bar );
5790 };
5791
5792 /* Setup */
5793
5794 OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
5795 OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
5796 OO.mixinClass( OO.ui.Toolbar, OO.ui.GroupElement );
5797
5798 /* Methods */
5799
5800 /**
5801 * Get the tool factory.
5802 *
5803 * @return {OO.ui.ToolFactory} Tool factory
5804 */
5805 OO.ui.Toolbar.prototype.getToolFactory = function () {
5806 return this.toolFactory;
5807 };
5808
5809 /**
5810 * Get the tool group factory.
5811 *
5812 * @return {OO.Factory} Tool group factory
5813 */
5814 OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
5815 return this.toolGroupFactory;
5816 };
5817
5818 /**
5819 * Handles mouse down events.
5820 *
5821 * @param {jQuery.Event} e Mouse down event
5822 */
5823 OO.ui.Toolbar.prototype.onPointerDown = function ( e ) {
5824 var $closestWidgetToEvent = this.$( e.target ).closest( '.oo-ui-widget' ),
5825 $closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
5826 if ( !$closestWidgetToEvent.length || $closestWidgetToEvent[0] === $closestWidgetToToolbar[0] ) {
5827 return false;
5828 }
5829 };
5830
5831 /**
5832 * Sets up handles and preloads required information for the toolbar to work.
5833 * This must be called immediately after it is attached to a visible document.
5834 */
5835 OO.ui.Toolbar.prototype.initialize = function () {
5836 this.initialized = true;
5837 };
5838
5839 /**
5840 * Setup toolbar.
5841 *
5842 * Tools can be specified in the following ways:
5843 *
5844 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
5845 * - All tools in a group: `{ group: 'group-name' }`
5846 * - All tools: `'*'` - Using this will make the group a list with a "More" label by default
5847 *
5848 * @param {Object.<string,Array>} groups List of tool group configurations
5849 * @param {Array|string} [groups.include] Tools to include
5850 * @param {Array|string} [groups.exclude] Tools to exclude
5851 * @param {Array|string} [groups.promote] Tools to promote to the beginning
5852 * @param {Array|string} [groups.demote] Tools to demote to the end
5853 */
5854 OO.ui.Toolbar.prototype.setup = function ( groups ) {
5855 var i, len, type, group,
5856 items = [],
5857 defaultType = 'bar';
5858
5859 // Cleanup previous groups
5860 this.reset();
5861
5862 // Build out new groups
5863 for ( i = 0, len = groups.length; i < len; i++ ) {
5864 group = groups[i];
5865 if ( group.include === '*' ) {
5866 // Apply defaults to catch-all groups
5867 if ( group.type === undefined ) {
5868 group.type = 'list';
5869 }
5870 if ( group.label === undefined ) {
5871 group.label = OO.ui.msg( 'ooui-toolbar-more' );
5872 }
5873 }
5874 // Check type has been registered
5875 type = this.getToolGroupFactory().lookup( group.type ) ? group.type : defaultType;
5876 items.push(
5877 this.getToolGroupFactory().create( type, this, $.extend( { $: this.$ }, group ) )
5878 );
5879 }
5880 this.addItems( items );
5881 };
5882
5883 /**
5884 * Remove all tools and groups from the toolbar.
5885 */
5886 OO.ui.Toolbar.prototype.reset = function () {
5887 var i, len;
5888
5889 this.groups = [];
5890 this.tools = {};
5891 for ( i = 0, len = this.items.length; i < len; i++ ) {
5892 this.items[i].destroy();
5893 }
5894 this.clearItems();
5895 };
5896
5897 /**
5898 * Destroys toolbar, removing event handlers and DOM elements.
5899 *
5900 * Call this whenever you are done using a toolbar.
5901 */
5902 OO.ui.Toolbar.prototype.destroy = function () {
5903 this.reset();
5904 this.$element.remove();
5905 };
5906
5907 /**
5908 * Check if tool has not been used yet.
5909 *
5910 * @param {string} name Symbolic name of tool
5911 * @return {boolean} Tool is available
5912 */
5913 OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
5914 return !this.tools[name];
5915 };
5916
5917 /**
5918 * Prevent tool from being used again.
5919 *
5920 * @param {OO.ui.Tool} tool Tool to reserve
5921 */
5922 OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
5923 this.tools[tool.getName()] = tool;
5924 };
5925
5926 /**
5927 * Allow tool to be used again.
5928 *
5929 * @param {OO.ui.Tool} tool Tool to release
5930 */
5931 OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
5932 delete this.tools[tool.getName()];
5933 };
5934
5935 /**
5936 * Get accelerator label for tool.
5937 *
5938 * This is a stub that should be overridden to provide access to accelerator information.
5939 *
5940 * @param {string} name Symbolic name of tool
5941 * @return {string|undefined} Tool accelerator label if available
5942 */
5943 OO.ui.Toolbar.prototype.getToolAccelerator = function () {
5944 return undefined;
5945 };
5946
5947 /**
5948 * Collection of tools.
5949 *
5950 * Tools can be specified in the following ways:
5951 *
5952 * - A specific tool: `{ name: 'tool-name' }` or `'tool-name'`
5953 * - All tools in a group: `{ group: 'group-name' }`
5954 * - All tools: `'*'`
5955 *
5956 * @abstract
5957 * @class
5958 * @extends OO.ui.Widget
5959 * @mixins OO.ui.GroupElement
5960 *
5961 * @constructor
5962 * @param {OO.ui.Toolbar} toolbar
5963 * @param {Object} [config] Configuration options
5964 * @cfg {Array|string} [include=[]] List of tools to include
5965 * @cfg {Array|string} [exclude=[]] List of tools to exclude
5966 * @cfg {Array|string} [promote=[]] List of tools to promote to the beginning
5967 * @cfg {Array|string} [demote=[]] List of tools to demote to the end
5968 */
5969 OO.ui.ToolGroup = function OoUiToolGroup( toolbar, config ) {
5970 // Configuration initialization
5971 config = config || {};
5972
5973 // Parent constructor
5974 OO.ui.ToolGroup.super.call( this, config );
5975
5976 // Mixin constructors
5977 OO.ui.GroupElement.call( this, config );
5978
5979 // Properties
5980 this.toolbar = toolbar;
5981 this.tools = {};
5982 this.pressed = null;
5983 this.autoDisabled = false;
5984 this.include = config.include || [];
5985 this.exclude = config.exclude || [];
5986 this.promote = config.promote || [];
5987 this.demote = config.demote || [];
5988 this.onCapturedMouseUpHandler = this.onCapturedMouseUp.bind( this );
5989
5990 // Events
5991 this.$element.on( {
5992 'mousedown touchstart': this.onPointerDown.bind( this ),
5993 'mouseup touchend': this.onPointerUp.bind( this ),
5994 mouseover: this.onMouseOver.bind( this ),
5995 mouseout: this.onMouseOut.bind( this )
5996 } );
5997 this.toolbar.getToolFactory().connect( this, { register: 'onToolFactoryRegister' } );
5998 this.aggregate( { disable: 'itemDisable' } );
5999 this.connect( this, { itemDisable: 'updateDisabled' } );
6000
6001 // Initialization
6002 this.$group.addClass( 'oo-ui-toolGroup-tools' );
6003 this.$element
6004 .addClass( 'oo-ui-toolGroup' )
6005 .append( this.$group );
6006 this.populate();
6007 };
6008
6009 /* Setup */
6010
6011 OO.inheritClass( OO.ui.ToolGroup, OO.ui.Widget );
6012 OO.mixinClass( OO.ui.ToolGroup, OO.ui.GroupElement );
6013
6014 /* Events */
6015
6016 /**
6017 * @event update
6018 */
6019
6020 /* Static Properties */
6021
6022 /**
6023 * Show labels in tooltips.
6024 *
6025 * @static
6026 * @inheritable
6027 * @property {boolean}
6028 */
6029 OO.ui.ToolGroup.static.titleTooltips = false;
6030
6031 /**
6032 * Show acceleration labels in tooltips.
6033 *
6034 * @static
6035 * @inheritable
6036 * @property {boolean}
6037 */
6038 OO.ui.ToolGroup.static.accelTooltips = false;
6039
6040 /**
6041 * Automatically disable the toolgroup when all tools are disabled
6042 *
6043 * @static
6044 * @inheritable
6045 * @property {boolean}
6046 */
6047 OO.ui.ToolGroup.static.autoDisable = true;
6048
6049 /* Methods */
6050
6051 /**
6052 * @inheritdoc
6053 */
6054 OO.ui.ToolGroup.prototype.isDisabled = function () {
6055 return this.autoDisabled || OO.ui.ToolGroup.super.prototype.isDisabled.apply( this, arguments );
6056 };
6057
6058 /**
6059 * @inheritdoc
6060 */
6061 OO.ui.ToolGroup.prototype.updateDisabled = function () {
6062 var i, item, allDisabled = true;
6063
6064 if ( this.constructor.static.autoDisable ) {
6065 for ( i = this.items.length - 1; i >= 0; i-- ) {
6066 item = this.items[i];
6067 if ( !item.isDisabled() ) {
6068 allDisabled = false;
6069 break;
6070 }
6071 }
6072 this.autoDisabled = allDisabled;
6073 }
6074 OO.ui.ToolGroup.super.prototype.updateDisabled.apply( this, arguments );
6075 };
6076
6077 /**
6078 * Handle mouse down events.
6079 *
6080 * @param {jQuery.Event} e Mouse down event
6081 */
6082 OO.ui.ToolGroup.prototype.onPointerDown = function ( e ) {
6083 // e.which is 0 for touch events, 1 for left mouse button
6084 if ( !this.isDisabled() && e.which <= 1 ) {
6085 this.pressed = this.getTargetTool( e );
6086 if ( this.pressed ) {
6087 this.pressed.setActive( true );
6088 this.getElementDocument().addEventListener(
6089 'mouseup', this.onCapturedMouseUpHandler, true
6090 );
6091 }
6092 }
6093 return false;
6094 };
6095
6096 /**
6097 * Handle captured mouse up events.
6098 *
6099 * @param {Event} e Mouse up event
6100 */
6101 OO.ui.ToolGroup.prototype.onCapturedMouseUp = function ( e ) {
6102 this.getElementDocument().removeEventListener( 'mouseup', this.onCapturedMouseUpHandler, true );
6103 // onPointerUp may be called a second time, depending on where the mouse is when the button is
6104 // released, but since `this.pressed` will no longer be true, the second call will be ignored.
6105 this.onPointerUp( e );
6106 };
6107
6108 /**
6109 * Handle mouse up events.
6110 *
6111 * @param {jQuery.Event} e Mouse up event
6112 */
6113 OO.ui.ToolGroup.prototype.onPointerUp = function ( e ) {
6114 var tool = this.getTargetTool( e );
6115
6116 // e.which is 0 for touch events, 1 for left mouse button
6117 if ( !this.isDisabled() && e.which <= 1 && this.pressed && this.pressed === tool ) {
6118 this.pressed.onSelect();
6119 }
6120
6121 this.pressed = null;
6122 return false;
6123 };
6124
6125 /**
6126 * Handle mouse over events.
6127 *
6128 * @param {jQuery.Event} e Mouse over event
6129 */
6130 OO.ui.ToolGroup.prototype.onMouseOver = function ( e ) {
6131 var tool = this.getTargetTool( e );
6132
6133 if ( this.pressed && this.pressed === tool ) {
6134 this.pressed.setActive( true );
6135 }
6136 };
6137
6138 /**
6139 * Handle mouse out events.
6140 *
6141 * @param {jQuery.Event} e Mouse out event
6142 */
6143 OO.ui.ToolGroup.prototype.onMouseOut = function ( e ) {
6144 var tool = this.getTargetTool( e );
6145
6146 if ( this.pressed && this.pressed === tool ) {
6147 this.pressed.setActive( false );
6148 }
6149 };
6150
6151 /**
6152 * Get the closest tool to a jQuery.Event.
6153 *
6154 * Only tool links are considered, which prevents other elements in the tool such as popups from
6155 * triggering tool group interactions.
6156 *
6157 * @private
6158 * @param {jQuery.Event} e
6159 * @return {OO.ui.Tool|null} Tool, `null` if none was found
6160 */
6161 OO.ui.ToolGroup.prototype.getTargetTool = function ( e ) {
6162 var tool,
6163 $item = this.$( e.target ).closest( '.oo-ui-tool-link' );
6164
6165 if ( $item.length ) {
6166 tool = $item.parent().data( 'oo-ui-tool' );
6167 }
6168
6169 return tool && !tool.isDisabled() ? tool : null;
6170 };
6171
6172 /**
6173 * Handle tool registry register events.
6174 *
6175 * If a tool is registered after the group is created, we must repopulate the list to account for:
6176 *
6177 * - a tool being added that may be included
6178 * - a tool already included being overridden
6179 *
6180 * @param {string} name Symbolic name of tool
6181 */
6182 OO.ui.ToolGroup.prototype.onToolFactoryRegister = function () {
6183 this.populate();
6184 };
6185
6186 /**
6187 * Get the toolbar this group is in.
6188 *
6189 * @return {OO.ui.Toolbar} Toolbar of group
6190 */
6191 OO.ui.ToolGroup.prototype.getToolbar = function () {
6192 return this.toolbar;
6193 };
6194
6195 /**
6196 * Add and remove tools based on configuration.
6197 */
6198 OO.ui.ToolGroup.prototype.populate = function () {
6199 var i, len, name, tool,
6200 toolFactory = this.toolbar.getToolFactory(),
6201 names = {},
6202 add = [],
6203 remove = [],
6204 list = this.toolbar.getToolFactory().getTools(
6205 this.include, this.exclude, this.promote, this.demote
6206 );
6207
6208 // Build a list of needed tools
6209 for ( i = 0, len = list.length; i < len; i++ ) {
6210 name = list[i];
6211 if (
6212 // Tool exists
6213 toolFactory.lookup( name ) &&
6214 // Tool is available or is already in this group
6215 ( this.toolbar.isToolAvailable( name ) || this.tools[name] )
6216 ) {
6217 tool = this.tools[name];
6218 if ( !tool ) {
6219 // Auto-initialize tools on first use
6220 this.tools[name] = tool = toolFactory.create( name, this );
6221 tool.updateTitle();
6222 }
6223 this.toolbar.reserveTool( tool );
6224 add.push( tool );
6225 names[name] = true;
6226 }
6227 }
6228 // Remove tools that are no longer needed
6229 for ( name in this.tools ) {
6230 if ( !names[name] ) {
6231 this.tools[name].destroy();
6232 this.toolbar.releaseTool( this.tools[name] );
6233 remove.push( this.tools[name] );
6234 delete this.tools[name];
6235 }
6236 }
6237 if ( remove.length ) {
6238 this.removeItems( remove );
6239 }
6240 // Update emptiness state
6241 if ( add.length ) {
6242 this.$element.removeClass( 'oo-ui-toolGroup-empty' );
6243 } else {
6244 this.$element.addClass( 'oo-ui-toolGroup-empty' );
6245 }
6246 // Re-add tools (moving existing ones to new locations)
6247 this.addItems( add );
6248 // Disabled state may depend on items
6249 this.updateDisabled();
6250 };
6251
6252 /**
6253 * Destroy tool group.
6254 */
6255 OO.ui.ToolGroup.prototype.destroy = function () {
6256 var name;
6257
6258 this.clearItems();
6259 this.toolbar.getToolFactory().disconnect( this );
6260 for ( name in this.tools ) {
6261 this.toolbar.releaseTool( this.tools[name] );
6262 this.tools[name].disconnect( this ).destroy();
6263 delete this.tools[name];
6264 }
6265 this.$element.remove();
6266 };
6267
6268 /**
6269 * Dialog for showing a message.
6270 *
6271 * User interface:
6272 * - Registers two actions by default (safe and primary).
6273 * - Renders action widgets in the footer.
6274 *
6275 * @class
6276 * @extends OO.ui.Dialog
6277 *
6278 * @constructor
6279 * @param {Object} [config] Configuration options
6280 */
6281 OO.ui.MessageDialog = function OoUiMessageDialog( config ) {
6282 // Parent constructor
6283 OO.ui.MessageDialog.super.call( this, config );
6284
6285 // Properties
6286 this.verticalActionLayout = null;
6287
6288 // Initialization
6289 this.$element.addClass( 'oo-ui-messageDialog' );
6290 };
6291
6292 /* Inheritance */
6293
6294 OO.inheritClass( OO.ui.MessageDialog, OO.ui.Dialog );
6295
6296 /* Static Properties */
6297
6298 OO.ui.MessageDialog.static.name = 'message';
6299
6300 OO.ui.MessageDialog.static.size = 'small';
6301
6302 OO.ui.MessageDialog.static.verbose = false;
6303
6304 /**
6305 * Dialog title.
6306 *
6307 * A confirmation dialog's title should describe what the progressive action will do. An alert
6308 * dialog's title should describe what event occurred.
6309 *
6310 * @static
6311 * inheritable
6312 * @property {jQuery|string|Function|null}
6313 */
6314 OO.ui.MessageDialog.static.title = null;
6315
6316 /**
6317 * A confirmation dialog's message should describe the consequences of the progressive action. An
6318 * alert dialog's message should describe why the event occurred.
6319 *
6320 * @static
6321 * inheritable
6322 * @property {jQuery|string|Function|null}
6323 */
6324 OO.ui.MessageDialog.static.message = null;
6325
6326 OO.ui.MessageDialog.static.actions = [
6327 { action: 'accept', label: OO.ui.deferMsg( 'ooui-dialog-message-accept' ), flags: 'primary' },
6328 { action: 'reject', label: OO.ui.deferMsg( 'ooui-dialog-message-reject' ), flags: 'safe' }
6329 ];
6330
6331 /* Methods */
6332
6333 /**
6334 * @inheritdoc
6335 */
6336 OO.ui.MessageDialog.prototype.setManager = function ( manager ) {
6337 OO.ui.MessageDialog.super.prototype.setManager.call( this, manager );
6338
6339 // Events
6340 this.manager.connect( this, {
6341 resize: 'onResize'
6342 } );
6343
6344 return this;
6345 };
6346
6347 /**
6348 * @inheritdoc
6349 */
6350 OO.ui.MessageDialog.prototype.onActionResize = function ( action ) {
6351 this.fitActions();
6352 return OO.ui.MessageDialog.super.prototype.onActionResize.call( this, action );
6353 };
6354
6355 /**
6356 * Handle window resized events.
6357 */
6358 OO.ui.MessageDialog.prototype.onResize = function () {
6359 var dialog = this;
6360 dialog.fitActions();
6361 // Wait for CSS transition to finish and do it again :(
6362 setTimeout( function () {
6363 dialog.fitActions();
6364 }, 300 );
6365 };
6366
6367 /**
6368 * Toggle action layout between vertical and horizontal.
6369 *
6370 * @param {boolean} [value] Layout actions vertically, omit to toggle
6371 * @chainable
6372 */
6373 OO.ui.MessageDialog.prototype.toggleVerticalActionLayout = function ( value ) {
6374 value = value === undefined ? !this.verticalActionLayout : !!value;
6375
6376 if ( value !== this.verticalActionLayout ) {
6377 this.verticalActionLayout = value;
6378 this.$actions
6379 .toggleClass( 'oo-ui-messageDialog-actions-vertical', value )
6380 .toggleClass( 'oo-ui-messageDialog-actions-horizontal', !value );
6381 }
6382
6383 return this;
6384 };
6385
6386 /**
6387 * @inheritdoc
6388 */
6389 OO.ui.MessageDialog.prototype.getActionProcess = function ( action ) {
6390 if ( action ) {
6391 return new OO.ui.Process( function () {
6392 this.close( { action: action } );
6393 }, this );
6394 }
6395 return OO.ui.MessageDialog.super.prototype.getActionProcess.call( this, action );
6396 };
6397
6398 /**
6399 * @inheritdoc
6400 *
6401 * @param {Object} [data] Dialog opening data
6402 * @param {jQuery|string|Function|null} [data.title] Description of the action being confirmed
6403 * @param {jQuery|string|Function|null} [data.message] Description of the action's consequence
6404 * @param {boolean} [data.verbose] Message is verbose and should be styled as a long message
6405 * @param {Object[]} [data.actions] List of OO.ui.ActionOptionWidget configuration options for each
6406 * action item
6407 */
6408 OO.ui.MessageDialog.prototype.getSetupProcess = function ( data ) {
6409 data = data || {};
6410
6411 // Parent method
6412 return OO.ui.MessageDialog.super.prototype.getSetupProcess.call( this, data )
6413 .next( function () {
6414 this.title.setLabel(
6415 data.title !== undefined ? data.title : this.constructor.static.title
6416 );
6417 this.message.setLabel(
6418 data.message !== undefined ? data.message : this.constructor.static.message
6419 );
6420 this.message.$element.toggleClass(
6421 'oo-ui-messageDialog-message-verbose',
6422 data.verbose !== undefined ? data.verbose : this.constructor.static.verbose
6423 );
6424 }, this );
6425 };
6426
6427 /**
6428 * @inheritdoc
6429 */
6430 OO.ui.MessageDialog.prototype.getBodyHeight = function () {
6431 var bodyHeight, oldOverflow,
6432 $scrollable = this.container.$element;
6433
6434 oldOverflow = $scrollable[0].style.overflow;
6435 $scrollable[0].style.overflow = 'hidden';
6436
6437 // Force… ugh… something to happen
6438 $scrollable.contents().hide();
6439 $scrollable.height();
6440 $scrollable.contents().show();
6441
6442 bodyHeight = Math.round( this.text.$element.outerHeight( true ) );
6443 $scrollable[0].style.overflow = oldOverflow;
6444
6445 return bodyHeight;
6446 };
6447
6448 /**
6449 * @inheritdoc
6450 */
6451 OO.ui.MessageDialog.prototype.setDimensions = function ( dim ) {
6452 var $scrollable = this.container.$element;
6453 OO.ui.MessageDialog.super.prototype.setDimensions.call( this, dim );
6454
6455 // Twiddle the overflow property, otherwise an unnecessary scrollbar will be produced.
6456 // Need to do it after transition completes (250ms), add 50ms just in case.
6457 setTimeout( function () {
6458 var oldOverflow = $scrollable[0].style.overflow;
6459 $scrollable[0].style.overflow = 'hidden';
6460
6461 // Force… ugh… something to happen
6462 $scrollable.contents().hide();
6463 $scrollable.height();
6464 $scrollable.contents().show();
6465
6466 $scrollable[0].style.overflow = oldOverflow;
6467 }, 300 );
6468
6469 return this;
6470 };
6471
6472 /**
6473 * @inheritdoc
6474 */
6475 OO.ui.MessageDialog.prototype.initialize = function () {
6476 // Parent method
6477 OO.ui.MessageDialog.super.prototype.initialize.call( this );
6478
6479 // Properties
6480 this.$actions = this.$( '<div>' );
6481 this.container = new OO.ui.PanelLayout( {
6482 $: this.$, scrollable: true, classes: [ 'oo-ui-messageDialog-container' ]
6483 } );
6484 this.text = new OO.ui.PanelLayout( {
6485 $: this.$, padded: true, expanded: false, classes: [ 'oo-ui-messageDialog-text' ]
6486 } );
6487 this.message = new OO.ui.LabelWidget( {
6488 $: this.$, classes: [ 'oo-ui-messageDialog-message' ]
6489 } );
6490
6491 // Initialization
6492 this.title.$element.addClass( 'oo-ui-messageDialog-title' );
6493 this.$content.addClass( 'oo-ui-messageDialog-content' );
6494 this.container.$element.append( this.text.$element );
6495 this.text.$element.append( this.title.$element, this.message.$element );
6496 this.$body.append( this.container.$element );
6497 this.$actions.addClass( 'oo-ui-messageDialog-actions' );
6498 this.$foot.append( this.$actions );
6499 };
6500
6501 /**
6502 * @inheritdoc
6503 */
6504 OO.ui.MessageDialog.prototype.attachActions = function () {
6505 var i, len, other, special, others;
6506
6507 // Parent method
6508 OO.ui.MessageDialog.super.prototype.attachActions.call( this );
6509
6510 special = this.actions.getSpecial();
6511 others = this.actions.getOthers();
6512 if ( special.safe ) {
6513 this.$actions.append( special.safe.$element );
6514 special.safe.toggleFramed( false );
6515 }
6516 if ( others.length ) {
6517 for ( i = 0, len = others.length; i < len; i++ ) {
6518 other = others[i];
6519 this.$actions.append( other.$element );
6520 other.toggleFramed( false );
6521 }
6522 }
6523 if ( special.primary ) {
6524 this.$actions.append( special.primary.$element );
6525 special.primary.toggleFramed( false );
6526 }
6527
6528 if ( !this.isOpening() ) {
6529 // If the dialog is currently opening, this will be called automatically soon.
6530 // This also calls #fitActions.
6531 this.manager.updateWindowSize( this );
6532 }
6533 };
6534
6535 /**
6536 * Fit action actions into columns or rows.
6537 *
6538 * Columns will be used if all labels can fit without overflow, otherwise rows will be used.
6539 */
6540 OO.ui.MessageDialog.prototype.fitActions = function () {
6541 var i, len, action,
6542 previous = this.verticalActionLayout,
6543 actions = this.actions.get();
6544
6545 // Detect clipping
6546 this.toggleVerticalActionLayout( false );
6547 for ( i = 0, len = actions.length; i < len; i++ ) {
6548 action = actions[i];
6549 if ( action.$element.innerWidth() < action.$label.outerWidth( true ) ) {
6550 this.toggleVerticalActionLayout( true );
6551 break;
6552 }
6553 }
6554
6555 if ( this.verticalActionLayout !== previous ) {
6556 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
6557 // We changed the layout, window height might need to be updated.
6558 this.manager.updateWindowSize( this );
6559 }
6560 };
6561
6562 /**
6563 * Navigation dialog window.
6564 *
6565 * Logic:
6566 * - Show and hide errors.
6567 * - Retry an action.
6568 *
6569 * User interface:
6570 * - Renders header with dialog title and one action widget on either side
6571 * (a 'safe' button on the left, and a 'primary' button on the right, both of
6572 * which close the dialog).
6573 * - Displays any action widgets in the footer (none by default).
6574 * - Ability to dismiss errors.
6575 *
6576 * Subclass responsibilities:
6577 * - Register a 'safe' action.
6578 * - Register a 'primary' action.
6579 * - Add content to the dialog.
6580 *
6581 * @abstract
6582 * @class
6583 * @extends OO.ui.Dialog
6584 *
6585 * @constructor
6586 * @param {Object} [config] Configuration options
6587 */
6588 OO.ui.ProcessDialog = function OoUiProcessDialog( config ) {
6589 // Parent constructor
6590 OO.ui.ProcessDialog.super.call( this, config );
6591
6592 // Initialization
6593 this.$element.addClass( 'oo-ui-processDialog' );
6594 };
6595
6596 /* Setup */
6597
6598 OO.inheritClass( OO.ui.ProcessDialog, OO.ui.Dialog );
6599
6600 /* Methods */
6601
6602 /**
6603 * Handle dismiss button click events.
6604 *
6605 * Hides errors.
6606 */
6607 OO.ui.ProcessDialog.prototype.onDismissErrorButtonClick = function () {
6608 this.hideErrors();
6609 };
6610
6611 /**
6612 * Handle retry button click events.
6613 *
6614 * Hides errors and then tries again.
6615 */
6616 OO.ui.ProcessDialog.prototype.onRetryButtonClick = function () {
6617 this.hideErrors();
6618 this.executeAction( this.currentAction.getAction() );
6619 };
6620
6621 /**
6622 * @inheritdoc
6623 */
6624 OO.ui.ProcessDialog.prototype.onActionResize = function ( action ) {
6625 if ( this.actions.isSpecial( action ) ) {
6626 this.fitLabel();
6627 }
6628 return OO.ui.ProcessDialog.super.prototype.onActionResize.call( this, action );
6629 };
6630
6631 /**
6632 * @inheritdoc
6633 */
6634 OO.ui.ProcessDialog.prototype.initialize = function () {
6635 // Parent method
6636 OO.ui.ProcessDialog.super.prototype.initialize.call( this );
6637
6638 // Properties
6639 this.$navigation = this.$( '<div>' );
6640 this.$location = this.$( '<div>' );
6641 this.$safeActions = this.$( '<div>' );
6642 this.$primaryActions = this.$( '<div>' );
6643 this.$otherActions = this.$( '<div>' );
6644 this.dismissButton = new OO.ui.ButtonWidget( {
6645 $: this.$,
6646 label: OO.ui.msg( 'ooui-dialog-process-dismiss' )
6647 } );
6648 this.retryButton = new OO.ui.ButtonWidget( { $: this.$ } );
6649 this.$errors = this.$( '<div>' );
6650 this.$errorsTitle = this.$( '<div>' );
6651
6652 // Events
6653 this.dismissButton.connect( this, { click: 'onDismissErrorButtonClick' } );
6654 this.retryButton.connect( this, { click: 'onRetryButtonClick' } );
6655
6656 // Initialization
6657 this.title.$element.addClass( 'oo-ui-processDialog-title' );
6658 this.$location
6659 .append( this.title.$element )
6660 .addClass( 'oo-ui-processDialog-location' );
6661 this.$safeActions.addClass( 'oo-ui-processDialog-actions-safe' );
6662 this.$primaryActions.addClass( 'oo-ui-processDialog-actions-primary' );
6663 this.$otherActions.addClass( 'oo-ui-processDialog-actions-other' );
6664 this.$errorsTitle
6665 .addClass( 'oo-ui-processDialog-errors-title' )
6666 .text( OO.ui.msg( 'ooui-dialog-process-error' ) );
6667 this.$errors
6668 .addClass( 'oo-ui-processDialog-errors' )
6669 .append( this.$errorsTitle, this.dismissButton.$element, this.retryButton.$element );
6670 this.$content
6671 .addClass( 'oo-ui-processDialog-content' )
6672 .append( this.$errors );
6673 this.$navigation
6674 .addClass( 'oo-ui-processDialog-navigation' )
6675 .append( this.$safeActions, this.$location, this.$primaryActions );
6676 this.$head.append( this.$navigation );
6677 this.$foot.append( this.$otherActions );
6678 };
6679
6680 /**
6681 * @inheritdoc
6682 */
6683 OO.ui.ProcessDialog.prototype.attachActions = function () {
6684 var i, len, other, special, others;
6685
6686 // Parent method
6687 OO.ui.ProcessDialog.super.prototype.attachActions.call( this );
6688
6689 special = this.actions.getSpecial();
6690 others = this.actions.getOthers();
6691 if ( special.primary ) {
6692 this.$primaryActions.append( special.primary.$element );
6693 special.primary.toggleFramed( true );
6694 }
6695 if ( others.length ) {
6696 for ( i = 0, len = others.length; i < len; i++ ) {
6697 other = others[i];
6698 this.$otherActions.append( other.$element );
6699 other.toggleFramed( true );
6700 }
6701 }
6702 if ( special.safe ) {
6703 this.$safeActions.append( special.safe.$element );
6704 special.safe.toggleFramed( true );
6705 }
6706
6707 this.fitLabel();
6708 this.$body.css( 'bottom', this.$foot.outerHeight( true ) );
6709 };
6710
6711 /**
6712 * @inheritdoc
6713 */
6714 OO.ui.ProcessDialog.prototype.executeAction = function ( action ) {
6715 OO.ui.ProcessDialog.super.prototype.executeAction.call( this, action )
6716 .fail( this.showErrors.bind( this ) );
6717 };
6718
6719 /**
6720 * Fit label between actions.
6721 *
6722 * @chainable
6723 */
6724 OO.ui.ProcessDialog.prototype.fitLabel = function () {
6725 var width = Math.max(
6726 this.$safeActions.is( ':visible' ) ? this.$safeActions.width() : 0,
6727 this.$primaryActions.is( ':visible' ) ? this.$primaryActions.width() : 0
6728 );
6729 this.$location.css( { paddingLeft: width, paddingRight: width } );
6730
6731 return this;
6732 };
6733
6734 /**
6735 * Handle errors that occurred during accept or reject processes.
6736 *
6737 * @param {OO.ui.Error[]} errors Errors to be handled
6738 */
6739 OO.ui.ProcessDialog.prototype.showErrors = function ( errors ) {
6740 var i, len, $item,
6741 items = [],
6742 recoverable = true,
6743 warning = false;
6744
6745 for ( i = 0, len = errors.length; i < len; i++ ) {
6746 if ( !errors[i].isRecoverable() ) {
6747 recoverable = false;
6748 }
6749 if ( errors[i].isWarning() ) {
6750 warning = true;
6751 }
6752 $item = this.$( '<div>' )
6753 .addClass( 'oo-ui-processDialog-error' )
6754 .append( errors[i].getMessage() );
6755 items.push( $item[0] );
6756 }
6757 this.$errorItems = this.$( items );
6758 if ( recoverable ) {
6759 this.retryButton.clearFlags().setFlags( this.currentAction.getFlags() );
6760 } else {
6761 this.currentAction.setDisabled( true );
6762 }
6763 if ( warning ) {
6764 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-continue' ) );
6765 } else {
6766 this.retryButton.setLabel( OO.ui.msg( 'ooui-dialog-process-retry' ) );
6767 }
6768 this.retryButton.toggle( recoverable );
6769 this.$errorsTitle.after( this.$errorItems );
6770 this.$errors.show().scrollTop( 0 );
6771 };
6772
6773 /**
6774 * Hide errors.
6775 */
6776 OO.ui.ProcessDialog.prototype.hideErrors = function () {
6777 this.$errors.hide();
6778 this.$errorItems.remove();
6779 this.$errorItems = null;
6780 };
6781
6782 /**
6783 * Layout containing a series of pages.
6784 *
6785 * @class
6786 * @extends OO.ui.Layout
6787 *
6788 * @constructor
6789 * @param {Object} [config] Configuration options
6790 * @cfg {boolean} [continuous=false] Show all pages, one after another
6791 * @cfg {boolean} [autoFocus=true] Focus on the first focusable element when changing to a page
6792 * @cfg {boolean} [outlined=false] Show an outline
6793 * @cfg {boolean} [editable=false] Show controls for adding, removing and reordering pages
6794 */
6795 OO.ui.BookletLayout = function OoUiBookletLayout( config ) {
6796 // Configuration initialization
6797 config = config || {};
6798
6799 // Parent constructor
6800 OO.ui.BookletLayout.super.call( this, config );
6801
6802 // Properties
6803 this.currentPageName = null;
6804 this.pages = {};
6805 this.ignoreFocus = false;
6806 this.stackLayout = new OO.ui.StackLayout( { $: this.$, continuous: !!config.continuous } );
6807 this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;
6808 this.outlineVisible = false;
6809 this.outlined = !!config.outlined;
6810 if ( this.outlined ) {
6811 this.editable = !!config.editable;
6812 this.outlineControlsWidget = null;
6813 this.outlineSelectWidget = new OO.ui.OutlineSelectWidget( { $: this.$ } );
6814 this.outlinePanel = new OO.ui.PanelLayout( { $: this.$, scrollable: true } );
6815 this.gridLayout = new OO.ui.GridLayout(
6816 [ this.outlinePanel, this.stackLayout ],
6817 { $: this.$, widths: [ 1, 2 ] }
6818 );
6819 this.outlineVisible = true;
6820 if ( this.editable ) {
6821 this.outlineControlsWidget = new OO.ui.OutlineControlsWidget(
6822 this.outlineSelectWidget, { $: this.$ }
6823 );
6824 }
6825 }
6826
6827 // Events
6828 this.stackLayout.connect( this, { set: 'onStackLayoutSet' } );
6829 if ( this.outlined ) {
6830 this.outlineSelectWidget.connect( this, { select: 'onOutlineSelectWidgetSelect' } );
6831 }
6832 if ( this.autoFocus ) {
6833 // Event 'focus' does not bubble, but 'focusin' does
6834 this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
6835 }
6836
6837 // Initialization
6838 this.$element.addClass( 'oo-ui-bookletLayout' );
6839 this.stackLayout.$element.addClass( 'oo-ui-bookletLayout-stackLayout' );
6840 if ( this.outlined ) {
6841 this.outlinePanel.$element
6842 .addClass( 'oo-ui-bookletLayout-outlinePanel' )
6843 .append( this.outlineSelectWidget.$element );
6844 if ( this.editable ) {
6845 this.outlinePanel.$element
6846 .addClass( 'oo-ui-bookletLayout-outlinePanel-editable' )
6847 .append( this.outlineControlsWidget.$element );
6848 }
6849 this.$element.append( this.gridLayout.$element );
6850 } else {
6851 this.$element.append( this.stackLayout.$element );
6852 }
6853 };
6854
6855 /* Setup */
6856
6857 OO.inheritClass( OO.ui.BookletLayout, OO.ui.Layout );
6858
6859 /* Events */
6860
6861 /**
6862 * @event set
6863 * @param {OO.ui.PageLayout} page Current page
6864 */
6865
6866 /**
6867 * @event add
6868 * @param {OO.ui.PageLayout[]} page Added pages
6869 * @param {number} index Index pages were added at
6870 */
6871
6872 /**
6873 * @event remove
6874 * @param {OO.ui.PageLayout[]} pages Removed pages
6875 */
6876
6877 /* Methods */
6878
6879 /**
6880 * Handle stack layout focus.
6881 *
6882 * @param {jQuery.Event} e Focusin event
6883 */
6884 OO.ui.BookletLayout.prototype.onStackLayoutFocus = function ( e ) {
6885 var name, $target;
6886
6887 // Find the page that an element was focused within
6888 $target = $( e.target ).closest( '.oo-ui-pageLayout' );
6889 for ( name in this.pages ) {
6890 // Check for page match, exclude current page to find only page changes
6891 if ( this.pages[name].$element[0] === $target[0] && name !== this.currentPageName ) {
6892 this.setPage( name );
6893 break;
6894 }
6895 }
6896 };
6897
6898 /**
6899 * Handle stack layout set events.
6900 *
6901 * @param {OO.ui.PanelLayout|null} page The page panel that is now the current panel
6902 */
6903 OO.ui.BookletLayout.prototype.onStackLayoutSet = function ( page ) {
6904 var layout = this;
6905 if ( page ) {
6906 page.scrollElementIntoView( { complete: function () {
6907 if ( layout.autoFocus ) {
6908 layout.focus();
6909 }
6910 } } );
6911 }
6912 };
6913
6914 /**
6915 * Focus the first input in the current page.
6916 *
6917 * If no page is selected, the first selectable page will be selected.
6918 * If the focus is already in an element on the current page, nothing will happen.
6919 */
6920 OO.ui.BookletLayout.prototype.focus = function () {
6921 var $input, page = this.stackLayout.getCurrentItem();
6922 if ( !page && this.outlined ) {
6923 this.selectFirstSelectablePage();
6924 page = this.stackLayout.getCurrentItem();
6925 if ( !page ) {
6926 return;
6927 }
6928 }
6929 // Only change the focus if is not already in the current page
6930 if ( !page.$element.find( ':focus' ).length ) {
6931 $input = page.$element.find( ':input:first' );
6932 if ( $input.length ) {
6933 $input[0].focus();
6934 }
6935 }
6936 };
6937
6938 /**
6939 * Handle outline widget select events.
6940 *
6941 * @param {OO.ui.OptionWidget|null} item Selected item
6942 */
6943 OO.ui.BookletLayout.prototype.onOutlineSelectWidgetSelect = function ( item ) {
6944 if ( item ) {
6945 this.setPage( item.getData() );
6946 }
6947 };
6948
6949 /**
6950 * Check if booklet has an outline.
6951 *
6952 * @return {boolean}
6953 */
6954 OO.ui.BookletLayout.prototype.isOutlined = function () {
6955 return this.outlined;
6956 };
6957
6958 /**
6959 * Check if booklet has editing controls.
6960 *
6961 * @return {boolean}
6962 */
6963 OO.ui.BookletLayout.prototype.isEditable = function () {
6964 return this.editable;
6965 };
6966
6967 /**
6968 * Check if booklet has a visible outline.
6969 *
6970 * @return {boolean}
6971 */
6972 OO.ui.BookletLayout.prototype.isOutlineVisible = function () {
6973 return this.outlined && this.outlineVisible;
6974 };
6975
6976 /**
6977 * Hide or show the outline.
6978 *
6979 * @param {boolean} [show] Show outline, omit to invert current state
6980 * @chainable
6981 */
6982 OO.ui.BookletLayout.prototype.toggleOutline = function ( show ) {
6983 if ( this.outlined ) {
6984 show = show === undefined ? !this.outlineVisible : !!show;
6985 this.outlineVisible = show;
6986 this.gridLayout.layout( show ? [ 1, 2 ] : [ 0, 1 ], [ 1 ] );
6987 }
6988
6989 return this;
6990 };
6991
6992 /**
6993 * Get the outline widget.
6994 *
6995 * @param {OO.ui.PageLayout} page Page to be selected
6996 * @return {OO.ui.PageLayout|null} Closest page to another
6997 */
6998 OO.ui.BookletLayout.prototype.getClosestPage = function ( page ) {
6999 var next, prev, level,
7000 pages = this.stackLayout.getItems(),
7001 index = $.inArray( page, pages );
7002
7003 if ( index !== -1 ) {
7004 next = pages[index + 1];
7005 prev = pages[index - 1];
7006 // Prefer adjacent pages at the same level
7007 if ( this.outlined ) {
7008 level = this.outlineSelectWidget.getItemFromData( page.getName() ).getLevel();
7009 if (
7010 prev &&
7011 level === this.outlineSelectWidget.getItemFromData( prev.getName() ).getLevel()
7012 ) {
7013 return prev;
7014 }
7015 if (
7016 next &&
7017 level === this.outlineSelectWidget.getItemFromData( next.getName() ).getLevel()
7018 ) {
7019 return next;
7020 }
7021 }
7022 }
7023 return prev || next || null;
7024 };
7025
7026 /**
7027 * Get the outline widget.
7028 *
7029 * @return {OO.ui.OutlineSelectWidget|null} Outline widget, or null if booklet has no outline
7030 */
7031 OO.ui.BookletLayout.prototype.getOutline = function () {
7032 return this.outlineSelectWidget;
7033 };
7034
7035 /**
7036 * Get the outline controls widget. If the outline is not editable, null is returned.
7037 *
7038 * @return {OO.ui.OutlineControlsWidget|null} The outline controls widget.
7039 */
7040 OO.ui.BookletLayout.prototype.getOutlineControls = function () {
7041 return this.outlineControlsWidget;
7042 };
7043
7044 /**
7045 * Get a page by name.
7046 *
7047 * @param {string} name Symbolic name of page
7048 * @return {OO.ui.PageLayout|undefined} Page, if found
7049 */
7050 OO.ui.BookletLayout.prototype.getPage = function ( name ) {
7051 return this.pages[name];
7052 };
7053
7054 /**
7055 * Get the current page name.
7056 *
7057 * @return {string|null} Current page name
7058 */
7059 OO.ui.BookletLayout.prototype.getCurrentPageName = function () {
7060 return this.currentPageName;
7061 };
7062
7063 /**
7064 * Add a page to the layout.
7065 *
7066 * When pages are added with the same names as existing pages, the existing pages will be
7067 * automatically removed before the new pages are added.
7068 *
7069 * @param {OO.ui.PageLayout[]} pages Pages to add
7070 * @param {number} index Index to insert pages after
7071 * @fires add
7072 * @chainable
7073 */
7074 OO.ui.BookletLayout.prototype.addPages = function ( pages, index ) {
7075 var i, len, name, page, item, currentIndex,
7076 stackLayoutPages = this.stackLayout.getItems(),
7077 remove = [],
7078 items = [];
7079
7080 // Remove pages with same names
7081 for ( i = 0, len = pages.length; i < len; i++ ) {
7082 page = pages[i];
7083 name = page.getName();
7084
7085 if ( Object.prototype.hasOwnProperty.call( this.pages, name ) ) {
7086 // Correct the insertion index
7087 currentIndex = $.inArray( this.pages[name], stackLayoutPages );
7088 if ( currentIndex !== -1 && currentIndex + 1 < index ) {
7089 index--;
7090 }
7091 remove.push( this.pages[name] );
7092 }
7093 }
7094 if ( remove.length ) {
7095 this.removePages( remove );
7096 }
7097
7098 // Add new pages
7099 for ( i = 0, len = pages.length; i < len; i++ ) {
7100 page = pages[i];
7101 name = page.getName();
7102 this.pages[page.getName()] = page;
7103 if ( this.outlined ) {
7104 item = new OO.ui.OutlineOptionWidget( { $: this.$, data: name } );
7105 page.setOutlineItem( item );
7106 items.push( item );
7107 }
7108 }
7109
7110 if ( this.outlined && items.length ) {
7111 this.outlineSelectWidget.addItems( items, index );
7112 this.selectFirstSelectablePage();
7113 }
7114 this.stackLayout.addItems( pages, index );
7115 this.emit( 'add', pages, index );
7116
7117 return this;
7118 };
7119
7120 /**
7121 * Remove a page from the layout.
7122 *
7123 * @fires remove
7124 * @chainable
7125 */
7126 OO.ui.BookletLayout.prototype.removePages = function ( pages ) {
7127 var i, len, name, page,
7128 items = [];
7129
7130 for ( i = 0, len = pages.length; i < len; i++ ) {
7131 page = pages[i];
7132 name = page.getName();
7133 delete this.pages[name];
7134 if ( this.outlined ) {
7135 items.push( this.outlineSelectWidget.getItemFromData( name ) );
7136 page.setOutlineItem( null );
7137 }
7138 }
7139 if ( this.outlined && items.length ) {
7140 this.outlineSelectWidget.removeItems( items );
7141 this.selectFirstSelectablePage();
7142 }
7143 this.stackLayout.removeItems( pages );
7144 this.emit( 'remove', pages );
7145
7146 return this;
7147 };
7148
7149 /**
7150 * Clear all pages from the layout.
7151 *
7152 * @fires remove
7153 * @chainable
7154 */
7155 OO.ui.BookletLayout.prototype.clearPages = function () {
7156 var i, len,
7157 pages = this.stackLayout.getItems();
7158
7159 this.pages = {};
7160 this.currentPageName = null;
7161 if ( this.outlined ) {
7162 this.outlineSelectWidget.clearItems();
7163 for ( i = 0, len = pages.length; i < len; i++ ) {
7164 pages[i].setOutlineItem( null );
7165 }
7166 }
7167 this.stackLayout.clearItems();
7168
7169 this.emit( 'remove', pages );
7170
7171 return this;
7172 };
7173
7174 /**
7175 * Set the current page by name.
7176 *
7177 * @fires set
7178 * @param {string} name Symbolic name of page
7179 */
7180 OO.ui.BookletLayout.prototype.setPage = function ( name ) {
7181 var selectedItem,
7182 $focused,
7183 page = this.pages[name];
7184
7185 if ( name !== this.currentPageName ) {
7186 if ( this.outlined ) {
7187 selectedItem = this.outlineSelectWidget.getSelectedItem();
7188 if ( selectedItem && selectedItem.getData() !== name ) {
7189 this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getItemFromData( name ) );
7190 }
7191 }
7192 if ( page ) {
7193 if ( this.currentPageName && this.pages[this.currentPageName] ) {
7194 this.pages[this.currentPageName].setActive( false );
7195 // Blur anything focused if the next page doesn't have anything focusable - this
7196 // is not needed if the next page has something focusable because once it is focused
7197 // this blur happens automatically
7198 if ( this.autoFocus && !page.$element.find( ':input' ).length ) {
7199 $focused = this.pages[this.currentPageName].$element.find( ':focus' );
7200 if ( $focused.length ) {
7201 $focused[0].blur();
7202 }
7203 }
7204 }
7205 this.currentPageName = name;
7206 this.stackLayout.setItem( page );
7207 page.setActive( true );
7208 this.emit( 'set', page );
7209 }
7210 }
7211 };
7212
7213 /**
7214 * Select the first selectable page.
7215 *
7216 * @chainable
7217 */
7218 OO.ui.BookletLayout.prototype.selectFirstSelectablePage = function () {
7219 if ( !this.outlineSelectWidget.getSelectedItem() ) {
7220 this.outlineSelectWidget.selectItem( this.outlineSelectWidget.getFirstSelectableItem() );
7221 }
7222
7223 return this;
7224 };
7225
7226 /**
7227 * Layout made of a field and optional label.
7228 *
7229 * Available label alignment modes include:
7230 * - left: Label is before the field and aligned away from it, best for when the user will be
7231 * scanning for a specific label in a form with many fields
7232 * - right: Label is before the field and aligned toward it, best for forms the user is very
7233 * familiar with and will tab through field checking quickly to verify which field they are in
7234 * - top: Label is before the field and above it, best for when the user will need to fill out all
7235 * fields from top to bottom in a form with few fields
7236 * - inline: Label is after the field and aligned toward it, best for small boolean fields like
7237 * checkboxes or radio buttons
7238 *
7239 * @class
7240 * @extends OO.ui.Layout
7241 * @mixins OO.ui.LabelElement
7242 *
7243 * @constructor
7244 * @param {OO.ui.Widget} fieldWidget Field widget
7245 * @param {Object} [config] Configuration options
7246 * @cfg {string} [align='left'] Alignment mode, either 'left', 'right', 'top' or 'inline'
7247 * @cfg {string} [help] Explanatory text shown as a '?' icon.
7248 */
7249 OO.ui.FieldLayout = function OoUiFieldLayout( fieldWidget, config ) {
7250 var hasInputWidget = fieldWidget instanceof OO.ui.InputWidget;
7251
7252 // Configuration initialization
7253 config = $.extend( { align: 'left' }, config );
7254
7255 // Properties (must be set before parent constructor, which calls #getTagName)
7256 this.fieldWidget = fieldWidget;
7257
7258 // Parent constructor
7259 OO.ui.FieldLayout.super.call( this, config );
7260
7261 // Mixin constructors
7262 OO.ui.LabelElement.call( this, config );
7263
7264 // Properties
7265 this.$field = this.$( '<div>' );
7266 this.$body = this.$( '<' + ( hasInputWidget ? 'label' : 'div' ) + '>' );
7267 this.align = null;
7268 if ( config.help ) {
7269 this.popupButtonWidget = new OO.ui.PopupButtonWidget( {
7270 $: this.$,
7271 classes: [ 'oo-ui-fieldLayout-help' ],
7272 framed: false,
7273 icon: 'info'
7274 } );
7275
7276 this.popupButtonWidget.getPopup().$body.append(
7277 this.$( '<div>' )
7278 .text( config.help )
7279 .addClass( 'oo-ui-fieldLayout-help-content' )
7280 );
7281 this.$help = this.popupButtonWidget.$element;
7282 } else {
7283 this.$help = this.$( [] );
7284 }
7285
7286 // Events
7287 if ( hasInputWidget ) {
7288 this.$label.on( 'click', this.onLabelClick.bind( this ) );
7289 }
7290 this.fieldWidget.connect( this, { disable: 'onFieldDisable' } );
7291
7292 // Initialization
7293 this.$element
7294 .addClass( 'oo-ui-fieldLayout' )
7295 .append( this.$help, this.$body );
7296 this.$body.addClass( 'oo-ui-fieldLayout-body' );
7297 this.$field
7298 .addClass( 'oo-ui-fieldLayout-field' )
7299 .toggleClass( 'oo-ui-fieldLayout-disable', this.fieldWidget.isDisabled() )
7300 .append( this.fieldWidget.$element );
7301
7302 this.setAlignment( config.align );
7303 };
7304
7305 /* Setup */
7306
7307 OO.inheritClass( OO.ui.FieldLayout, OO.ui.Layout );
7308 OO.mixinClass( OO.ui.FieldLayout, OO.ui.LabelElement );
7309
7310 /* Methods */
7311
7312 /**
7313 * Handle field disable events.
7314 *
7315 * @param {boolean} value Field is disabled
7316 */
7317 OO.ui.FieldLayout.prototype.onFieldDisable = function ( value ) {
7318 this.$element.toggleClass( 'oo-ui-fieldLayout-disabled', value );
7319 };
7320
7321 /**
7322 * Handle label mouse click events.
7323 *
7324 * @param {jQuery.Event} e Mouse click event
7325 */
7326 OO.ui.FieldLayout.prototype.onLabelClick = function () {
7327 this.fieldWidget.simulateLabelClick();
7328 return false;
7329 };
7330
7331 /**
7332 * Get the field.
7333 *
7334 * @return {OO.ui.Widget} Field widget
7335 */
7336 OO.ui.FieldLayout.prototype.getField = function () {
7337 return this.fieldWidget;
7338 };
7339
7340 /**
7341 * Set the field alignment mode.
7342 *
7343 * @private
7344 * @param {string} value Alignment mode, either 'left', 'right', 'top' or 'inline'
7345 * @chainable
7346 */
7347 OO.ui.FieldLayout.prototype.setAlignment = function ( value ) {
7348 if ( value !== this.align ) {
7349 // Default to 'left'
7350 if ( [ 'left', 'right', 'top', 'inline' ].indexOf( value ) === -1 ) {
7351 value = 'left';
7352 }
7353 // Reorder elements
7354 if ( value === 'inline' ) {
7355 this.$body.append( this.$field, this.$label );
7356 } else {
7357 this.$body.append( this.$label, this.$field );
7358 }
7359 // Set classes. The following classes can be used here:
7360 // * oo-ui-fieldLayout-align-left
7361 // * oo-ui-fieldLayout-align-right
7362 // * oo-ui-fieldLayout-align-top
7363 // * oo-ui-fieldLayout-align-inline
7364 if ( this.align ) {
7365 this.$element.removeClass( 'oo-ui-fieldLayout-align-' + this.align );
7366 }
7367 this.$element.addClass( 'oo-ui-fieldLayout-align-' + value );
7368 this.align = value;
7369 }
7370
7371 return this;
7372 };
7373
7374 /**
7375 * Layout made of a fieldset and optional legend.
7376 *
7377 * Just add OO.ui.FieldLayout items.
7378 *
7379 * @class
7380 * @extends OO.ui.Layout
7381 * @mixins OO.ui.IconElement
7382 * @mixins OO.ui.LabelElement
7383 * @mixins OO.ui.GroupElement
7384 *
7385 * @constructor
7386 * @param {Object} [config] Configuration options
7387 * @cfg {OO.ui.FieldLayout[]} [items] Items to add
7388 */
7389 OO.ui.FieldsetLayout = function OoUiFieldsetLayout( config ) {
7390 // Configuration initialization
7391 config = config || {};
7392
7393 // Parent constructor
7394 OO.ui.FieldsetLayout.super.call( this, config );
7395
7396 // Mixin constructors
7397 OO.ui.IconElement.call( this, config );
7398 OO.ui.LabelElement.call( this, config );
7399 OO.ui.GroupElement.call( this, config );
7400
7401 // Initialization
7402 this.$element
7403 .addClass( 'oo-ui-fieldsetLayout' )
7404 .prepend( this.$icon, this.$label, this.$group );
7405 if ( $.isArray( config.items ) ) {
7406 this.addItems( config.items );
7407 }
7408 };
7409
7410 /* Setup */
7411
7412 OO.inheritClass( OO.ui.FieldsetLayout, OO.ui.Layout );
7413 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.IconElement );
7414 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.LabelElement );
7415 OO.mixinClass( OO.ui.FieldsetLayout, OO.ui.GroupElement );
7416
7417 /**
7418 * Layout with an HTML form.
7419 *
7420 * @class
7421 * @extends OO.ui.Layout
7422 *
7423 * @constructor
7424 * @param {Object} [config] Configuration options
7425 * @cfg {string} [method] HTML form `method` attribute
7426 * @cfg {string} [action] HTML form `action` attribute
7427 * @cfg {string} [enctype] HTML form `enctype` attribute
7428 */
7429 OO.ui.FormLayout = function OoUiFormLayout( config ) {
7430 // Configuration initialization
7431 config = config || {};
7432
7433 // Parent constructor
7434 OO.ui.FormLayout.super.call( this, config );
7435
7436 // Events
7437 this.$element.on( 'submit', this.onFormSubmit.bind( this ) );
7438
7439 // Initialization
7440 this.$element
7441 .addClass( 'oo-ui-formLayout' )
7442 .attr( {
7443 method: config.method,
7444 action: config.action,
7445 enctype: config.enctype
7446 } );
7447 };
7448
7449 /* Setup */
7450
7451 OO.inheritClass( OO.ui.FormLayout, OO.ui.Layout );
7452
7453 /* Events */
7454
7455 /**
7456 * @event submit
7457 */
7458
7459 /* Static Properties */
7460
7461 OO.ui.FormLayout.static.tagName = 'form';
7462
7463 /* Methods */
7464
7465 /**
7466 * Handle form submit events.
7467 *
7468 * @param {jQuery.Event} e Submit event
7469 * @fires submit
7470 */
7471 OO.ui.FormLayout.prototype.onFormSubmit = function () {
7472 this.emit( 'submit' );
7473 return false;
7474 };
7475
7476 /**
7477 * Layout made of proportionally sized columns and rows.
7478 *
7479 * @class
7480 * @extends OO.ui.Layout
7481 *
7482 * @constructor
7483 * @param {OO.ui.PanelLayout[]} panels Panels in the grid
7484 * @param {Object} [config] Configuration options
7485 * @cfg {number[]} [widths] Widths of columns as ratios
7486 * @cfg {number[]} [heights] Heights of rows as ratios
7487 */
7488 OO.ui.GridLayout = function OoUiGridLayout( panels, config ) {
7489 var i, len, widths;
7490
7491 // Configuration initialization
7492 config = config || {};
7493
7494 // Parent constructor
7495 OO.ui.GridLayout.super.call( this, config );
7496
7497 // Properties
7498 this.panels = [];
7499 this.widths = [];
7500 this.heights = [];
7501
7502 // Initialization
7503 this.$element.addClass( 'oo-ui-gridLayout' );
7504 for ( i = 0, len = panels.length; i < len; i++ ) {
7505 this.panels.push( panels[i] );
7506 this.$element.append( panels[i].$element );
7507 }
7508 if ( config.widths || config.heights ) {
7509 this.layout( config.widths || [ 1 ], config.heights || [ 1 ] );
7510 } else {
7511 // Arrange in columns by default
7512 widths = this.panels.map( function () { return 1; } );
7513 this.layout( widths, [ 1 ] );
7514 }
7515 };
7516
7517 /* Setup */
7518
7519 OO.inheritClass( OO.ui.GridLayout, OO.ui.Layout );
7520
7521 /* Events */
7522
7523 /**
7524 * @event layout
7525 */
7526
7527 /**
7528 * @event update
7529 */
7530
7531 /* Methods */
7532
7533 /**
7534 * Set grid dimensions.
7535 *
7536 * @param {number[]} widths Widths of columns as ratios
7537 * @param {number[]} heights Heights of rows as ratios
7538 * @fires layout
7539 * @throws {Error} If grid is not large enough to fit all panels
7540 */
7541 OO.ui.GridLayout.prototype.layout = function ( widths, heights ) {
7542 var x, y,
7543 xd = 0,
7544 yd = 0,
7545 cols = widths.length,
7546 rows = heights.length;
7547
7548 // Verify grid is big enough to fit panels
7549 if ( cols * rows < this.panels.length ) {
7550 throw new Error( 'Grid is not large enough to fit ' + this.panels.length + 'panels' );
7551 }
7552
7553 // Sum up denominators
7554 for ( x = 0; x < cols; x++ ) {
7555 xd += widths[x];
7556 }
7557 for ( y = 0; y < rows; y++ ) {
7558 yd += heights[y];
7559 }
7560 // Store factors
7561 this.widths = [];
7562 this.heights = [];
7563 for ( x = 0; x < cols; x++ ) {
7564 this.widths[x] = widths[x] / xd;
7565 }
7566 for ( y = 0; y < rows; y++ ) {
7567 this.heights[y] = heights[y] / yd;
7568 }
7569 // Synchronize view
7570 this.update();
7571 this.emit( 'layout' );
7572 };
7573
7574 /**
7575 * Update panel positions and sizes.
7576 *
7577 * @fires update
7578 */
7579 OO.ui.GridLayout.prototype.update = function () {
7580 var x, y, panel, width, height, dimensions,
7581 i = 0,
7582 top = 0,
7583 left = 0,
7584 cols = this.widths.length,
7585 rows = this.heights.length;
7586
7587 for ( y = 0; y < rows; y++ ) {
7588 height = this.heights[y];
7589 for ( x = 0; x < cols; x++ ) {
7590 width = this.widths[x];
7591 panel = this.panels[i];
7592 dimensions = {
7593 width: ( width * 100 ) + '%',
7594 height: ( height * 100 ) + '%',
7595 top: ( top * 100 ) + '%'
7596 };
7597 // If RTL, reverse:
7598 if ( OO.ui.Element.static.getDir( this.$.context ) === 'rtl' ) {
7599 dimensions.right = ( left * 100 ) + '%';
7600 } else {
7601 dimensions.left = ( left * 100 ) + '%';
7602 }
7603 // HACK: Work around IE bug by setting visibility: hidden; if width or height is zero
7604 if ( width === 0 || height === 0 ) {
7605 dimensions.visibility = 'hidden';
7606 } else {
7607 dimensions.visibility = '';
7608 }
7609 panel.$element.css( dimensions );
7610 i++;
7611 left += width;
7612 }
7613 top += height;
7614 left = 0;
7615 }
7616
7617 this.emit( 'update' );
7618 };
7619
7620 /**
7621 * Get a panel at a given position.
7622 *
7623 * The x and y position is affected by the current grid layout.
7624 *
7625 * @param {number} x Horizontal position
7626 * @param {number} y Vertical position
7627 * @return {OO.ui.PanelLayout} The panel at the given position
7628 */
7629 OO.ui.GridLayout.prototype.getPanel = function ( x, y ) {
7630 return this.panels[ ( x * this.widths.length ) + y ];
7631 };
7632
7633 /**
7634 * Layout that expands to cover the entire area of its parent, with optional scrolling and padding.
7635 *
7636 * @class
7637 * @extends OO.ui.Layout
7638 *
7639 * @constructor
7640 * @param {Object} [config] Configuration options
7641 * @cfg {boolean} [scrollable=false] Allow vertical scrolling
7642 * @cfg {boolean} [padded=false] Pad the content from the edges
7643 * @cfg {boolean} [expanded=true] Expand size to fill the entire parent element
7644 */
7645 OO.ui.PanelLayout = function OoUiPanelLayout( config ) {
7646 // Configuration initialization
7647 config = $.extend( {
7648 scrollable: false,
7649 padded: false,
7650 expanded: true
7651 }, config );
7652
7653 // Parent constructor
7654 OO.ui.PanelLayout.super.call( this, config );
7655
7656 // Initialization
7657 this.$element.addClass( 'oo-ui-panelLayout' );
7658 if ( config.scrollable ) {
7659 this.$element.addClass( 'oo-ui-panelLayout-scrollable' );
7660 }
7661 if ( config.padded ) {
7662 this.$element.addClass( 'oo-ui-panelLayout-padded' );
7663 }
7664 if ( config.expanded ) {
7665 this.$element.addClass( 'oo-ui-panelLayout-expanded' );
7666 }
7667 };
7668
7669 /* Setup */
7670
7671 OO.inheritClass( OO.ui.PanelLayout, OO.ui.Layout );
7672
7673 /**
7674 * Page within an booklet layout.
7675 *
7676 * @class
7677 * @extends OO.ui.PanelLayout
7678 *
7679 * @constructor
7680 * @param {string} name Unique symbolic name of page
7681 * @param {Object} [config] Configuration options
7682 */
7683 OO.ui.PageLayout = function OoUiPageLayout( name, config ) {
7684 // Configuration initialization
7685 config = $.extend( { scrollable: true }, config );
7686
7687 // Parent constructor
7688 OO.ui.PageLayout.super.call( this, config );
7689
7690 // Properties
7691 this.name = name;
7692 this.outlineItem = null;
7693 this.active = false;
7694
7695 // Initialization
7696 this.$element.addClass( 'oo-ui-pageLayout' );
7697 };
7698
7699 /* Setup */
7700
7701 OO.inheritClass( OO.ui.PageLayout, OO.ui.PanelLayout );
7702
7703 /* Events */
7704
7705 /**
7706 * @event active
7707 * @param {boolean} active Page is active
7708 */
7709
7710 /* Methods */
7711
7712 /**
7713 * Get page name.
7714 *
7715 * @return {string} Symbolic name of page
7716 */
7717 OO.ui.PageLayout.prototype.getName = function () {
7718 return this.name;
7719 };
7720
7721 /**
7722 * Check if page is active.
7723 *
7724 * @return {boolean} Page is active
7725 */
7726 OO.ui.PageLayout.prototype.isActive = function () {
7727 return this.active;
7728 };
7729
7730 /**
7731 * Get outline item.
7732 *
7733 * @return {OO.ui.OutlineOptionWidget|null} Outline item widget
7734 */
7735 OO.ui.PageLayout.prototype.getOutlineItem = function () {
7736 return this.outlineItem;
7737 };
7738
7739 /**
7740 * Set outline item.
7741 *
7742 * @localdoc Subclasses should override #setupOutlineItem instead of this method to adjust the
7743 * outline item as desired; this method is called for setting (with an object) and unsetting
7744 * (with null) and overriding methods would have to check the value of `outlineItem` to avoid
7745 * operating on null instead of an OO.ui.OutlineOptionWidget object.
7746 *
7747 * @param {OO.ui.OutlineOptionWidget|null} outlineItem Outline item widget, null to clear
7748 * @chainable
7749 */
7750 OO.ui.PageLayout.prototype.setOutlineItem = function ( outlineItem ) {
7751 this.outlineItem = outlineItem || null;
7752 if ( outlineItem ) {
7753 this.setupOutlineItem();
7754 }
7755 return this;
7756 };
7757
7758 /**
7759 * Setup outline item.
7760 *
7761 * @localdoc Subclasses should override this method to adjust the outline item as desired.
7762 *
7763 * @param {OO.ui.OutlineOptionWidget} outlineItem Outline item widget to setup
7764 * @chainable
7765 */
7766 OO.ui.PageLayout.prototype.setupOutlineItem = function () {
7767 return this;
7768 };
7769
7770 /**
7771 * Set page active state.
7772 *
7773 * @param {boolean} Page is active
7774 * @fires active
7775 */
7776 OO.ui.PageLayout.prototype.setActive = function ( active ) {
7777 active = !!active;
7778
7779 if ( active !== this.active ) {
7780 this.active = active;
7781 this.$element.toggleClass( 'oo-ui-pageLayout-active', active );
7782 this.emit( 'active', this.active );
7783 }
7784 };
7785
7786 /**
7787 * Layout containing a series of mutually exclusive pages.
7788 *
7789 * @class
7790 * @extends OO.ui.PanelLayout
7791 * @mixins OO.ui.GroupElement
7792 *
7793 * @constructor
7794 * @param {Object} [config] Configuration options
7795 * @cfg {boolean} [continuous=false] Show all pages, one after another
7796 * @cfg {OO.ui.Layout[]} [items] Layouts to add
7797 */
7798 OO.ui.StackLayout = function OoUiStackLayout( config ) {
7799 // Configuration initialization
7800 config = $.extend( { scrollable: true }, config );
7801
7802 // Parent constructor
7803 OO.ui.StackLayout.super.call( this, config );
7804
7805 // Mixin constructors
7806 OO.ui.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
7807
7808 // Properties
7809 this.currentItem = null;
7810 this.continuous = !!config.continuous;
7811
7812 // Initialization
7813 this.$element.addClass( 'oo-ui-stackLayout' );
7814 if ( this.continuous ) {
7815 this.$element.addClass( 'oo-ui-stackLayout-continuous' );
7816 }
7817 if ( $.isArray( config.items ) ) {
7818 this.addItems( config.items );
7819 }
7820 };
7821
7822 /* Setup */
7823
7824 OO.inheritClass( OO.ui.StackLayout, OO.ui.PanelLayout );
7825 OO.mixinClass( OO.ui.StackLayout, OO.ui.GroupElement );
7826
7827 /* Events */
7828
7829 /**
7830 * @event set
7831 * @param {OO.ui.Layout|null} item Current item or null if there is no longer a layout shown
7832 */
7833
7834 /* Methods */
7835
7836 /**
7837 * Get the current item.
7838 *
7839 * @return {OO.ui.Layout|null}
7840 */
7841 OO.ui.StackLayout.prototype.getCurrentItem = function () {
7842 return this.currentItem;
7843 };
7844
7845 /**
7846 * Unset the current item.
7847 *
7848 * @private
7849 * @param {OO.ui.StackLayout} layout
7850 * @fires set
7851 */
7852 OO.ui.StackLayout.prototype.unsetCurrentItem = function () {
7853 var prevItem = this.currentItem;
7854 if ( prevItem === null ) {
7855 return;
7856 }
7857
7858 this.currentItem = null;
7859 this.emit( 'set', null );
7860 };
7861
7862 /**
7863 * Add items.
7864 *
7865 * Adding an existing item (by value) will move it.
7866 *
7867 * @param {OO.ui.Layout[]} items Items to add
7868 * @param {number} [index] Index to insert items after
7869 * @chainable
7870 */
7871 OO.ui.StackLayout.prototype.addItems = function ( items, index ) {
7872 // Mixin method
7873 OO.ui.GroupElement.prototype.addItems.call( this, items, index );
7874
7875 if ( !this.currentItem && items.length ) {
7876 this.setItem( items[0] );
7877 }
7878
7879 return this;
7880 };
7881
7882 /**
7883 * Remove items.
7884 *
7885 * Items will be detached, not removed, so they can be used later.
7886 *
7887 * @param {OO.ui.Layout[]} items Items to remove
7888 * @chainable
7889 * @fires set
7890 */
7891 OO.ui.StackLayout.prototype.removeItems = function ( items ) {
7892 // Mixin method
7893 OO.ui.GroupElement.prototype.removeItems.call( this, items );
7894
7895 if ( $.inArray( this.currentItem, items ) !== -1 ) {
7896 if ( this.items.length ) {
7897 this.setItem( this.items[0] );
7898 } else {
7899 this.unsetCurrentItem();
7900 }
7901 }
7902
7903 return this;
7904 };
7905
7906 /**
7907 * Clear all items.
7908 *
7909 * Items will be detached, not removed, so they can be used later.
7910 *
7911 * @chainable
7912 * @fires set
7913 */
7914 OO.ui.StackLayout.prototype.clearItems = function () {
7915 this.unsetCurrentItem();
7916 OO.ui.GroupElement.prototype.clearItems.call( this );
7917
7918 return this;
7919 };
7920
7921 /**
7922 * Show item.
7923 *
7924 * Any currently shown item will be hidden.
7925 *
7926 * FIXME: If the passed item to show has not been added in the items list, then
7927 * this method drops it and unsets the current item.
7928 *
7929 * @param {OO.ui.Layout} item Item to show
7930 * @chainable
7931 * @fires set
7932 */
7933 OO.ui.StackLayout.prototype.setItem = function ( item ) {
7934 var i, len;
7935
7936 if ( item !== this.currentItem ) {
7937 if ( !this.continuous ) {
7938 for ( i = 0, len = this.items.length; i < len; i++ ) {
7939 this.items[i].$element.css( 'display', '' );
7940 }
7941 }
7942 if ( $.inArray( item, this.items ) !== -1 ) {
7943 if ( !this.continuous ) {
7944 item.$element.css( 'display', 'block' );
7945 }
7946 this.currentItem = item;
7947 this.emit( 'set', item );
7948 } else {
7949 this.unsetCurrentItem();
7950 }
7951 }
7952
7953 return this;
7954 };
7955
7956 /**
7957 * Horizontal bar layout of tools as icon buttons.
7958 *
7959 * @class
7960 * @extends OO.ui.ToolGroup
7961 *
7962 * @constructor
7963 * @param {OO.ui.Toolbar} toolbar
7964 * @param {Object} [config] Configuration options
7965 */
7966 OO.ui.BarToolGroup = function OoUiBarToolGroup( toolbar, config ) {
7967 // Parent constructor
7968 OO.ui.BarToolGroup.super.call( this, toolbar, config );
7969
7970 // Initialization
7971 this.$element.addClass( 'oo-ui-barToolGroup' );
7972 };
7973
7974 /* Setup */
7975
7976 OO.inheritClass( OO.ui.BarToolGroup, OO.ui.ToolGroup );
7977
7978 /* Static Properties */
7979
7980 OO.ui.BarToolGroup.static.titleTooltips = true;
7981
7982 OO.ui.BarToolGroup.static.accelTooltips = true;
7983
7984 OO.ui.BarToolGroup.static.name = 'bar';
7985
7986 /**
7987 * Popup list of tools with an icon and optional label.
7988 *
7989 * @abstract
7990 * @class
7991 * @extends OO.ui.ToolGroup
7992 * @mixins OO.ui.IconElement
7993 * @mixins OO.ui.IndicatorElement
7994 * @mixins OO.ui.LabelElement
7995 * @mixins OO.ui.TitledElement
7996 * @mixins OO.ui.ClippableElement
7997 *
7998 * @constructor
7999 * @param {OO.ui.Toolbar} toolbar
8000 * @param {Object} [config] Configuration options
8001 * @cfg {string} [header] Text to display at the top of the pop-up
8002 */
8003 OO.ui.PopupToolGroup = function OoUiPopupToolGroup( toolbar, config ) {
8004 // Configuration initialization
8005 config = config || {};
8006
8007 // Parent constructor
8008 OO.ui.PopupToolGroup.super.call( this, toolbar, config );
8009
8010 // Mixin constructors
8011 OO.ui.IconElement.call( this, config );
8012 OO.ui.IndicatorElement.call( this, config );
8013 OO.ui.LabelElement.call( this, config );
8014 OO.ui.TitledElement.call( this, config );
8015 OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
8016
8017 // Properties
8018 this.active = false;
8019 this.dragging = false;
8020 this.onBlurHandler = this.onBlur.bind( this );
8021 this.$handle = this.$( '<span>' );
8022
8023 // Events
8024 this.$handle.on( {
8025 'mousedown touchstart': this.onHandlePointerDown.bind( this ),
8026 'mouseup touchend': this.onHandlePointerUp.bind( this )
8027 } );
8028
8029 // Initialization
8030 this.$handle
8031 .addClass( 'oo-ui-popupToolGroup-handle' )
8032 .append( this.$icon, this.$label, this.$indicator );
8033 // If the pop-up should have a header, add it to the top of the toolGroup.
8034 // Note: If this feature is useful for other widgets, we could abstract it into an
8035 // OO.ui.HeaderedElement mixin constructor.
8036 if ( config.header !== undefined ) {
8037 this.$group
8038 .prepend( this.$( '<span>' )
8039 .addClass( 'oo-ui-popupToolGroup-header' )
8040 .text( config.header )
8041 );
8042 }
8043 this.$element
8044 .addClass( 'oo-ui-popupToolGroup' )
8045 .prepend( this.$handle );
8046 };
8047
8048 /* Setup */
8049
8050 OO.inheritClass( OO.ui.PopupToolGroup, OO.ui.ToolGroup );
8051 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IconElement );
8052 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.IndicatorElement );
8053 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.LabelElement );
8054 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.TitledElement );
8055 OO.mixinClass( OO.ui.PopupToolGroup, OO.ui.ClippableElement );
8056
8057 /* Static Properties */
8058
8059 /* Methods */
8060
8061 /**
8062 * @inheritdoc
8063 */
8064 OO.ui.PopupToolGroup.prototype.setDisabled = function () {
8065 // Parent method
8066 OO.ui.PopupToolGroup.super.prototype.setDisabled.apply( this, arguments );
8067
8068 if ( this.isDisabled() && this.isElementAttached() ) {
8069 this.setActive( false );
8070 }
8071 };
8072
8073 /**
8074 * Handle focus being lost.
8075 *
8076 * The event is actually generated from a mouseup, so it is not a normal blur event object.
8077 *
8078 * @param {jQuery.Event} e Mouse up event
8079 */
8080 OO.ui.PopupToolGroup.prototype.onBlur = function ( e ) {
8081 // Only deactivate when clicking outside the dropdown element
8082 if ( this.$( e.target ).closest( '.oo-ui-popupToolGroup' )[0] !== this.$element[0] ) {
8083 this.setActive( false );
8084 }
8085 };
8086
8087 /**
8088 * @inheritdoc
8089 */
8090 OO.ui.PopupToolGroup.prototype.onPointerUp = function ( e ) {
8091 // e.which is 0 for touch events, 1 for left mouse button
8092 // Only close toolgroup when a tool was actually selected
8093 // FIXME: this duplicates logic from the parent class
8094 if ( !this.isDisabled() && e.which <= 1 && this.pressed && this.pressed === this.getTargetTool( e ) ) {
8095 this.setActive( false );
8096 }
8097 return OO.ui.PopupToolGroup.super.prototype.onPointerUp.call( this, e );
8098 };
8099
8100 /**
8101 * Handle mouse up events.
8102 *
8103 * @param {jQuery.Event} e Mouse up event
8104 */
8105 OO.ui.PopupToolGroup.prototype.onHandlePointerUp = function () {
8106 return false;
8107 };
8108
8109 /**
8110 * Handle mouse down events.
8111 *
8112 * @param {jQuery.Event} e Mouse down event
8113 */
8114 OO.ui.PopupToolGroup.prototype.onHandlePointerDown = function ( e ) {
8115 // e.which is 0 for touch events, 1 for left mouse button
8116 if ( !this.isDisabled() && e.which <= 1 ) {
8117 this.setActive( !this.active );
8118 }
8119 return false;
8120 };
8121
8122 /**
8123 * Switch into active mode.
8124 *
8125 * When active, mouseup events anywhere in the document will trigger deactivation.
8126 */
8127 OO.ui.PopupToolGroup.prototype.setActive = function ( value ) {
8128 value = !!value;
8129 if ( this.active !== value ) {
8130 this.active = value;
8131 if ( value ) {
8132 this.getElementDocument().addEventListener( 'mouseup', this.onBlurHandler, true );
8133
8134 // Try anchoring the popup to the left first
8135 this.$element.addClass( 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left' );
8136 this.toggleClipping( true );
8137 if ( this.isClippedHorizontally() ) {
8138 // Anchoring to the left caused the popup to clip, so anchor it to the right instead
8139 this.toggleClipping( false );
8140 this.$element
8141 .removeClass( 'oo-ui-popupToolGroup-left' )
8142 .addClass( 'oo-ui-popupToolGroup-right' );
8143 this.toggleClipping( true );
8144 }
8145 } else {
8146 this.getElementDocument().removeEventListener( 'mouseup', this.onBlurHandler, true );
8147 this.$element.removeClass(
8148 'oo-ui-popupToolGroup-active oo-ui-popupToolGroup-left oo-ui-popupToolGroup-right'
8149 );
8150 this.toggleClipping( false );
8151 }
8152 }
8153 };
8154
8155 /**
8156 * Drop down list layout of tools as labeled icon buttons.
8157 *
8158 * This layout allows some tools to be collapsible, controlled by a "More" / "Fewer" option at the
8159 * bottom of the main list. These are not automatically positioned at the bottom of the list; you
8160 * may want to use the 'promote' and 'demote' configuration options to achieve this.
8161 *
8162 * @class
8163 * @extends OO.ui.PopupToolGroup
8164 *
8165 * @constructor
8166 * @param {OO.ui.Toolbar} toolbar
8167 * @param {Object} [config] Configuration options
8168 * @cfg {Array} [allowCollapse] List of tools that can be collapsed. Remaining tools will be always
8169 * shown.
8170 * @cfg {Array} [forceExpand] List of tools that *may not* be collapsed. All remaining tools will be
8171 * allowed to be collapsed.
8172 * @cfg {boolean} [expanded=false] Whether the collapsible tools are expanded by default
8173 */
8174 OO.ui.ListToolGroup = function OoUiListToolGroup( toolbar, config ) {
8175 // Configuration initialization
8176 config = config || {};
8177
8178 // Properties (must be set before parent constructor, which calls #populate)
8179 this.allowCollapse = config.allowCollapse;
8180 this.forceExpand = config.forceExpand;
8181 this.expanded = config.expanded !== undefined ? config.expanded : false;
8182 this.collapsibleTools = [];
8183
8184 // Parent constructor
8185 OO.ui.ListToolGroup.super.call( this, toolbar, config );
8186
8187 // Initialization
8188 this.$element.addClass( 'oo-ui-listToolGroup' );
8189 };
8190
8191 /* Setup */
8192
8193 OO.inheritClass( OO.ui.ListToolGroup, OO.ui.PopupToolGroup );
8194
8195 /* Static Properties */
8196
8197 OO.ui.ListToolGroup.static.accelTooltips = true;
8198
8199 OO.ui.ListToolGroup.static.name = 'list';
8200
8201 /* Methods */
8202
8203 /**
8204 * @inheritdoc
8205 */
8206 OO.ui.ListToolGroup.prototype.populate = function () {
8207 var i, len, allowCollapse = [];
8208
8209 OO.ui.ListToolGroup.super.prototype.populate.call( this );
8210
8211 // Update the list of collapsible tools
8212 if ( this.allowCollapse !== undefined ) {
8213 allowCollapse = this.allowCollapse;
8214 } else if ( this.forceExpand !== undefined ) {
8215 allowCollapse = OO.simpleArrayDifference( Object.keys( this.tools ), this.forceExpand );
8216 }
8217
8218 this.collapsibleTools = [];
8219 for ( i = 0, len = allowCollapse.length; i < len; i++ ) {
8220 if ( this.tools[ allowCollapse[i] ] !== undefined ) {
8221 this.collapsibleTools.push( this.tools[ allowCollapse[i] ] );
8222 }
8223 }
8224
8225 // Keep at the end, even when tools are added
8226 this.$group.append( this.getExpandCollapseTool().$element );
8227
8228 this.getExpandCollapseTool().toggle( this.collapsibleTools.length !== 0 );
8229
8230 // Calling jQuery's .hide() and then .show() on a detached element caches the default value of its
8231 // 'display' attribute and restores it, and the tool uses a <span> and can be hidden and re-shown.
8232 // Is this a jQuery bug? http://jsfiddle.net/gtj4hu3h/
8233 if ( this.getExpandCollapseTool().$element.css( 'display' ) === 'inline' ) {
8234 this.getExpandCollapseTool().$element.css( 'display', 'block' );
8235 }
8236
8237 this.updateCollapsibleState();
8238 };
8239
8240 OO.ui.ListToolGroup.prototype.getExpandCollapseTool = function () {
8241 if ( this.expandCollapseTool === undefined ) {
8242 var ExpandCollapseTool = function () {
8243 ExpandCollapseTool.super.apply( this, arguments );
8244 };
8245
8246 OO.inheritClass( ExpandCollapseTool, OO.ui.Tool );
8247
8248 ExpandCollapseTool.prototype.onSelect = function () {
8249 this.toolGroup.expanded = !this.toolGroup.expanded;
8250 this.toolGroup.updateCollapsibleState();
8251 this.setActive( false );
8252 };
8253 ExpandCollapseTool.prototype.onUpdateState = function () {
8254 // Do nothing. Tool interface requires an implementation of this function.
8255 };
8256
8257 ExpandCollapseTool.static.name = 'more-fewer';
8258
8259 this.expandCollapseTool = new ExpandCollapseTool( this );
8260 }
8261 return this.expandCollapseTool;
8262 };
8263
8264 /**
8265 * @inheritdoc
8266 */
8267 OO.ui.ListToolGroup.prototype.onPointerUp = function ( e ) {
8268 var ret = OO.ui.ListToolGroup.super.prototype.onPointerUp.call( this, e );
8269
8270 // Do not close the popup when the user wants to show more/fewer tools
8271 if ( this.$( e.target ).closest( '.oo-ui-tool-name-more-fewer' ).length ) {
8272 // Prevent the popup list from being hidden
8273 this.setActive( true );
8274 }
8275
8276 return ret;
8277 };
8278
8279 OO.ui.ListToolGroup.prototype.updateCollapsibleState = function () {
8280 var i, len;
8281
8282 this.getExpandCollapseTool()
8283 .setIcon( this.expanded ? 'collapse' : 'expand' )
8284 .setTitle( OO.ui.msg( this.expanded ? 'ooui-toolgroup-collapse' : 'ooui-toolgroup-expand' ) );
8285
8286 for ( i = 0, len = this.collapsibleTools.length; i < len; i++ ) {
8287 this.collapsibleTools[i].toggle( this.expanded );
8288 }
8289 };
8290
8291 /**
8292 * Drop down menu layout of tools as selectable menu items.
8293 *
8294 * @class
8295 * @extends OO.ui.PopupToolGroup
8296 *
8297 * @constructor
8298 * @param {OO.ui.Toolbar} toolbar
8299 * @param {Object} [config] Configuration options
8300 */
8301 OO.ui.MenuToolGroup = function OoUiMenuToolGroup( toolbar, config ) {
8302 // Configuration initialization
8303 config = config || {};
8304
8305 // Parent constructor
8306 OO.ui.MenuToolGroup.super.call( this, toolbar, config );
8307
8308 // Events
8309 this.toolbar.connect( this, { updateState: 'onUpdateState' } );
8310
8311 // Initialization
8312 this.$element.addClass( 'oo-ui-menuToolGroup' );
8313 };
8314
8315 /* Setup */
8316
8317 OO.inheritClass( OO.ui.MenuToolGroup, OO.ui.PopupToolGroup );
8318
8319 /* Static Properties */
8320
8321 OO.ui.MenuToolGroup.static.accelTooltips = true;
8322
8323 OO.ui.MenuToolGroup.static.name = 'menu';
8324
8325 /* Methods */
8326
8327 /**
8328 * Handle the toolbar state being updated.
8329 *
8330 * When the state changes, the title of each active item in the menu will be joined together and
8331 * used as a label for the group. The label will be empty if none of the items are active.
8332 */
8333 OO.ui.MenuToolGroup.prototype.onUpdateState = function () {
8334 var name,
8335 labelTexts = [];
8336
8337 for ( name in this.tools ) {
8338 if ( this.tools[name].isActive() ) {
8339 labelTexts.push( this.tools[name].getTitle() );
8340 }
8341 }
8342
8343 this.setLabel( labelTexts.join( ', ' ) || ' ' );
8344 };
8345
8346 /**
8347 * Tool that shows a popup when selected.
8348 *
8349 * @abstract
8350 * @class
8351 * @extends OO.ui.Tool
8352 * @mixins OO.ui.PopupElement
8353 *
8354 * @constructor
8355 * @param {OO.ui.Toolbar} toolbar
8356 * @param {Object} [config] Configuration options
8357 */
8358 OO.ui.PopupTool = function OoUiPopupTool( toolbar, config ) {
8359 // Parent constructor
8360 OO.ui.PopupTool.super.call( this, toolbar, config );
8361
8362 // Mixin constructors
8363 OO.ui.PopupElement.call( this, config );
8364
8365 // Initialization
8366 this.$element
8367 .addClass( 'oo-ui-popupTool' )
8368 .append( this.popup.$element );
8369 };
8370
8371 /* Setup */
8372
8373 OO.inheritClass( OO.ui.PopupTool, OO.ui.Tool );
8374 OO.mixinClass( OO.ui.PopupTool, OO.ui.PopupElement );
8375
8376 /* Methods */
8377
8378 /**
8379 * Handle the tool being selected.
8380 *
8381 * @inheritdoc
8382 */
8383 OO.ui.PopupTool.prototype.onSelect = function () {
8384 if ( !this.isDisabled() ) {
8385 this.popup.toggle();
8386 }
8387 this.setActive( false );
8388 return false;
8389 };
8390
8391 /**
8392 * Handle the toolbar state being updated.
8393 *
8394 * @inheritdoc
8395 */
8396 OO.ui.PopupTool.prototype.onUpdateState = function () {
8397 this.setActive( false );
8398 };
8399
8400 /**
8401 * Mixin for OO.ui.Widget subclasses to provide OO.ui.GroupElement.
8402 *
8403 * Use together with OO.ui.ItemWidget to make disabled state inheritable.
8404 *
8405 * @abstract
8406 * @class
8407 * @extends OO.ui.GroupElement
8408 *
8409 * @constructor
8410 * @param {Object} [config] Configuration options
8411 */
8412 OO.ui.GroupWidget = function OoUiGroupWidget( config ) {
8413 // Parent constructor
8414 OO.ui.GroupWidget.super.call( this, config );
8415 };
8416
8417 /* Setup */
8418
8419 OO.inheritClass( OO.ui.GroupWidget, OO.ui.GroupElement );
8420
8421 /* Methods */
8422
8423 /**
8424 * Set the disabled state of the widget.
8425 *
8426 * This will also update the disabled state of child widgets.
8427 *
8428 * @param {boolean} disabled Disable widget
8429 * @chainable
8430 */
8431 OO.ui.GroupWidget.prototype.setDisabled = function ( disabled ) {
8432 var i, len;
8433
8434 // Parent method
8435 // Note: Calling #setDisabled this way assumes this is mixed into an OO.ui.Widget
8436 OO.ui.Widget.prototype.setDisabled.call( this, disabled );
8437
8438 // During construction, #setDisabled is called before the OO.ui.GroupElement constructor
8439 if ( this.items ) {
8440 for ( i = 0, len = this.items.length; i < len; i++ ) {
8441 this.items[i].updateDisabled();
8442 }
8443 }
8444
8445 return this;
8446 };
8447
8448 /**
8449 * Mixin for widgets used as items in widgets that inherit OO.ui.GroupWidget.
8450 *
8451 * Item widgets have a reference to a OO.ui.GroupWidget while they are attached to the group. This
8452 * allows bidirectional communication.
8453 *
8454 * Use together with OO.ui.GroupWidget to make disabled state inheritable.
8455 *
8456 * @abstract
8457 * @class
8458 *
8459 * @constructor
8460 */
8461 OO.ui.ItemWidget = function OoUiItemWidget() {
8462 //
8463 };
8464
8465 /* Methods */
8466
8467 /**
8468 * Check if widget is disabled.
8469 *
8470 * Checks parent if present, making disabled state inheritable.
8471 *
8472 * @return {boolean} Widget is disabled
8473 */
8474 OO.ui.ItemWidget.prototype.isDisabled = function () {
8475 return this.disabled ||
8476 ( this.elementGroup instanceof OO.ui.Widget && this.elementGroup.isDisabled() );
8477 };
8478
8479 /**
8480 * Set group element is in.
8481 *
8482 * @param {OO.ui.GroupElement|null} group Group element, null if none
8483 * @chainable
8484 */
8485 OO.ui.ItemWidget.prototype.setElementGroup = function ( group ) {
8486 // Parent method
8487 // Note: Calling #setElementGroup this way assumes this is mixed into an OO.ui.Element
8488 OO.ui.Element.prototype.setElementGroup.call( this, group );
8489
8490 // Initialize item disabled states
8491 this.updateDisabled();
8492
8493 return this;
8494 };
8495
8496 /**
8497 * Mixin that adds a menu showing suggested values for a text input.
8498 *
8499 * Subclasses must handle `select` and `choose` events on #lookupMenu to make use of selections.
8500 *
8501 * Subclasses that set the value of #lookupInput from their `choose` or `select` handler should
8502 * be aware that this will cause new suggestions to be looked up for the new value. If this is
8503 * not desired, disable lookups with #setLookupsDisabled, then set the value, then re-enable lookups.
8504 *
8505 * @class
8506 * @abstract
8507 *
8508 * @constructor
8509 * @param {OO.ui.TextInputWidget} input Input widget
8510 * @param {Object} [config] Configuration options
8511 * @cfg {jQuery} [$overlay] Overlay for dropdown; defaults to relative positioning
8512 * @cfg {jQuery} [$container=input.$element] Element to render menu under
8513 */
8514 OO.ui.LookupInputWidget = function OoUiLookupInputWidget( input, config ) {
8515 // Configuration initialization
8516 config = config || {};
8517
8518 // Properties
8519 this.lookupInput = input;
8520 this.$overlay = config.$overlay || this.$element;
8521 this.lookupMenu = new OO.ui.TextInputMenuSelectWidget( this, {
8522 $: OO.ui.Element.static.getJQuery( this.$overlay ),
8523 input: this.lookupInput,
8524 $container: config.$container
8525 } );
8526 this.lookupCache = {};
8527 this.lookupQuery = null;
8528 this.lookupRequest = null;
8529 this.lookupsDisabled = false;
8530 this.lookupInputFocused = false;
8531
8532 // Events
8533 this.lookupInput.$input.on( {
8534 focus: this.onLookupInputFocus.bind( this ),
8535 blur: this.onLookupInputBlur.bind( this ),
8536 mousedown: this.onLookupInputMouseDown.bind( this )
8537 } );
8538 this.lookupInput.connect( this, { change: 'onLookupInputChange' } );
8539 this.lookupMenu.connect( this, { toggle: 'onLookupMenuToggle' } );
8540
8541 // Initialization
8542 this.$element.addClass( 'oo-ui-lookupWidget' );
8543 this.lookupMenu.$element.addClass( 'oo-ui-lookupWidget-menu' );
8544 this.$overlay.append( this.lookupMenu.$element );
8545 };
8546
8547 /* Methods */
8548
8549 /**
8550 * Handle input focus event.
8551 *
8552 * @param {jQuery.Event} e Input focus event
8553 */
8554 OO.ui.LookupInputWidget.prototype.onLookupInputFocus = function () {
8555 this.lookupInputFocused = true;
8556 this.populateLookupMenu();
8557 };
8558
8559 /**
8560 * Handle input blur event.
8561 *
8562 * @param {jQuery.Event} e Input blur event
8563 */
8564 OO.ui.LookupInputWidget.prototype.onLookupInputBlur = function () {
8565 this.closeLookupMenu();
8566 this.lookupInputFocused = false;
8567 };
8568
8569 /**
8570 * Handle input mouse down event.
8571 *
8572 * @param {jQuery.Event} e Input mouse down event
8573 */
8574 OO.ui.LookupInputWidget.prototype.onLookupInputMouseDown = function () {
8575 // Only open the menu if the input was already focused.
8576 // This way we allow the user to open the menu again after closing it with Esc
8577 // by clicking in the input. Opening (and populating) the menu when initially
8578 // clicking into the input is handled by the focus handler.
8579 if ( this.lookupInputFocused && !this.lookupMenu.isVisible() ) {
8580 this.populateLookupMenu();
8581 }
8582 };
8583
8584 /**
8585 * Handle input change event.
8586 *
8587 * @param {string} value New input value
8588 */
8589 OO.ui.LookupInputWidget.prototype.onLookupInputChange = function () {
8590 if ( this.lookupInputFocused ) {
8591 this.populateLookupMenu();
8592 }
8593 };
8594
8595 /**
8596 * Handle the lookup menu being shown/hidden.
8597 * @param {boolean} visible Whether the lookup menu is now visible.
8598 */
8599 OO.ui.LookupInputWidget.prototype.onLookupMenuToggle = function ( visible ) {
8600 if ( !visible ) {
8601 // When the menu is hidden, abort any active request and clear the menu.
8602 // This has to be done here in addition to closeLookupMenu(), because
8603 // MenuSelectWidget will close itself when the user presses Esc.
8604 this.abortLookupRequest();
8605 this.lookupMenu.clearItems();
8606 }
8607 };
8608
8609 /**
8610 * Get lookup menu.
8611 *
8612 * @return {OO.ui.TextInputMenuSelectWidget}
8613 */
8614 OO.ui.LookupInputWidget.prototype.getLookupMenu = function () {
8615 return this.lookupMenu;
8616 };
8617
8618 /**
8619 * Disable or re-enable lookups.
8620 *
8621 * When lookups are disabled, calls to #populateLookupMenu will be ignored.
8622 *
8623 * @param {boolean} disabled Disable lookups
8624 */
8625 OO.ui.LookupInputWidget.prototype.setLookupsDisabled = function ( disabled ) {
8626 this.lookupsDisabled = !!disabled;
8627 };
8628
8629 /**
8630 * Open the menu. If there are no entries in the menu, this does nothing.
8631 *
8632 * @chainable
8633 */
8634 OO.ui.LookupInputWidget.prototype.openLookupMenu = function () {
8635 if ( !this.lookupMenu.isEmpty() ) {
8636 this.lookupMenu.toggle( true );
8637 }
8638 return this;
8639 };
8640
8641 /**
8642 * Close the menu, empty it, and abort any pending request.
8643 *
8644 * @chainable
8645 */
8646 OO.ui.LookupInputWidget.prototype.closeLookupMenu = function () {
8647 this.lookupMenu.toggle( false );
8648 this.abortLookupRequest();
8649 this.lookupMenu.clearItems();
8650 return this;
8651 };
8652
8653 /**
8654 * Request menu items based on the input's current value, and when they arrive,
8655 * populate the menu with these items and show the menu.
8656 *
8657 * If lookups have been disabled with #setLookupsDisabled, this function does nothing.
8658 *
8659 * @chainable
8660 */
8661 OO.ui.LookupInputWidget.prototype.populateLookupMenu = function () {
8662 var widget = this,
8663 value = this.lookupInput.getValue();
8664
8665 if ( this.lookupsDisabled ) {
8666 return;
8667 }
8668
8669 // If the input is empty, clear the menu
8670 if ( value === '' ) {
8671 this.closeLookupMenu();
8672 // Skip population if there is already a request pending for the current value
8673 } else if ( value !== this.lookupQuery ) {
8674 this.getLookupMenuItems()
8675 .done( function ( items ) {
8676 widget.lookupMenu.clearItems();
8677 if ( items.length ) {
8678 widget.lookupMenu
8679 .addItems( items )
8680 .toggle( true );
8681 widget.initializeLookupMenuSelection();
8682 } else {
8683 widget.lookupMenu.toggle( false );
8684 }
8685 } )
8686 .fail( function () {
8687 widget.lookupMenu.clearItems();
8688 } );
8689 }
8690
8691 return this;
8692 };
8693
8694 /**
8695 * Select and highlight the first selectable item in the menu.
8696 *
8697 * @chainable
8698 */
8699 OO.ui.LookupInputWidget.prototype.initializeLookupMenuSelection = function () {
8700 if ( !this.lookupMenu.getSelectedItem() ) {
8701 this.lookupMenu.selectItem( this.lookupMenu.getFirstSelectableItem() );
8702 }
8703 this.lookupMenu.highlightItem( this.lookupMenu.getSelectedItem() );
8704 };
8705
8706 /**
8707 * Get lookup menu items for the current query.
8708 *
8709 * @return {jQuery.Promise} Promise object which will be passed menu items as the first argument
8710 * of the done event. If the request was aborted to make way for a subsequent request,
8711 * this promise will not be rejected: it will remain pending forever.
8712 */
8713 OO.ui.LookupInputWidget.prototype.getLookupMenuItems = function () {
8714 var widget = this,
8715 value = this.lookupInput.getValue(),
8716 deferred = $.Deferred(),
8717 ourRequest;
8718
8719 this.abortLookupRequest();
8720 if ( Object.prototype.hasOwnProperty.call( this.lookupCache, value ) ) {
8721 deferred.resolve( this.getLookupMenuItemsFromData( this.lookupCache[value] ) );
8722 } else {
8723 this.lookupInput.pushPending();
8724 this.lookupQuery = value;
8725 ourRequest = this.lookupRequest = this.getLookupRequest();
8726 ourRequest
8727 .always( function () {
8728 // We need to pop pending even if this is an old request, otherwise
8729 // the widget will remain pending forever.
8730 // TODO: this assumes that an aborted request will fail or succeed soon after
8731 // being aborted, or at least eventually. It would be nice if we could popPending()
8732 // at abort time, but only if we knew that we hadn't already called popPending()
8733 // for that request.
8734 widget.lookupInput.popPending();
8735 } )
8736 .done( function ( data ) {
8737 // If this is an old request (and aborting it somehow caused it to still succeed),
8738 // ignore its success completely
8739 if ( ourRequest === widget.lookupRequest ) {
8740 widget.lookupQuery = null;
8741 widget.lookupRequest = null;
8742 widget.lookupCache[value] = widget.getLookupCacheItemFromData( data );
8743 deferred.resolve( widget.getLookupMenuItemsFromData( widget.lookupCache[value] ) );
8744 }
8745 } )
8746 .fail( function () {
8747 // If this is an old request (or a request failing because it's being aborted),
8748 // ignore its failure completely
8749 if ( ourRequest === widget.lookupRequest ) {
8750 widget.lookupQuery = null;
8751 widget.lookupRequest = null;
8752 deferred.reject();
8753 }
8754 } );
8755 }
8756 return deferred.promise();
8757 };
8758
8759 /**
8760 * Abort the currently pending lookup request, if any.
8761 */
8762 OO.ui.LookupInputWidget.prototype.abortLookupRequest = function () {
8763 var oldRequest = this.lookupRequest;
8764 if ( oldRequest ) {
8765 // First unset this.lookupRequest to the fail handler will notice
8766 // that the request is no longer current
8767 this.lookupRequest = null;
8768 this.lookupQuery = null;
8769 oldRequest.abort();
8770 }
8771 };
8772
8773 /**
8774 * Get a new request object of the current lookup query value.
8775 *
8776 * @abstract
8777 * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
8778 */
8779 OO.ui.LookupInputWidget.prototype.getLookupRequest = function () {
8780 // Stub, implemented in subclass
8781 return null;
8782 };
8783
8784 /**
8785 * Get a list of menu item widgets from the data stored by the lookup request's done handler.
8786 *
8787 * @abstract
8788 * @param {Mixed} data Cached result data, usually an array
8789 * @return {OO.ui.MenuOptionWidget[]} Menu items
8790 */
8791 OO.ui.LookupInputWidget.prototype.getLookupMenuItemsFromData = function () {
8792 // Stub, implemented in subclass
8793 return [];
8794 };
8795
8796 /**
8797 * Get lookup cache item from server response data.
8798 *
8799 * @abstract
8800 * @param {Mixed} data Response from server
8801 * @return {Mixed} Cached result data
8802 */
8803 OO.ui.LookupInputWidget.prototype.getLookupCacheItemFromData = function () {
8804 // Stub, implemented in subclass
8805 return [];
8806 };
8807
8808 /**
8809 * Set of controls for an OO.ui.OutlineSelectWidget.
8810 *
8811 * Controls include moving items up and down, removing items, and adding different kinds of items.
8812 *
8813 * @class
8814 * @extends OO.ui.Widget
8815 * @mixins OO.ui.GroupElement
8816 * @mixins OO.ui.IconElement
8817 *
8818 * @constructor
8819 * @param {OO.ui.OutlineSelectWidget} outline Outline to control
8820 * @param {Object} [config] Configuration options
8821 */
8822 OO.ui.OutlineControlsWidget = function OoUiOutlineControlsWidget( outline, config ) {
8823 // Configuration initialization
8824 config = $.extend( { icon: 'add' }, config );
8825
8826 // Parent constructor
8827 OO.ui.OutlineControlsWidget.super.call( this, config );
8828
8829 // Mixin constructors
8830 OO.ui.GroupElement.call( this, config );
8831 OO.ui.IconElement.call( this, config );
8832
8833 // Properties
8834 this.outline = outline;
8835 this.$movers = this.$( '<div>' );
8836 this.upButton = new OO.ui.ButtonWidget( {
8837 $: this.$,
8838 framed: false,
8839 icon: 'collapse',
8840 title: OO.ui.msg( 'ooui-outline-control-move-up' )
8841 } );
8842 this.downButton = new OO.ui.ButtonWidget( {
8843 $: this.$,
8844 framed: false,
8845 icon: 'expand',
8846 title: OO.ui.msg( 'ooui-outline-control-move-down' )
8847 } );
8848 this.removeButton = new OO.ui.ButtonWidget( {
8849 $: this.$,
8850 framed: false,
8851 icon: 'remove',
8852 title: OO.ui.msg( 'ooui-outline-control-remove' )
8853 } );
8854
8855 // Events
8856 outline.connect( this, {
8857 select: 'onOutlineChange',
8858 add: 'onOutlineChange',
8859 remove: 'onOutlineChange'
8860 } );
8861 this.upButton.connect( this, { click: [ 'emit', 'move', -1 ] } );
8862 this.downButton.connect( this, { click: [ 'emit', 'move', 1 ] } );
8863 this.removeButton.connect( this, { click: [ 'emit', 'remove' ] } );
8864
8865 // Initialization
8866 this.$element.addClass( 'oo-ui-outlineControlsWidget' );
8867 this.$group.addClass( 'oo-ui-outlineControlsWidget-items' );
8868 this.$movers
8869 .addClass( 'oo-ui-outlineControlsWidget-movers' )
8870 .append( this.removeButton.$element, this.upButton.$element, this.downButton.$element );
8871 this.$element.append( this.$icon, this.$group, this.$movers );
8872 };
8873
8874 /* Setup */
8875
8876 OO.inheritClass( OO.ui.OutlineControlsWidget, OO.ui.Widget );
8877 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.GroupElement );
8878 OO.mixinClass( OO.ui.OutlineControlsWidget, OO.ui.IconElement );
8879
8880 /* Events */
8881
8882 /**
8883 * @event move
8884 * @param {number} places Number of places to move
8885 */
8886
8887 /**
8888 * @event remove
8889 */
8890
8891 /* Methods */
8892
8893 /**
8894 * Handle outline change events.
8895 */
8896 OO.ui.OutlineControlsWidget.prototype.onOutlineChange = function () {
8897 var i, len, firstMovable, lastMovable,
8898 items = this.outline.getItems(),
8899 selectedItem = this.outline.getSelectedItem(),
8900 movable = selectedItem && selectedItem.isMovable(),
8901 removable = selectedItem && selectedItem.isRemovable();
8902
8903 if ( movable ) {
8904 i = -1;
8905 len = items.length;
8906 while ( ++i < len ) {
8907 if ( items[i].isMovable() ) {
8908 firstMovable = items[i];
8909 break;
8910 }
8911 }
8912 i = len;
8913 while ( i-- ) {
8914 if ( items[i].isMovable() ) {
8915 lastMovable = items[i];
8916 break;
8917 }
8918 }
8919 }
8920 this.upButton.setDisabled( !movable || selectedItem === firstMovable );
8921 this.downButton.setDisabled( !movable || selectedItem === lastMovable );
8922 this.removeButton.setDisabled( !removable );
8923 };
8924
8925 /**
8926 * Mixin for widgets with a boolean on/off state.
8927 *
8928 * @abstract
8929 * @class
8930 *
8931 * @constructor
8932 * @param {Object} [config] Configuration options
8933 * @cfg {boolean} [value=false] Initial value
8934 */
8935 OO.ui.ToggleWidget = function OoUiToggleWidget( config ) {
8936 // Configuration initialization
8937 config = config || {};
8938
8939 // Properties
8940 this.value = null;
8941
8942 // Initialization
8943 this.$element.addClass( 'oo-ui-toggleWidget' );
8944 this.setValue( !!config.value );
8945 };
8946
8947 /* Events */
8948
8949 /**
8950 * @event change
8951 * @param {boolean} value Changed value
8952 */
8953
8954 /* Methods */
8955
8956 /**
8957 * Get the value of the toggle.
8958 *
8959 * @return {boolean}
8960 */
8961 OO.ui.ToggleWidget.prototype.getValue = function () {
8962 return this.value;
8963 };
8964
8965 /**
8966 * Set the value of the toggle.
8967 *
8968 * @param {boolean} value New value
8969 * @fires change
8970 * @chainable
8971 */
8972 OO.ui.ToggleWidget.prototype.setValue = function ( value ) {
8973 value = !!value;
8974 if ( this.value !== value ) {
8975 this.value = value;
8976 this.emit( 'change', value );
8977 this.$element.toggleClass( 'oo-ui-toggleWidget-on', value );
8978 this.$element.toggleClass( 'oo-ui-toggleWidget-off', !value );
8979 }
8980 return this;
8981 };
8982
8983 /**
8984 * Group widget for multiple related buttons.
8985 *
8986 * Use together with OO.ui.ButtonWidget.
8987 *
8988 * @class
8989 * @extends OO.ui.Widget
8990 * @mixins OO.ui.GroupElement
8991 *
8992 * @constructor
8993 * @param {Object} [config] Configuration options
8994 * @cfg {OO.ui.ButtonWidget[]} [items] Buttons to add
8995 */
8996 OO.ui.ButtonGroupWidget = function OoUiButtonGroupWidget( config ) {
8997 // Configuration initialization
8998 config = config || {};
8999
9000 // Parent constructor
9001 OO.ui.ButtonGroupWidget.super.call( this, config );
9002
9003 // Mixin constructors
9004 OO.ui.GroupElement.call( this, $.extend( {}, config, { $group: this.$element } ) );
9005
9006 // Initialization
9007 this.$element.addClass( 'oo-ui-buttonGroupWidget' );
9008 if ( $.isArray( config.items ) ) {
9009 this.addItems( config.items );
9010 }
9011 };
9012
9013 /* Setup */
9014
9015 OO.inheritClass( OO.ui.ButtonGroupWidget, OO.ui.Widget );
9016 OO.mixinClass( OO.ui.ButtonGroupWidget, OO.ui.GroupElement );
9017
9018 /**
9019 * Generic widget for buttons.
9020 *
9021 * @class
9022 * @extends OO.ui.Widget
9023 * @mixins OO.ui.ButtonElement
9024 * @mixins OO.ui.IconElement
9025 * @mixins OO.ui.IndicatorElement
9026 * @mixins OO.ui.LabelElement
9027 * @mixins OO.ui.TitledElement
9028 * @mixins OO.ui.FlaggedElement
9029 *
9030 * @constructor
9031 * @param {Object} [config] Configuration options
9032 * @cfg {string} [href] Hyperlink to visit when clicked
9033 * @cfg {string} [target] Target to open hyperlink in
9034 */
9035 OO.ui.ButtonWidget = function OoUiButtonWidget( config ) {
9036 // Configuration initialization
9037 config = config || {};
9038
9039 // Parent constructor
9040 OO.ui.ButtonWidget.super.call( this, config );
9041
9042 // Mixin constructors
9043 OO.ui.ButtonElement.call( this, config );
9044 OO.ui.IconElement.call( this, config );
9045 OO.ui.IndicatorElement.call( this, config );
9046 OO.ui.LabelElement.call( this, config );
9047 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$button } ) );
9048 OO.ui.FlaggedElement.call( this, config );
9049
9050 // Properties
9051 this.href = null;
9052 this.target = null;
9053 this.isHyperlink = false;
9054
9055 // Events
9056 this.$button.on( {
9057 click: this.onClick.bind( this ),
9058 keypress: this.onKeyPress.bind( this )
9059 } );
9060
9061 // Initialization
9062 this.$button.append( this.$icon, this.$label, this.$indicator );
9063 this.$element
9064 .addClass( 'oo-ui-buttonWidget' )
9065 .append( this.$button );
9066 this.setHref( config.href );
9067 this.setTarget( config.target );
9068 };
9069
9070 /* Setup */
9071
9072 OO.inheritClass( OO.ui.ButtonWidget, OO.ui.Widget );
9073 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.ButtonElement );
9074 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IconElement );
9075 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.IndicatorElement );
9076 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.LabelElement );
9077 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.TitledElement );
9078 OO.mixinClass( OO.ui.ButtonWidget, OO.ui.FlaggedElement );
9079
9080 /* Events */
9081
9082 /**
9083 * @event click
9084 */
9085
9086 /* Methods */
9087
9088 /**
9089 * Handles mouse click events.
9090 *
9091 * @param {jQuery.Event} e Mouse click event
9092 * @fires click
9093 */
9094 OO.ui.ButtonWidget.prototype.onClick = function () {
9095 if ( !this.isDisabled() ) {
9096 this.emit( 'click' );
9097 if ( this.isHyperlink ) {
9098 return true;
9099 }
9100 }
9101 return false;
9102 };
9103
9104 /**
9105 * Handles keypress events.
9106 *
9107 * @param {jQuery.Event} e Keypress event
9108 * @fires click
9109 */
9110 OO.ui.ButtonWidget.prototype.onKeyPress = function ( e ) {
9111 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
9112 this.emit( 'click' );
9113 if ( this.isHyperlink ) {
9114 return true;
9115 }
9116 }
9117 return false;
9118 };
9119
9120 /**
9121 * Get hyperlink location.
9122 *
9123 * @return {string} Hyperlink location
9124 */
9125 OO.ui.ButtonWidget.prototype.getHref = function () {
9126 return this.href;
9127 };
9128
9129 /**
9130 * Get hyperlink target.
9131 *
9132 * @return {string} Hyperlink target
9133 */
9134 OO.ui.ButtonWidget.prototype.getTarget = function () {
9135 return this.target;
9136 };
9137
9138 /**
9139 * Set hyperlink location.
9140 *
9141 * @param {string|null} href Hyperlink location, null to remove
9142 */
9143 OO.ui.ButtonWidget.prototype.setHref = function ( href ) {
9144 href = typeof href === 'string' ? href : null;
9145
9146 if ( href !== this.href ) {
9147 this.href = href;
9148 if ( href !== null ) {
9149 this.$button.attr( 'href', href );
9150 this.isHyperlink = true;
9151 } else {
9152 this.$button.removeAttr( 'href' );
9153 this.isHyperlink = false;
9154 }
9155 }
9156
9157 return this;
9158 };
9159
9160 /**
9161 * Set hyperlink target.
9162 *
9163 * @param {string|null} target Hyperlink target, null to remove
9164 */
9165 OO.ui.ButtonWidget.prototype.setTarget = function ( target ) {
9166 target = typeof target === 'string' ? target : null;
9167
9168 if ( target !== this.target ) {
9169 this.target = target;
9170 if ( target !== null ) {
9171 this.$button.attr( 'target', target );
9172 } else {
9173 this.$button.removeAttr( 'target' );
9174 }
9175 }
9176
9177 return this;
9178 };
9179
9180 /**
9181 * Button widget that executes an action and is managed by an OO.ui.ActionSet.
9182 *
9183 * @class
9184 * @extends OO.ui.ButtonWidget
9185 * @mixins OO.ui.PendingElement
9186 *
9187 * @constructor
9188 * @param {Object} [config] Configuration options
9189 * @cfg {string} [action] Symbolic action name
9190 * @cfg {string[]} [modes] Symbolic mode names
9191 * @cfg {boolean} [framed=false] Render button with a frame
9192 */
9193 OO.ui.ActionWidget = function OoUiActionWidget( config ) {
9194 // Configuration initialization
9195 config = $.extend( { framed: false }, config );
9196
9197 // Parent constructor
9198 OO.ui.ActionWidget.super.call( this, config );
9199
9200 // Mixin constructors
9201 OO.ui.PendingElement.call( this, config );
9202
9203 // Properties
9204 this.action = config.action || '';
9205 this.modes = config.modes || [];
9206 this.width = 0;
9207 this.height = 0;
9208
9209 // Initialization
9210 this.$element.addClass( 'oo-ui-actionWidget' );
9211 };
9212
9213 /* Setup */
9214
9215 OO.inheritClass( OO.ui.ActionWidget, OO.ui.ButtonWidget );
9216 OO.mixinClass( OO.ui.ActionWidget, OO.ui.PendingElement );
9217
9218 /* Events */
9219
9220 /**
9221 * @event resize
9222 */
9223
9224 /* Methods */
9225
9226 /**
9227 * Check if action is available in a certain mode.
9228 *
9229 * @param {string} mode Name of mode
9230 * @return {boolean} Has mode
9231 */
9232 OO.ui.ActionWidget.prototype.hasMode = function ( mode ) {
9233 return this.modes.indexOf( mode ) !== -1;
9234 };
9235
9236 /**
9237 * Get symbolic action name.
9238 *
9239 * @return {string}
9240 */
9241 OO.ui.ActionWidget.prototype.getAction = function () {
9242 return this.action;
9243 };
9244
9245 /**
9246 * Get symbolic action name.
9247 *
9248 * @return {string}
9249 */
9250 OO.ui.ActionWidget.prototype.getModes = function () {
9251 return this.modes.slice();
9252 };
9253
9254 /**
9255 * Emit a resize event if the size has changed.
9256 *
9257 * @chainable
9258 */
9259 OO.ui.ActionWidget.prototype.propagateResize = function () {
9260 var width, height;
9261
9262 if ( this.isElementAttached() ) {
9263 width = this.$element.width();
9264 height = this.$element.height();
9265
9266 if ( width !== this.width || height !== this.height ) {
9267 this.width = width;
9268 this.height = height;
9269 this.emit( 'resize' );
9270 }
9271 }
9272
9273 return this;
9274 };
9275
9276 /**
9277 * @inheritdoc
9278 */
9279 OO.ui.ActionWidget.prototype.setIcon = function () {
9280 // Mixin method
9281 OO.ui.IconElement.prototype.setIcon.apply( this, arguments );
9282 this.propagateResize();
9283
9284 return this;
9285 };
9286
9287 /**
9288 * @inheritdoc
9289 */
9290 OO.ui.ActionWidget.prototype.setLabel = function () {
9291 // Mixin method
9292 OO.ui.LabelElement.prototype.setLabel.apply( this, arguments );
9293 this.propagateResize();
9294
9295 return this;
9296 };
9297
9298 /**
9299 * @inheritdoc
9300 */
9301 OO.ui.ActionWidget.prototype.setFlags = function () {
9302 // Mixin method
9303 OO.ui.FlaggedElement.prototype.setFlags.apply( this, arguments );
9304 this.propagateResize();
9305
9306 return this;
9307 };
9308
9309 /**
9310 * @inheritdoc
9311 */
9312 OO.ui.ActionWidget.prototype.clearFlags = function () {
9313 // Mixin method
9314 OO.ui.FlaggedElement.prototype.clearFlags.apply( this, arguments );
9315 this.propagateResize();
9316
9317 return this;
9318 };
9319
9320 /**
9321 * Toggle visibility of button.
9322 *
9323 * @param {boolean} [show] Show button, omit to toggle visibility
9324 * @chainable
9325 */
9326 OO.ui.ActionWidget.prototype.toggle = function () {
9327 // Parent method
9328 OO.ui.ActionWidget.super.prototype.toggle.apply( this, arguments );
9329 this.propagateResize();
9330
9331 return this;
9332 };
9333
9334 /**
9335 * Button that shows and hides a popup.
9336 *
9337 * @class
9338 * @extends OO.ui.ButtonWidget
9339 * @mixins OO.ui.PopupElement
9340 *
9341 * @constructor
9342 * @param {Object} [config] Configuration options
9343 */
9344 OO.ui.PopupButtonWidget = function OoUiPopupButtonWidget( config ) {
9345 // Parent constructor
9346 OO.ui.PopupButtonWidget.super.call( this, config );
9347
9348 // Mixin constructors
9349 OO.ui.PopupElement.call( this, config );
9350
9351 // Initialization
9352 this.$element
9353 .addClass( 'oo-ui-popupButtonWidget' )
9354 .append( this.popup.$element );
9355 };
9356
9357 /* Setup */
9358
9359 OO.inheritClass( OO.ui.PopupButtonWidget, OO.ui.ButtonWidget );
9360 OO.mixinClass( OO.ui.PopupButtonWidget, OO.ui.PopupElement );
9361
9362 /* Methods */
9363
9364 /**
9365 * Handles mouse click events.
9366 *
9367 * @param {jQuery.Event} e Mouse click event
9368 */
9369 OO.ui.PopupButtonWidget.prototype.onClick = function ( e ) {
9370 // Skip clicks within the popup
9371 if ( $.contains( this.popup.$element[0], e.target ) ) {
9372 return;
9373 }
9374
9375 if ( !this.isDisabled() ) {
9376 this.popup.toggle();
9377 // Parent method
9378 OO.ui.PopupButtonWidget.super.prototype.onClick.call( this );
9379 }
9380 return false;
9381 };
9382
9383 /**
9384 * Button that toggles on and off.
9385 *
9386 * @class
9387 * @extends OO.ui.ButtonWidget
9388 * @mixins OO.ui.ToggleWidget
9389 *
9390 * @constructor
9391 * @param {Object} [config] Configuration options
9392 * @cfg {boolean} [value=false] Initial value
9393 */
9394 OO.ui.ToggleButtonWidget = function OoUiToggleButtonWidget( config ) {
9395 // Configuration initialization
9396 config = config || {};
9397
9398 // Parent constructor
9399 OO.ui.ToggleButtonWidget.super.call( this, config );
9400
9401 // Mixin constructors
9402 OO.ui.ToggleWidget.call( this, config );
9403
9404 // Initialization
9405 this.$element.addClass( 'oo-ui-toggleButtonWidget' );
9406 };
9407
9408 /* Setup */
9409
9410 OO.inheritClass( OO.ui.ToggleButtonWidget, OO.ui.ButtonWidget );
9411 OO.mixinClass( OO.ui.ToggleButtonWidget, OO.ui.ToggleWidget );
9412
9413 /* Methods */
9414
9415 /**
9416 * @inheritdoc
9417 */
9418 OO.ui.ToggleButtonWidget.prototype.onClick = function () {
9419 if ( !this.isDisabled() ) {
9420 this.setValue( !this.value );
9421 }
9422
9423 // Parent method
9424 return OO.ui.ToggleButtonWidget.super.prototype.onClick.call( this );
9425 };
9426
9427 /**
9428 * @inheritdoc
9429 */
9430 OO.ui.ToggleButtonWidget.prototype.setValue = function ( value ) {
9431 value = !!value;
9432 if ( value !== this.value ) {
9433 this.setActive( value );
9434 }
9435
9436 // Parent method (from mixin)
9437 OO.ui.ToggleWidget.prototype.setValue.call( this, value );
9438
9439 return this;
9440 };
9441
9442 /**
9443 * Dropdown menu of options.
9444 *
9445 * Dropdown menus provide a control for accessing a menu and compose a menu within the widget, which
9446 * can be accessed using the #getMenu method.
9447 *
9448 * Use with OO.ui.MenuOptionWidget.
9449 *
9450 * @class
9451 * @extends OO.ui.Widget
9452 * @mixins OO.ui.IconElement
9453 * @mixins OO.ui.IndicatorElement
9454 * @mixins OO.ui.LabelElement
9455 * @mixins OO.ui.TitledElement
9456 *
9457 * @constructor
9458 * @param {Object} [config] Configuration options
9459 * @cfg {Object} [menu] Configuration options to pass to menu widget
9460 */
9461 OO.ui.DropdownWidget = function OoUiDropdownWidget( config ) {
9462 // Configuration initialization
9463 config = $.extend( { indicator: 'down' }, config );
9464
9465 // Parent constructor
9466 OO.ui.DropdownWidget.super.call( this, config );
9467
9468 // Mixin constructors
9469 OO.ui.IconElement.call( this, config );
9470 OO.ui.IndicatorElement.call( this, config );
9471 OO.ui.LabelElement.call( this, config );
9472 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$label } ) );
9473
9474 // Properties
9475 this.menu = new OO.ui.MenuSelectWidget( $.extend( { $: this.$, widget: this }, config.menu ) );
9476 this.$handle = this.$( '<span>' );
9477
9478 // Events
9479 this.$element.on( { click: this.onClick.bind( this ) } );
9480 this.menu.connect( this, { select: 'onMenuSelect' } );
9481
9482 // Initialization
9483 this.$handle
9484 .addClass( 'oo-ui-dropdownWidget-handle' )
9485 .append( this.$icon, this.$label, this.$indicator );
9486 this.$element
9487 .addClass( 'oo-ui-dropdownWidget' )
9488 .append( this.$handle, this.menu.$element );
9489 };
9490
9491 /* Setup */
9492
9493 OO.inheritClass( OO.ui.DropdownWidget, OO.ui.Widget );
9494 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.IconElement );
9495 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.IndicatorElement );
9496 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.LabelElement );
9497 OO.mixinClass( OO.ui.DropdownWidget, OO.ui.TitledElement );
9498
9499 /* Methods */
9500
9501 /**
9502 * Get the menu.
9503 *
9504 * @return {OO.ui.MenuSelectWidget} Menu of widget
9505 */
9506 OO.ui.DropdownWidget.prototype.getMenu = function () {
9507 return this.menu;
9508 };
9509
9510 /**
9511 * Handles menu select events.
9512 *
9513 * @param {OO.ui.MenuOptionWidget} item Selected menu item
9514 */
9515 OO.ui.DropdownWidget.prototype.onMenuSelect = function ( item ) {
9516 var selectedLabel;
9517
9518 if ( !item ) {
9519 return;
9520 }
9521
9522 selectedLabel = item.getLabel();
9523
9524 // If the label is a DOM element, clone it, because setLabel will append() it
9525 if ( selectedLabel instanceof jQuery ) {
9526 selectedLabel = selectedLabel.clone();
9527 }
9528
9529 this.setLabel( selectedLabel );
9530 };
9531
9532 /**
9533 * Handles mouse click events.
9534 *
9535 * @param {jQuery.Event} e Mouse click event
9536 */
9537 OO.ui.DropdownWidget.prototype.onClick = function ( e ) {
9538 // Skip clicks within the menu
9539 if ( $.contains( this.menu.$element[0], e.target ) ) {
9540 return;
9541 }
9542
9543 if ( !this.isDisabled() ) {
9544 if ( this.menu.isVisible() ) {
9545 this.menu.toggle( false );
9546 } else {
9547 this.menu.toggle( true );
9548 }
9549 }
9550 return false;
9551 };
9552
9553 /**
9554 * Icon widget.
9555 *
9556 * See OO.ui.IconElement for more information.
9557 *
9558 * @class
9559 * @extends OO.ui.Widget
9560 * @mixins OO.ui.IconElement
9561 * @mixins OO.ui.TitledElement
9562 *
9563 * @constructor
9564 * @param {Object} [config] Configuration options
9565 */
9566 OO.ui.IconWidget = function OoUiIconWidget( config ) {
9567 // Configuration initialization
9568 config = config || {};
9569
9570 // Parent constructor
9571 OO.ui.IconWidget.super.call( this, config );
9572
9573 // Mixin constructors
9574 OO.ui.IconElement.call( this, $.extend( {}, config, { $icon: this.$element } ) );
9575 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
9576
9577 // Initialization
9578 this.$element.addClass( 'oo-ui-iconWidget' );
9579 };
9580
9581 /* Setup */
9582
9583 OO.inheritClass( OO.ui.IconWidget, OO.ui.Widget );
9584 OO.mixinClass( OO.ui.IconWidget, OO.ui.IconElement );
9585 OO.mixinClass( OO.ui.IconWidget, OO.ui.TitledElement );
9586
9587 /* Static Properties */
9588
9589 OO.ui.IconWidget.static.tagName = 'span';
9590
9591 /**
9592 * Indicator widget.
9593 *
9594 * See OO.ui.IndicatorElement for more information.
9595 *
9596 * @class
9597 * @extends OO.ui.Widget
9598 * @mixins OO.ui.IndicatorElement
9599 * @mixins OO.ui.TitledElement
9600 *
9601 * @constructor
9602 * @param {Object} [config] Configuration options
9603 */
9604 OO.ui.IndicatorWidget = function OoUiIndicatorWidget( config ) {
9605 // Configuration initialization
9606 config = config || {};
9607
9608 // Parent constructor
9609 OO.ui.IndicatorWidget.super.call( this, config );
9610
9611 // Mixin constructors
9612 OO.ui.IndicatorElement.call( this, $.extend( {}, config, { $indicator: this.$element } ) );
9613 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$element } ) );
9614
9615 // Initialization
9616 this.$element.addClass( 'oo-ui-indicatorWidget' );
9617 };
9618
9619 /* Setup */
9620
9621 OO.inheritClass( OO.ui.IndicatorWidget, OO.ui.Widget );
9622 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.IndicatorElement );
9623 OO.mixinClass( OO.ui.IndicatorWidget, OO.ui.TitledElement );
9624
9625 /* Static Properties */
9626
9627 OO.ui.IndicatorWidget.static.tagName = 'span';
9628
9629 /**
9630 * Base class for input widgets.
9631 *
9632 * @abstract
9633 * @class
9634 * @extends OO.ui.Widget
9635 * @mixins OO.ui.FlaggedElement
9636 *
9637 * @constructor
9638 * @param {Object} [config] Configuration options
9639 * @cfg {string} [name=''] HTML input name
9640 * @cfg {string} [value=''] Input value
9641 * @cfg {Function} [inputFilter] Filter function to apply to the input. Takes a string argument and returns a string.
9642 */
9643 OO.ui.InputWidget = function OoUiInputWidget( config ) {
9644 // Configuration initialization
9645 config = config || {};
9646
9647 // Parent constructor
9648 OO.ui.InputWidget.super.call( this, config );
9649
9650 // Mixin constructors
9651 OO.ui.FlaggedElement.call( this, config );
9652
9653 // Properties
9654 this.$input = this.getInputElement( config );
9655 this.value = '';
9656 this.inputFilter = config.inputFilter;
9657
9658 // Events
9659 this.$input.on( 'keydown mouseup cut paste change input select', this.onEdit.bind( this ) );
9660
9661 // Initialization
9662 this.$input
9663 .attr( 'name', config.name )
9664 .prop( 'disabled', this.isDisabled() );
9665 this.$element.addClass( 'oo-ui-inputWidget' ).append( this.$input, $( '<span>' ) );
9666 this.setValue( config.value );
9667 };
9668
9669 /* Setup */
9670
9671 OO.inheritClass( OO.ui.InputWidget, OO.ui.Widget );
9672 OO.mixinClass( OO.ui.InputWidget, OO.ui.FlaggedElement );
9673
9674 /* Events */
9675
9676 /**
9677 * @event change
9678 * @param {string} value
9679 */
9680
9681 /* Methods */
9682
9683 /**
9684 * Get input element.
9685 *
9686 * @private
9687 * @param {Object} [config] Configuration options
9688 * @return {jQuery} Input element
9689 */
9690 OO.ui.InputWidget.prototype.getInputElement = function () {
9691 return this.$( '<input>' );
9692 };
9693
9694 /**
9695 * Handle potentially value-changing events.
9696 *
9697 * @param {jQuery.Event} e Key down, mouse up, cut, paste, change, input, or select event
9698 */
9699 OO.ui.InputWidget.prototype.onEdit = function () {
9700 var widget = this;
9701 if ( !this.isDisabled() ) {
9702 // Allow the stack to clear so the value will be updated
9703 setTimeout( function () {
9704 widget.setValue( widget.$input.val() );
9705 } );
9706 }
9707 };
9708
9709 /**
9710 * Get the value of the input.
9711 *
9712 * @return {string} Input value
9713 */
9714 OO.ui.InputWidget.prototype.getValue = function () {
9715 return this.value;
9716 };
9717
9718 /**
9719 * Sets the direction of the current input, either RTL or LTR
9720 *
9721 * @param {boolean} isRTL
9722 */
9723 OO.ui.InputWidget.prototype.setRTL = function ( isRTL ) {
9724 if ( isRTL ) {
9725 this.$input.removeClass( 'oo-ui-ltr' );
9726 this.$input.addClass( 'oo-ui-rtl' );
9727 } else {
9728 this.$input.removeClass( 'oo-ui-rtl' );
9729 this.$input.addClass( 'oo-ui-ltr' );
9730 }
9731 };
9732
9733 /**
9734 * Set the value of the input.
9735 *
9736 * @param {string} value New value
9737 * @fires change
9738 * @chainable
9739 */
9740 OO.ui.InputWidget.prototype.setValue = function ( value ) {
9741 value = this.cleanUpValue( value );
9742 // Update the DOM if it has changed. Note that with cleanUpValue, it
9743 // is possible for the DOM value to change without this.value changing.
9744 if ( this.$input.val() !== value ) {
9745 this.$input.val( value );
9746 }
9747 if ( this.value !== value ) {
9748 this.value = value;
9749 this.emit( 'change', this.value );
9750 }
9751 return this;
9752 };
9753
9754 /**
9755 * Clean up incoming value.
9756 *
9757 * Ensures value is a string, and converts undefined and null to empty string.
9758 *
9759 * @private
9760 * @param {string} value Original value
9761 * @return {string} Cleaned up value
9762 */
9763 OO.ui.InputWidget.prototype.cleanUpValue = function ( value ) {
9764 if ( value === undefined || value === null ) {
9765 return '';
9766 } else if ( this.inputFilter ) {
9767 return this.inputFilter( String( value ) );
9768 } else {
9769 return String( value );
9770 }
9771 };
9772
9773 /**
9774 * Simulate the behavior of clicking on a label bound to this input.
9775 */
9776 OO.ui.InputWidget.prototype.simulateLabelClick = function () {
9777 if ( !this.isDisabled() ) {
9778 if ( this.$input.is( ':checkbox,:radio' ) ) {
9779 this.$input.click();
9780 } else if ( this.$input.is( ':input' ) ) {
9781 this.$input[0].focus();
9782 }
9783 }
9784 };
9785
9786 /**
9787 * @inheritdoc
9788 */
9789 OO.ui.InputWidget.prototype.setDisabled = function ( state ) {
9790 OO.ui.InputWidget.super.prototype.setDisabled.call( this, state );
9791 if ( this.$input ) {
9792 this.$input.prop( 'disabled', this.isDisabled() );
9793 }
9794 return this;
9795 };
9796
9797 /**
9798 * Focus the input.
9799 *
9800 * @chainable
9801 */
9802 OO.ui.InputWidget.prototype.focus = function () {
9803 this.$input[0].focus();
9804 return this;
9805 };
9806
9807 /**
9808 * Blur the input.
9809 *
9810 * @chainable
9811 */
9812 OO.ui.InputWidget.prototype.blur = function () {
9813 this.$input[0].blur();
9814 return this;
9815 };
9816
9817 /**
9818 * A button that is an input widget. Intended to be used within a OO.ui.FormLayout.
9819 *
9820 * @class
9821 * @extends OO.ui.InputWidget
9822 * @mixins OO.ui.ButtonElement
9823 * @mixins OO.ui.IconElement
9824 * @mixins OO.ui.IndicatorElement
9825 * @mixins OO.ui.LabelElement
9826 * @mixins OO.ui.TitledElement
9827 * @mixins OO.ui.FlaggedElement
9828 *
9829 * @constructor
9830 * @param {Object} [config] Configuration options
9831 * @cfg {string} [type='button'] HTML tag `type` attribute, may be 'button', 'submit' or 'reset'
9832 * @cfg {boolean} [useInputTag=false] Whether to use `<input/>` rather than `<button/>`. Only useful
9833 * if you need IE 6 support in a form with multiple buttons. If you use this option, icons and
9834 * indicators will not be displayed, it won't be possible to have a non-plaintext label, and it
9835 * won't be possible to set a value (which will internally become identical to the label).
9836 */
9837 OO.ui.ButtonInputWidget = function OoUiButtonInputWidget( config ) {
9838 // Configuration initialization
9839 config = $.extend( { type: 'button', useInputTag: false }, config );
9840
9841 // Properties (must be set before parent constructor, which calls #setValue)
9842 this.useInputTag = config.useInputTag;
9843
9844 // Parent constructor
9845 OO.ui.ButtonInputWidget.super.call( this, config );
9846
9847 // Mixin constructors
9848 OO.ui.ButtonElement.call( this, $.extend( {}, config, { $button: this.$input } ) );
9849 OO.ui.IconElement.call( this, config );
9850 OO.ui.IndicatorElement.call( this, config );
9851 OO.ui.LabelElement.call( this, config );
9852 OO.ui.TitledElement.call( this, $.extend( {}, config, { $titled: this.$input } ) );
9853 OO.ui.FlaggedElement.call( this, config );
9854
9855 // Events
9856 this.$input.on( {
9857 click: this.onClick.bind( this ),
9858 keypress: this.onKeyPress.bind( this )
9859 } );
9860
9861 // Initialization
9862 if ( !config.useInputTag ) {
9863 this.$input.append( this.$icon, this.$label, this.$indicator );
9864 }
9865 this.$element.addClass( 'oo-ui-buttonInputWidget' );
9866 };
9867
9868 /* Setup */
9869
9870 OO.inheritClass( OO.ui.ButtonInputWidget, OO.ui.InputWidget );
9871 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.ButtonElement );
9872 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.IconElement );
9873 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.IndicatorElement );
9874 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.LabelElement );
9875 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.TitledElement );
9876 OO.mixinClass( OO.ui.ButtonInputWidget, OO.ui.FlaggedElement );
9877
9878 /* Events */
9879
9880 /**
9881 * @event click
9882 */
9883
9884 /* Methods */
9885
9886 /**
9887 * Get input element.
9888 *
9889 * @private
9890 * @param {Object} [config] Configuration options
9891 * @return {jQuery} Input element
9892 */
9893 OO.ui.ButtonInputWidget.prototype.getInputElement = function ( config ) {
9894 // Configuration initialization
9895 config = config || {};
9896
9897 var html = '<' + ( config.useInputTag ? 'input' : 'button' ) + ' type="' + config.type + '">';
9898
9899 return this.$( html );
9900 };
9901
9902 /**
9903 * Set label value.
9904 *
9905 * Overridden to support setting the 'value' of `<input/>` elements.
9906 *
9907 * @param {jQuery|string|Function|null} label Label nodes; text; a function that returns nodes or
9908 * text; or null for no label
9909 * @chainable
9910 */
9911 OO.ui.ButtonInputWidget.prototype.setLabel = function ( label ) {
9912 OO.ui.LabelElement.prototype.setLabel.call( this, label );
9913
9914 if ( this.useInputTag ) {
9915 if ( typeof label === 'function' ) {
9916 label = OO.ui.resolveMsg( label );
9917 }
9918 if ( label instanceof jQuery ) {
9919 label = label.text();
9920 }
9921 if ( !label ) {
9922 label = '';
9923 }
9924 this.$input.val( label );
9925 }
9926
9927 return this;
9928 };
9929
9930 /**
9931 * Set the value of the input.
9932 *
9933 * Overridden to disable for `<input/>` elements, which have value identical to the label.
9934 *
9935 * @param {string} value New value
9936 * @chainable
9937 */
9938 OO.ui.ButtonInputWidget.prototype.setValue = function ( value ) {
9939 if ( !this.useInputTag ) {
9940 OO.ui.ButtonInputWidget.super.prototype.setValue.call( this, value );
9941 }
9942 return this;
9943 };
9944
9945 /**
9946 * Handles mouse click events.
9947 *
9948 * @param {jQuery.Event} e Mouse click event
9949 * @fires click
9950 */
9951 OO.ui.ButtonInputWidget.prototype.onClick = function () {
9952 if ( !this.isDisabled() ) {
9953 this.emit( 'click' );
9954 }
9955 return false;
9956 };
9957
9958 /**
9959 * Handles keypress events.
9960 *
9961 * @param {jQuery.Event} e Keypress event
9962 * @fires click
9963 */
9964 OO.ui.ButtonInputWidget.prototype.onKeyPress = function ( e ) {
9965 if ( !this.isDisabled() && ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) ) {
9966 this.emit( 'click' );
9967 }
9968 return false;
9969 };
9970
9971 /**
9972 * Checkbox input widget.
9973 *
9974 * @class
9975 * @extends OO.ui.InputWidget
9976 *
9977 * @constructor
9978 * @param {Object} [config] Configuration options
9979 * @cfg {boolean} [selected=false] Whether the checkbox is initially selected
9980 */
9981 OO.ui.CheckboxInputWidget = function OoUiCheckboxInputWidget( config ) {
9982 // Parent constructor
9983 OO.ui.CheckboxInputWidget.super.call( this, config );
9984
9985 // Initialization
9986 this.$element.addClass( 'oo-ui-checkboxInputWidget' );
9987 this.setSelected( config.selected !== undefined ? config.selected : false );
9988 };
9989
9990 /* Setup */
9991
9992 OO.inheritClass( OO.ui.CheckboxInputWidget, OO.ui.InputWidget );
9993
9994 /* Methods */
9995
9996 /**
9997 * Get input element.
9998 *
9999 * @private
10000 * @return {jQuery} Input element
10001 */
10002 OO.ui.CheckboxInputWidget.prototype.getInputElement = function () {
10003 return this.$( '<input type="checkbox" />' );
10004 };
10005
10006 /**
10007 * @inheritdoc
10008 */
10009 OO.ui.CheckboxInputWidget.prototype.onEdit = function () {
10010 var widget = this;
10011 if ( !this.isDisabled() ) {
10012 // Allow the stack to clear so the value will be updated
10013 setTimeout( function () {
10014 widget.setSelected( widget.$input.prop( 'checked' ) );
10015 } );
10016 }
10017 };
10018
10019 /**
10020 * Set selection state of this checkbox.
10021 *
10022 * @param {boolean} state Whether the checkbox is selected
10023 * @chainable
10024 */
10025 OO.ui.CheckboxInputWidget.prototype.setSelected = function ( state ) {
10026 state = !!state;
10027 if ( this.selected !== state ) {
10028 this.selected = state;
10029 this.$input.prop( 'checked', this.selected );
10030 this.emit( 'change', this.selected );
10031 }
10032 return this;
10033 };
10034
10035 /**
10036 * Check if this checkbox is selected.
10037 *
10038 * @return {boolean} Checkbox is selected
10039 */
10040 OO.ui.CheckboxInputWidget.prototype.isSelected = function () {
10041 return this.selected;
10042 };
10043
10044 /**
10045 * Radio input widget.
10046 *
10047 * Radio buttons only make sense as a set, and you probably want to use the OO.ui.RadioSelectWidget
10048 * class instead of using this class directly.
10049 *
10050 * @class
10051 * @extends OO.ui.InputWidget
10052 *
10053 * @constructor
10054 * @param {Object} [config] Configuration options
10055 * @cfg {boolean} [selected=false] Whether the radio button is initially selected
10056 */
10057 OO.ui.RadioInputWidget = function OoUiRadioInputWidget( config ) {
10058 // Parent constructor
10059 OO.ui.RadioInputWidget.super.call( this, config );
10060
10061 // Initialization
10062 this.$element.addClass( 'oo-ui-radioInputWidget' );
10063 this.setSelected( config.selected !== undefined ? config.selected : false );
10064 };
10065
10066 /* Setup */
10067
10068 OO.inheritClass( OO.ui.RadioInputWidget, OO.ui.InputWidget );
10069
10070 /* Methods */
10071
10072 /**
10073 * Get input element.
10074 *
10075 * @private
10076 * @return {jQuery} Input element
10077 */
10078 OO.ui.RadioInputWidget.prototype.getInputElement = function () {
10079 return this.$( '<input type="radio" />' );
10080 };
10081
10082 /**
10083 * @inheritdoc
10084 */
10085 OO.ui.RadioInputWidget.prototype.onEdit = function () {
10086 // RadioInputWidget doesn't track its state.
10087 };
10088
10089 /**
10090 * Set selection state of this radio button.
10091 *
10092 * @param {boolean} state Whether the button is selected
10093 * @chainable
10094 */
10095 OO.ui.RadioInputWidget.prototype.setSelected = function ( state ) {
10096 // RadioInputWidget doesn't track its state.
10097 this.$input.prop( 'checked', state );
10098 return this;
10099 };
10100
10101 /**
10102 * Check if this radio button is selected.
10103 *
10104 * @return {boolean} Radio is selected
10105 */
10106 OO.ui.RadioInputWidget.prototype.isSelected = function () {
10107 return this.$input.prop( 'checked' );
10108 };
10109
10110 /**
10111 * Input widget with a text field.
10112 *
10113 * @class
10114 * @extends OO.ui.InputWidget
10115 * @mixins OO.ui.IconElement
10116 * @mixins OO.ui.IndicatorElement
10117 * @mixins OO.ui.PendingElement
10118 *
10119 * @constructor
10120 * @param {Object} [config] Configuration options
10121 * @cfg {string} [type='text'] HTML tag `type` attribute
10122 * @cfg {string} [placeholder] Placeholder text
10123 * @cfg {boolean} [readOnly=false] Prevent changes
10124 * @cfg {boolean} [multiline=false] Allow multiple lines of text
10125 * @cfg {boolean} [autosize=false] Automatically resize to fit content
10126 * @cfg {boolean} [maxRows=10] Maximum number of rows to make visible when autosizing
10127 * @cfg {RegExp|string} [validate] Regular expression (or symbolic name referencing
10128 * one, see #static-validationPatterns)
10129 */
10130 OO.ui.TextInputWidget = function OoUiTextInputWidget( config ) {
10131 // Configuration initialization
10132 config = $.extend( { readOnly: false }, config );
10133
10134 // Parent constructor
10135 OO.ui.TextInputWidget.super.call( this, config );
10136
10137 // Mixin constructors
10138 OO.ui.IconElement.call( this, config );
10139 OO.ui.IndicatorElement.call( this, config );
10140 OO.ui.PendingElement.call( this, config );
10141
10142 // Properties
10143 this.readOnly = false;
10144 this.multiline = !!config.multiline;
10145 this.autosize = !!config.autosize;
10146 this.maxRows = config.maxRows !== undefined ? config.maxRows : 10;
10147 this.validate = null;
10148
10149 // Clone for resizing
10150 if ( this.autosize ) {
10151 this.$clone = this.$input
10152 .clone()
10153 .insertAfter( this.$input )
10154 .hide();
10155 }
10156
10157 this.setValidation( config.validate );
10158
10159 // Events
10160 this.$input.on( {
10161 keypress: this.onKeyPress.bind( this ),
10162 blur: this.setValidityFlag.bind( this )
10163 } );
10164 this.$element.on( 'DOMNodeInsertedIntoDocument', this.onElementAttach.bind( this ) );
10165 this.$icon.on( 'mousedown', this.onIconMouseDown.bind( this ) );
10166 this.$indicator.on( 'mousedown', this.onIndicatorMouseDown.bind( this ) );
10167
10168 // Initialization
10169 this.$element
10170 .addClass( 'oo-ui-textInputWidget' )
10171 .append( this.$icon, this.$indicator );
10172 this.setReadOnly( config.readOnly );
10173 if ( config.placeholder ) {
10174 this.$input.attr( 'placeholder', config.placeholder );
10175 }
10176 this.$element.attr( 'role', 'textbox' );
10177 };
10178
10179 /* Setup */
10180
10181 OO.inheritClass( OO.ui.TextInputWidget, OO.ui.InputWidget );
10182 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IconElement );
10183 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.IndicatorElement );
10184 OO.mixinClass( OO.ui.TextInputWidget, OO.ui.PendingElement );
10185
10186 /* Static properties */
10187
10188 OO.ui.TextInputWidget.static.validationPatterns = {
10189 'non-empty': /.+/,
10190 integer: /^\d+$/
10191 };
10192
10193 /* Events */
10194
10195 /**
10196 * User presses enter inside the text box.
10197 *
10198 * Not called if input is multiline.
10199 *
10200 * @event enter
10201 */
10202
10203 /**
10204 * User clicks the icon.
10205 *
10206 * @event icon
10207 */
10208
10209 /**
10210 * User clicks the indicator.
10211 *
10212 * @event indicator
10213 */
10214
10215 /* Methods */
10216
10217 /**
10218 * Handle icon mouse down events.
10219 *
10220 * @param {jQuery.Event} e Mouse down event
10221 * @fires icon
10222 */
10223 OO.ui.TextInputWidget.prototype.onIconMouseDown = function ( e ) {
10224 if ( e.which === 1 ) {
10225 this.$input[0].focus();
10226 this.emit( 'icon' );
10227 return false;
10228 }
10229 };
10230
10231 /**
10232 * Handle indicator mouse down events.
10233 *
10234 * @param {jQuery.Event} e Mouse down event
10235 * @fires indicator
10236 */
10237 OO.ui.TextInputWidget.prototype.onIndicatorMouseDown = function ( e ) {
10238 if ( e.which === 1 ) {
10239 this.$input[0].focus();
10240 this.emit( 'indicator' );
10241 return false;
10242 }
10243 };
10244
10245 /**
10246 * Handle key press events.
10247 *
10248 * @param {jQuery.Event} e Key press event
10249 * @fires enter If enter key is pressed and input is not multiline
10250 */
10251 OO.ui.TextInputWidget.prototype.onKeyPress = function ( e ) {
10252 if ( e.which === OO.ui.Keys.ENTER && !this.multiline ) {
10253 this.emit( 'enter', e );
10254 }
10255 };
10256
10257 /**
10258 * Handle element attach events.
10259 *
10260 * @param {jQuery.Event} e Element attach event
10261 */
10262 OO.ui.TextInputWidget.prototype.onElementAttach = function () {
10263 this.adjustSize();
10264 };
10265
10266 /**
10267 * @inheritdoc
10268 */
10269 OO.ui.TextInputWidget.prototype.onEdit = function () {
10270 this.adjustSize();
10271
10272 // Parent method
10273 return OO.ui.TextInputWidget.super.prototype.onEdit.call( this );
10274 };
10275
10276 /**
10277 * @inheritdoc
10278 */
10279 OO.ui.TextInputWidget.prototype.setValue = function ( value ) {
10280 // Parent method
10281 OO.ui.TextInputWidget.super.prototype.setValue.call( this, value );
10282
10283 this.setValidityFlag();
10284 this.adjustSize();
10285 return this;
10286 };
10287
10288 /**
10289 * Check if the widget is read-only.
10290 *
10291 * @return {boolean}
10292 */
10293 OO.ui.TextInputWidget.prototype.isReadOnly = function () {
10294 return this.readOnly;
10295 };
10296
10297 /**
10298 * Set the read-only state of the widget.
10299 *
10300 * This should probably change the widget's appearance and prevent it from being used.
10301 *
10302 * @param {boolean} state Make input read-only
10303 * @chainable
10304 */
10305 OO.ui.TextInputWidget.prototype.setReadOnly = function ( state ) {
10306 this.readOnly = !!state;
10307 this.$input.prop( 'readOnly', this.readOnly );
10308 return this;
10309 };
10310
10311 /**
10312 * Automatically adjust the size of the text input.
10313 *
10314 * This only affects multi-line inputs that are auto-sized.
10315 *
10316 * @chainable
10317 */
10318 OO.ui.TextInputWidget.prototype.adjustSize = function () {
10319 var scrollHeight, innerHeight, outerHeight, maxInnerHeight, measurementError, idealHeight;
10320
10321 if ( this.multiline && this.autosize && this.$input.val() !== this.valCache ) {
10322 this.$clone
10323 .val( this.$input.val() )
10324 .attr( 'rows', '' )
10325 // Set inline height property to 0 to measure scroll height
10326 .css( 'height', 0 );
10327
10328 this.$clone[0].style.display = 'block';
10329
10330 this.valCache = this.$input.val();
10331
10332 scrollHeight = this.$clone[0].scrollHeight;
10333
10334 // Remove inline height property to measure natural heights
10335 this.$clone.css( 'height', '' );
10336 innerHeight = this.$clone.innerHeight();
10337 outerHeight = this.$clone.outerHeight();
10338
10339 // Measure max rows height
10340 this.$clone
10341 .attr( 'rows', this.maxRows )
10342 .css( 'height', 'auto' )
10343 .val( '' );
10344 maxInnerHeight = this.$clone.innerHeight();
10345
10346 // Difference between reported innerHeight and scrollHeight with no scrollbars present
10347 // Equals 1 on Blink-based browsers and 0 everywhere else
10348 measurementError = maxInnerHeight - this.$clone[0].scrollHeight;
10349 idealHeight = Math.min( maxInnerHeight, scrollHeight + measurementError );
10350
10351 this.$clone[0].style.display = 'none';
10352
10353 // Only apply inline height when expansion beyond natural height is needed
10354 if ( idealHeight > innerHeight ) {
10355 // Use the difference between the inner and outer height as a buffer
10356 this.$input.css( 'height', idealHeight + ( outerHeight - innerHeight ) );
10357 } else {
10358 this.$input.css( 'height', '' );
10359 }
10360 }
10361 return this;
10362 };
10363
10364 /**
10365 * Get input element.
10366 *
10367 * @private
10368 * @param {Object} [config] Configuration options
10369 * @return {jQuery} Input element
10370 */
10371 OO.ui.TextInputWidget.prototype.getInputElement = function ( config ) {
10372 // Configuration initialization
10373 config = config || {};
10374
10375 var type = config.type || 'text';
10376
10377 return config.multiline ? this.$( '<textarea>' ) : this.$( '<input type="' + type + '" />' );
10378 };
10379
10380 /**
10381 * Check if input supports multiple lines.
10382 *
10383 * @return {boolean}
10384 */
10385 OO.ui.TextInputWidget.prototype.isMultiline = function () {
10386 return !!this.multiline;
10387 };
10388
10389 /**
10390 * Check if input automatically adjusts its size.
10391 *
10392 * @return {boolean}
10393 */
10394 OO.ui.TextInputWidget.prototype.isAutosizing = function () {
10395 return !!this.autosize;
10396 };
10397
10398 /**
10399 * Select the contents of the input.
10400 *
10401 * @chainable
10402 */
10403 OO.ui.TextInputWidget.prototype.select = function () {
10404 this.$input.select();
10405 return this;
10406 };
10407
10408 /**
10409 * Sets the validation pattern to use.
10410 * @param {RegExp|string|null} validate Regular expression (or symbolic name referencing
10411 * one, see #static-validationPatterns)
10412 */
10413 OO.ui.TextInputWidget.prototype.setValidation = function ( validate ) {
10414 if ( validate instanceof RegExp ) {
10415 this.validate = validate;
10416 } else {
10417 this.validate = this.constructor.static.validationPatterns[validate] || /.*/;
10418 }
10419 };
10420
10421 /**
10422 * Sets the 'invalid' flag appropriately.
10423 */
10424 OO.ui.TextInputWidget.prototype.setValidityFlag = function () {
10425 var widget = this;
10426 this.isValid().done( function ( valid ) {
10427 widget.setFlags( { invalid: !valid } );
10428 } );
10429 };
10430
10431 /**
10432 * Returns whether or not the current value is considered valid, according to the
10433 * supplied validation pattern.
10434 *
10435 * @return {jQuery.Deferred}
10436 */
10437 OO.ui.TextInputWidget.prototype.isValid = function () {
10438 return $.Deferred().resolve( !!this.getValue().match( this.validate ) ).promise();
10439 };
10440
10441 /**
10442 * Text input with a menu of optional values.
10443 *
10444 * @class
10445 * @extends OO.ui.Widget
10446 *
10447 * @constructor
10448 * @param {Object} [config] Configuration options
10449 * @cfg {Object} [menu] Configuration options to pass to menu widget
10450 * @cfg {Object} [input] Configuration options to pass to input widget
10451 * @cfg {jQuery} [$overlay] Overlay layer; defaults to relative positioning
10452 */
10453 OO.ui.ComboBoxWidget = function OoUiComboBoxWidget( config ) {
10454 // Configuration initialization
10455 config = config || {};
10456
10457 // Parent constructor
10458 OO.ui.ComboBoxWidget.super.call( this, config );
10459
10460 // Properties
10461 this.$overlay = config.$overlay || this.$element;
10462 this.input = new OO.ui.TextInputWidget( $.extend(
10463 { $: this.$, indicator: 'down', disabled: this.isDisabled() },
10464 config.input
10465 ) );
10466 this.menu = new OO.ui.TextInputMenuSelectWidget( this.input, $.extend(
10467 {
10468 $: OO.ui.Element.static.getJQuery( this.$overlay ),
10469 widget: this,
10470 input: this.input,
10471 disabled: this.isDisabled()
10472 },
10473 config.menu
10474 ) );
10475
10476 // Events
10477 this.input.connect( this, {
10478 change: 'onInputChange',
10479 indicator: 'onInputIndicator',
10480 enter: 'onInputEnter'
10481 } );
10482 this.menu.connect( this, {
10483 choose: 'onMenuChoose',
10484 add: 'onMenuItemsChange',
10485 remove: 'onMenuItemsChange'
10486 } );
10487
10488 // Initialization
10489 this.$element.addClass( 'oo-ui-comboBoxWidget' ).append( this.input.$element );
10490 this.$overlay.append( this.menu.$element );
10491 this.onMenuItemsChange();
10492 };
10493
10494 /* Setup */
10495
10496 OO.inheritClass( OO.ui.ComboBoxWidget, OO.ui.Widget );
10497
10498 /* Methods */
10499
10500 /**
10501 * Get the combobox's menu.
10502 * @return {OO.ui.TextInputMenuSelectWidget} Menu widget
10503 */
10504 OO.ui.ComboBoxWidget.prototype.getMenu = function () {
10505 return this.menu;
10506 };
10507
10508 /**
10509 * Handle input change events.
10510 *
10511 * @param {string} value New value
10512 */
10513 OO.ui.ComboBoxWidget.prototype.onInputChange = function ( value ) {
10514 var match = this.menu.getItemFromData( value );
10515
10516 this.menu.selectItem( match );
10517
10518 if ( !this.isDisabled() ) {
10519 this.menu.toggle( true );
10520 }
10521 };
10522
10523 /**
10524 * Handle input indicator events.
10525 */
10526 OO.ui.ComboBoxWidget.prototype.onInputIndicator = function () {
10527 if ( !this.isDisabled() ) {
10528 this.menu.toggle();
10529 }
10530 };
10531
10532 /**
10533 * Handle input enter events.
10534 */
10535 OO.ui.ComboBoxWidget.prototype.onInputEnter = function () {
10536 if ( !this.isDisabled() ) {
10537 this.menu.toggle( false );
10538 }
10539 };
10540
10541 /**
10542 * Handle menu choose events.
10543 *
10544 * @param {OO.ui.OptionWidget} item Chosen item
10545 */
10546 OO.ui.ComboBoxWidget.prototype.onMenuChoose = function ( item ) {
10547 if ( item ) {
10548 this.input.setValue( item.getData() );
10549 }
10550 };
10551
10552 /**
10553 * Handle menu item change events.
10554 */
10555 OO.ui.ComboBoxWidget.prototype.onMenuItemsChange = function () {
10556 var match = this.menu.getItemFromData( this.input.getValue() );
10557 this.menu.selectItem( match );
10558 this.$element.toggleClass( 'oo-ui-comboBoxWidget-empty', this.menu.isEmpty() );
10559 };
10560
10561 /**
10562 * @inheritdoc
10563 */
10564 OO.ui.ComboBoxWidget.prototype.setDisabled = function ( disabled ) {
10565 // Parent method
10566 OO.ui.ComboBoxWidget.super.prototype.setDisabled.call( this, disabled );
10567
10568 if ( this.input ) {
10569 this.input.setDisabled( this.isDisabled() );
10570 }
10571 if ( this.menu ) {
10572 this.menu.setDisabled( this.isDisabled() );
10573 }
10574
10575 return this;
10576 };
10577
10578 /**
10579 * Label widget.
10580 *
10581 * @class
10582 * @extends OO.ui.Widget
10583 * @mixins OO.ui.LabelElement
10584 *
10585 * @constructor
10586 * @param {Object} [config] Configuration options
10587 * @cfg {OO.ui.InputWidget} [input] Input widget this label is for
10588 */
10589 OO.ui.LabelWidget = function OoUiLabelWidget( config ) {
10590 // Configuration initialization
10591 config = config || {};
10592
10593 // Parent constructor
10594 OO.ui.LabelWidget.super.call( this, config );
10595
10596 // Mixin constructors
10597 OO.ui.LabelElement.call( this, $.extend( {}, config, { $label: this.$element } ) );
10598 OO.ui.TitledElement.call( this, config );
10599
10600 // Properties
10601 this.input = config.input;
10602
10603 // Events
10604 if ( this.input instanceof OO.ui.InputWidget ) {
10605 this.$element.on( 'click', this.onClick.bind( this ) );
10606 }
10607
10608 // Initialization
10609 this.$element.addClass( 'oo-ui-labelWidget' );
10610 };
10611
10612 /* Setup */
10613
10614 OO.inheritClass( OO.ui.LabelWidget, OO.ui.Widget );
10615 OO.mixinClass( OO.ui.LabelWidget, OO.ui.LabelElement );
10616 OO.mixinClass( OO.ui.LabelWidget, OO.ui.TitledElement );
10617
10618 /* Static Properties */
10619
10620 OO.ui.LabelWidget.static.tagName = 'span';
10621
10622 /* Methods */
10623
10624 /**
10625 * Handles label mouse click events.
10626 *
10627 * @param {jQuery.Event} e Mouse click event
10628 */
10629 OO.ui.LabelWidget.prototype.onClick = function () {
10630 this.input.simulateLabelClick();
10631 return false;
10632 };
10633
10634 /**
10635 * Generic option widget for use with OO.ui.SelectWidget.
10636 *
10637 * @class
10638 * @extends OO.ui.Widget
10639 * @mixins OO.ui.LabelElement
10640 * @mixins OO.ui.FlaggedElement
10641 *
10642 * @constructor
10643 * @param {Object} [config] Configuration options
10644 */
10645 OO.ui.OptionWidget = function OoUiOptionWidget( config ) {
10646 // Configuration initialization
10647 config = config || {};
10648
10649 // Parent constructor
10650 OO.ui.OptionWidget.super.call( this, config );
10651
10652 // Mixin constructors
10653 OO.ui.ItemWidget.call( this );
10654 OO.ui.LabelElement.call( this, config );
10655 OO.ui.FlaggedElement.call( this, config );
10656
10657 // Properties
10658 this.selected = false;
10659 this.highlighted = false;
10660 this.pressed = false;
10661
10662 // Initialization
10663 this.$element
10664 .data( 'oo-ui-optionWidget', this )
10665 .attr( 'role', 'option' )
10666 .addClass( 'oo-ui-optionWidget' )
10667 .append( this.$label );
10668 };
10669
10670 /* Setup */
10671
10672 OO.inheritClass( OO.ui.OptionWidget, OO.ui.Widget );
10673 OO.mixinClass( OO.ui.OptionWidget, OO.ui.ItemWidget );
10674 OO.mixinClass( OO.ui.OptionWidget, OO.ui.LabelElement );
10675 OO.mixinClass( OO.ui.OptionWidget, OO.ui.FlaggedElement );
10676
10677 /* Static Properties */
10678
10679 OO.ui.OptionWidget.static.selectable = true;
10680
10681 OO.ui.OptionWidget.static.highlightable = true;
10682
10683 OO.ui.OptionWidget.static.pressable = true;
10684
10685 OO.ui.OptionWidget.static.scrollIntoViewOnSelect = false;
10686
10687 /* Methods */
10688
10689 /**
10690 * Check if option can be selected.
10691 *
10692 * @return {boolean} Item is selectable
10693 */
10694 OO.ui.OptionWidget.prototype.isSelectable = function () {
10695 return this.constructor.static.selectable && !this.isDisabled();
10696 };
10697
10698 /**
10699 * Check if option can be highlighted.
10700 *
10701 * @return {boolean} Item is highlightable
10702 */
10703 OO.ui.OptionWidget.prototype.isHighlightable = function () {
10704 return this.constructor.static.highlightable && !this.isDisabled();
10705 };
10706
10707 /**
10708 * Check if option can be pressed.
10709 *
10710 * @return {boolean} Item is pressable
10711 */
10712 OO.ui.OptionWidget.prototype.isPressable = function () {
10713 return this.constructor.static.pressable && !this.isDisabled();
10714 };
10715
10716 /**
10717 * Check if option is selected.
10718 *
10719 * @return {boolean} Item is selected
10720 */
10721 OO.ui.OptionWidget.prototype.isSelected = function () {
10722 return this.selected;
10723 };
10724
10725 /**
10726 * Check if option is highlighted.
10727 *
10728 * @return {boolean} Item is highlighted
10729 */
10730 OO.ui.OptionWidget.prototype.isHighlighted = function () {
10731 return this.highlighted;
10732 };
10733
10734 /**
10735 * Check if option is pressed.
10736 *
10737 * @return {boolean} Item is pressed
10738 */
10739 OO.ui.OptionWidget.prototype.isPressed = function () {
10740 return this.pressed;
10741 };
10742
10743 /**
10744 * Set selected state.
10745 *
10746 * @param {boolean} [state=false] Select option
10747 * @chainable
10748 */
10749 OO.ui.OptionWidget.prototype.setSelected = function ( state ) {
10750 if ( this.constructor.static.selectable ) {
10751 this.selected = !!state;
10752 this.$element.toggleClass( 'oo-ui-optionWidget-selected', state );
10753 if ( state && this.constructor.static.scrollIntoViewOnSelect ) {
10754 this.scrollElementIntoView();
10755 }
10756 this.updateThemeClasses();
10757 }
10758 return this;
10759 };
10760
10761 /**
10762 * Set highlighted state.
10763 *
10764 * @param {boolean} [state=false] Highlight option
10765 * @chainable
10766 */
10767 OO.ui.OptionWidget.prototype.setHighlighted = function ( state ) {
10768 if ( this.constructor.static.highlightable ) {
10769 this.highlighted = !!state;
10770 this.$element.toggleClass( 'oo-ui-optionWidget-highlighted', state );
10771 this.updateThemeClasses();
10772 }
10773 return this;
10774 };
10775
10776 /**
10777 * Set pressed state.
10778 *
10779 * @param {boolean} [state=false] Press option
10780 * @chainable
10781 */
10782 OO.ui.OptionWidget.prototype.setPressed = function ( state ) {
10783 if ( this.constructor.static.pressable ) {
10784 this.pressed = !!state;
10785 this.$element.toggleClass( 'oo-ui-optionWidget-pressed', state );
10786 this.updateThemeClasses();
10787 }
10788 return this;
10789 };
10790
10791 /**
10792 * Make the option's highlight flash.
10793 *
10794 * While flashing, the visual style of the pressed state is removed if present.
10795 *
10796 * @return {jQuery.Promise} Promise resolved when flashing is done
10797 */
10798 OO.ui.OptionWidget.prototype.flash = function () {
10799 var widget = this,
10800 $element = this.$element,
10801 deferred = $.Deferred();
10802
10803 if ( !this.isDisabled() && this.constructor.static.pressable ) {
10804 $element.removeClass( 'oo-ui-optionWidget-highlighted oo-ui-optionWidget-pressed' );
10805 setTimeout( function () {
10806 // Restore original classes
10807 $element
10808 .toggleClass( 'oo-ui-optionWidget-highlighted', widget.highlighted )
10809 .toggleClass( 'oo-ui-optionWidget-pressed', widget.pressed );
10810
10811 setTimeout( function () {
10812 deferred.resolve();
10813 }, 100 );
10814
10815 }, 100 );
10816 }
10817
10818 return deferred.promise();
10819 };
10820
10821 /**
10822 * Option widget with an option icon and indicator.
10823 *
10824 * Use together with OO.ui.SelectWidget.
10825 *
10826 * @class
10827 * @extends OO.ui.OptionWidget
10828 * @mixins OO.ui.IconElement
10829 * @mixins OO.ui.IndicatorElement
10830 *
10831 * @constructor
10832 * @param {Object} [config] Configuration options
10833 */
10834 OO.ui.DecoratedOptionWidget = function OoUiDecoratedOptionWidget( config ) {
10835 // Parent constructor
10836 OO.ui.DecoratedOptionWidget.super.call( this, config );
10837
10838 // Mixin constructors
10839 OO.ui.IconElement.call( this, config );
10840 OO.ui.IndicatorElement.call( this, config );
10841
10842 // Initialization
10843 this.$element
10844 .addClass( 'oo-ui-decoratedOptionWidget' )
10845 .prepend( this.$icon )
10846 .append( this.$indicator );
10847 };
10848
10849 /* Setup */
10850
10851 OO.inheritClass( OO.ui.DecoratedOptionWidget, OO.ui.OptionWidget );
10852 OO.mixinClass( OO.ui.OptionWidget, OO.ui.IconElement );
10853 OO.mixinClass( OO.ui.OptionWidget, OO.ui.IndicatorElement );
10854
10855 /**
10856 * Option widget that looks like a button.
10857 *
10858 * Use together with OO.ui.ButtonSelectWidget.
10859 *
10860 * @class
10861 * @extends OO.ui.DecoratedOptionWidget
10862 * @mixins OO.ui.ButtonElement
10863 *
10864 * @constructor
10865 * @param {Object} [config] Configuration options
10866 */
10867 OO.ui.ButtonOptionWidget = function OoUiButtonOptionWidget( config ) {
10868 // Parent constructor
10869 OO.ui.ButtonOptionWidget.super.call( this, config );
10870
10871 // Mixin constructors
10872 OO.ui.ButtonElement.call( this, config );
10873
10874 // Initialization
10875 this.$element.addClass( 'oo-ui-buttonOptionWidget' );
10876 this.$button.append( this.$element.contents() );
10877 this.$element.append( this.$button );
10878 };
10879
10880 /* Setup */
10881
10882 OO.inheritClass( OO.ui.ButtonOptionWidget, OO.ui.DecoratedOptionWidget );
10883 OO.mixinClass( OO.ui.ButtonOptionWidget, OO.ui.ButtonElement );
10884
10885 /* Static Properties */
10886
10887 // Allow button mouse down events to pass through so they can be handled by the parent select widget
10888 OO.ui.ButtonOptionWidget.static.cancelButtonMouseDownEvents = false;
10889
10890 /* Methods */
10891
10892 /**
10893 * @inheritdoc
10894 */
10895 OO.ui.ButtonOptionWidget.prototype.setSelected = function ( state ) {
10896 OO.ui.ButtonOptionWidget.super.prototype.setSelected.call( this, state );
10897
10898 if ( this.constructor.static.selectable ) {
10899 this.setActive( state );
10900 }
10901
10902 return this;
10903 };
10904
10905 /**
10906 * Option widget that looks like a radio button.
10907 *
10908 * Use together with OO.ui.RadioSelectWidget.
10909 *
10910 * @class
10911 * @extends OO.ui.OptionWidget
10912 *
10913 * @constructor
10914 * @param {Object} [config] Configuration options
10915 */
10916 OO.ui.RadioOptionWidget = function OoUiRadioOptionWidget( config ) {
10917 // Parent constructor
10918 OO.ui.RadioOptionWidget.super.call( this, config );
10919
10920 // Properties
10921 this.radio = new OO.ui.RadioInputWidget( { value: config.data } );
10922
10923 // Initialization
10924 this.$element
10925 .addClass( 'oo-ui-radioOptionWidget' )
10926 .prepend( this.radio.$element );
10927 };
10928
10929 /* Setup */
10930
10931 OO.inheritClass( OO.ui.RadioOptionWidget, OO.ui.OptionWidget );
10932
10933 /* Static Properties */
10934
10935 OO.ui.RadioOptionWidget.static.highlightable = false;
10936
10937 OO.ui.RadioOptionWidget.static.pressable = false;
10938
10939 /* Methods */
10940
10941 /**
10942 * @inheritdoc
10943 */
10944 OO.ui.RadioOptionWidget.prototype.setSelected = function ( state ) {
10945 OO.ui.RadioOptionWidget.super.prototype.setSelected.call( this, state );
10946
10947 this.radio.setSelected( state );
10948
10949 return this;
10950 };
10951
10952 /**
10953 * Item of an OO.ui.MenuSelectWidget.
10954 *
10955 * @class
10956 * @extends OO.ui.DecoratedOptionWidget
10957 *
10958 * @constructor
10959 * @param {Object} [config] Configuration options
10960 */
10961 OO.ui.MenuOptionWidget = function OoUiMenuOptionWidget( config ) {
10962 // Configuration initialization
10963 config = $.extend( { icon: 'check' }, config );
10964
10965 // Parent constructor
10966 OO.ui.MenuOptionWidget.super.call( this, config );
10967
10968 // Initialization
10969 this.$element
10970 .attr( 'role', 'menuitem' )
10971 .addClass( 'oo-ui-menuOptionWidget' );
10972 };
10973
10974 /* Setup */
10975
10976 OO.inheritClass( OO.ui.MenuOptionWidget, OO.ui.DecoratedOptionWidget );
10977
10978 /**
10979 * Section to group one or more items in a OO.ui.MenuSelectWidget.
10980 *
10981 * @class
10982 * @extends OO.ui.DecoratedOptionWidget
10983 *
10984 * @constructor
10985 * @param {Object} [config] Configuration options
10986 */
10987 OO.ui.MenuSectionOptionWidget = function OoUiMenuSectionOptionWidget( config ) {
10988 // Parent constructor
10989 OO.ui.MenuSectionOptionWidget.super.call( this, config );
10990
10991 // Initialization
10992 this.$element.addClass( 'oo-ui-menuSectionOptionWidget' );
10993 };
10994
10995 /* Setup */
10996
10997 OO.inheritClass( OO.ui.MenuSectionOptionWidget, OO.ui.DecoratedOptionWidget );
10998
10999 /* Static Properties */
11000
11001 OO.ui.MenuSectionOptionWidget.static.selectable = false;
11002
11003 OO.ui.MenuSectionOptionWidget.static.highlightable = false;
11004
11005 /**
11006 * Items for an OO.ui.OutlineSelectWidget.
11007 *
11008 * @class
11009 * @extends OO.ui.DecoratedOptionWidget
11010 *
11011 * @constructor
11012 * @param {Object} [config] Configuration options
11013 * @cfg {number} [level] Indentation level
11014 * @cfg {boolean} [movable] Allow modification from outline controls
11015 */
11016 OO.ui.OutlineOptionWidget = function OoUiOutlineOptionWidget( config ) {
11017 // Configuration initialization
11018 config = config || {};
11019
11020 // Parent constructor
11021 OO.ui.OutlineOptionWidget.super.call( this, config );
11022
11023 // Properties
11024 this.level = 0;
11025 this.movable = !!config.movable;
11026 this.removable = !!config.removable;
11027
11028 // Initialization
11029 this.$element.addClass( 'oo-ui-outlineOptionWidget' );
11030 this.setLevel( config.level );
11031 };
11032
11033 /* Setup */
11034
11035 OO.inheritClass( OO.ui.OutlineOptionWidget, OO.ui.DecoratedOptionWidget );
11036
11037 /* Static Properties */
11038
11039 OO.ui.OutlineOptionWidget.static.highlightable = false;
11040
11041 OO.ui.OutlineOptionWidget.static.scrollIntoViewOnSelect = true;
11042
11043 OO.ui.OutlineOptionWidget.static.levelClass = 'oo-ui-outlineOptionWidget-level-';
11044
11045 OO.ui.OutlineOptionWidget.static.levels = 3;
11046
11047 /* Methods */
11048
11049 /**
11050 * Check if item is movable.
11051 *
11052 * Movability is used by outline controls.
11053 *
11054 * @return {boolean} Item is movable
11055 */
11056 OO.ui.OutlineOptionWidget.prototype.isMovable = function () {
11057 return this.movable;
11058 };
11059
11060 /**
11061 * Check if item is removable.
11062 *
11063 * Removability is used by outline controls.
11064 *
11065 * @return {boolean} Item is removable
11066 */
11067 OO.ui.OutlineOptionWidget.prototype.isRemovable = function () {
11068 return this.removable;
11069 };
11070
11071 /**
11072 * Get indentation level.
11073 *
11074 * @return {number} Indentation level
11075 */
11076 OO.ui.OutlineOptionWidget.prototype.getLevel = function () {
11077 return this.level;
11078 };
11079
11080 /**
11081 * Set movability.
11082 *
11083 * Movability is used by outline controls.
11084 *
11085 * @param {boolean} movable Item is movable
11086 * @chainable
11087 */
11088 OO.ui.OutlineOptionWidget.prototype.setMovable = function ( movable ) {
11089 this.movable = !!movable;
11090 this.updateThemeClasses();
11091 return this;
11092 };
11093
11094 /**
11095 * Set removability.
11096 *
11097 * Removability is used by outline controls.
11098 *
11099 * @param {boolean} movable Item is removable
11100 * @chainable
11101 */
11102 OO.ui.OutlineOptionWidget.prototype.setRemovable = function ( removable ) {
11103 this.removable = !!removable;
11104 this.updateThemeClasses();
11105 return this;
11106 };
11107
11108 /**
11109 * Set indentation level.
11110 *
11111 * @param {number} [level=0] Indentation level, in the range of [0,#maxLevel]
11112 * @chainable
11113 */
11114 OO.ui.OutlineOptionWidget.prototype.setLevel = function ( level ) {
11115 var levels = this.constructor.static.levels,
11116 levelClass = this.constructor.static.levelClass,
11117 i = levels;
11118
11119 this.level = level ? Math.max( 0, Math.min( levels - 1, level ) ) : 0;
11120 while ( i-- ) {
11121 if ( this.level === i ) {
11122 this.$element.addClass( levelClass + i );
11123 } else {
11124 this.$element.removeClass( levelClass + i );
11125 }
11126 }
11127 this.updateThemeClasses();
11128
11129 return this;
11130 };
11131
11132 /**
11133 * Container for content that is overlaid and positioned absolutely.
11134 *
11135 * @class
11136 * @extends OO.ui.Widget
11137 * @mixins OO.ui.LabelElement
11138 *
11139 * @constructor
11140 * @param {Object} [config] Configuration options
11141 * @cfg {number} [width=320] Width of popup in pixels
11142 * @cfg {number} [height] Height of popup, omit to use automatic height
11143 * @cfg {boolean} [anchor=true] Show anchor pointing to origin of popup
11144 * @cfg {string} [align='center'] Alignment of popup to origin
11145 * @cfg {jQuery} [$container] Container to prevent popup from rendering outside of
11146 * @cfg {number} [containerPadding=10] How much padding to keep between popup and container
11147 * @cfg {jQuery} [$content] Content to append to the popup's body
11148 * @cfg {boolean} [autoClose=false] Popup auto-closes when it loses focus
11149 * @cfg {jQuery} [$autoCloseIgnore] Elements to not auto close when clicked
11150 * @cfg {boolean} [head] Show label and close button at the top
11151 * @cfg {boolean} [padded] Add padding to the body
11152 */
11153 OO.ui.PopupWidget = function OoUiPopupWidget( config ) {
11154 // Configuration initialization
11155 config = config || {};
11156
11157 // Parent constructor
11158 OO.ui.PopupWidget.super.call( this, config );
11159
11160 // Mixin constructors
11161 OO.ui.LabelElement.call( this, config );
11162 OO.ui.ClippableElement.call( this, config );
11163
11164 // Properties
11165 this.visible = false;
11166 this.$popup = this.$( '<div>' );
11167 this.$head = this.$( '<div>' );
11168 this.$body = this.$( '<div>' );
11169 this.$anchor = this.$( '<div>' );
11170 // If undefined, will be computed lazily in updateDimensions()
11171 this.$container = config.$container;
11172 this.containerPadding = config.containerPadding !== undefined ? config.containerPadding : 10;
11173 this.autoClose = !!config.autoClose;
11174 this.$autoCloseIgnore = config.$autoCloseIgnore;
11175 this.transitionTimeout = null;
11176 this.anchor = null;
11177 this.width = config.width !== undefined ? config.width : 320;
11178 this.height = config.height !== undefined ? config.height : null;
11179 this.align = config.align || 'center';
11180 this.closeButton = new OO.ui.ButtonWidget( { $: this.$, framed: false, icon: 'close' } );
11181 this.onMouseDownHandler = this.onMouseDown.bind( this );
11182
11183 // Events
11184 this.closeButton.connect( this, { click: 'onCloseButtonClick' } );
11185
11186 // Initialization
11187 this.toggleAnchor( config.anchor === undefined || config.anchor );
11188 this.$body.addClass( 'oo-ui-popupWidget-body' );
11189 this.$anchor.addClass( 'oo-ui-popupWidget-anchor' );
11190 this.$head
11191 .addClass( 'oo-ui-popupWidget-head' )
11192 .append( this.$label, this.closeButton.$element );
11193 if ( !config.head ) {
11194 this.$head.hide();
11195 }
11196 this.$popup
11197 .addClass( 'oo-ui-popupWidget-popup' )
11198 .append( this.$head, this.$body );
11199 this.$element
11200 .hide()
11201 .addClass( 'oo-ui-popupWidget' )
11202 .append( this.$popup, this.$anchor );
11203 // Move content, which was added to #$element by OO.ui.Widget, to the body
11204 if ( config.$content instanceof jQuery ) {
11205 this.$body.append( config.$content );
11206 }
11207 if ( config.padded ) {
11208 this.$body.addClass( 'oo-ui-popupWidget-body-padded' );
11209 }
11210 this.setClippableElement( this.$body );
11211 };
11212
11213 /* Setup */
11214
11215 OO.inheritClass( OO.ui.PopupWidget, OO.ui.Widget );
11216 OO.mixinClass( OO.ui.PopupWidget, OO.ui.LabelElement );
11217 OO.mixinClass( OO.ui.PopupWidget, OO.ui.ClippableElement );
11218
11219 /* Methods */
11220
11221 /**
11222 * Handles mouse down events.
11223 *
11224 * @param {jQuery.Event} e Mouse down event
11225 */
11226 OO.ui.PopupWidget.prototype.onMouseDown = function ( e ) {
11227 if (
11228 this.isVisible() &&
11229 !$.contains( this.$element[0], e.target ) &&
11230 ( !this.$autoCloseIgnore || !this.$autoCloseIgnore.has( e.target ).length )
11231 ) {
11232 this.toggle( false );
11233 }
11234 };
11235
11236 /**
11237 * Bind mouse down listener.
11238 */
11239 OO.ui.PopupWidget.prototype.bindMouseDownListener = function () {
11240 // Capture clicks outside popup
11241 this.getElementWindow().addEventListener( 'mousedown', this.onMouseDownHandler, true );
11242 };
11243
11244 /**
11245 * Handles close button click events.
11246 */
11247 OO.ui.PopupWidget.prototype.onCloseButtonClick = function () {
11248 if ( this.isVisible() ) {
11249 this.toggle( false );
11250 }
11251 };
11252
11253 /**
11254 * Unbind mouse down listener.
11255 */
11256 OO.ui.PopupWidget.prototype.unbindMouseDownListener = function () {
11257 this.getElementWindow().removeEventListener( 'mousedown', this.onMouseDownHandler, true );
11258 };
11259
11260 /**
11261 * Set whether to show a anchor.
11262 *
11263 * @param {boolean} [show] Show anchor, omit to toggle
11264 */
11265 OO.ui.PopupWidget.prototype.toggleAnchor = function ( show ) {
11266 show = show === undefined ? !this.anchored : !!show;
11267
11268 if ( this.anchored !== show ) {
11269 if ( show ) {
11270 this.$element.addClass( 'oo-ui-popupWidget-anchored' );
11271 } else {
11272 this.$element.removeClass( 'oo-ui-popupWidget-anchored' );
11273 }
11274 this.anchored = show;
11275 }
11276 };
11277
11278 /**
11279 * Check if showing a anchor.
11280 *
11281 * @return {boolean} anchor is visible
11282 */
11283 OO.ui.PopupWidget.prototype.hasAnchor = function () {
11284 return this.anchor;
11285 };
11286
11287 /**
11288 * @inheritdoc
11289 */
11290 OO.ui.PopupWidget.prototype.toggle = function ( show ) {
11291 show = show === undefined ? !this.isVisible() : !!show;
11292
11293 var change = show !== this.isVisible();
11294
11295 // Parent method
11296 OO.ui.PopupWidget.super.prototype.toggle.call( this, show );
11297
11298 if ( change ) {
11299 if ( show ) {
11300 if ( this.autoClose ) {
11301 this.bindMouseDownListener();
11302 }
11303 this.updateDimensions();
11304 this.toggleClipping( true );
11305 } else {
11306 this.toggleClipping( false );
11307 if ( this.autoClose ) {
11308 this.unbindMouseDownListener();
11309 }
11310 }
11311 }
11312
11313 return this;
11314 };
11315
11316 /**
11317 * Set the size of the popup.
11318 *
11319 * Changing the size may also change the popup's position depending on the alignment.
11320 *
11321 * @param {number} width Width
11322 * @param {number} height Height
11323 * @param {boolean} [transition=false] Use a smooth transition
11324 * @chainable
11325 */
11326 OO.ui.PopupWidget.prototype.setSize = function ( width, height, transition ) {
11327 this.width = width;
11328 this.height = height !== undefined ? height : null;
11329 if ( this.isVisible() ) {
11330 this.updateDimensions( transition );
11331 }
11332 };
11333
11334 /**
11335 * Update the size and position.
11336 *
11337 * Only use this to keep the popup properly anchored. Use #setSize to change the size, and this will
11338 * be called automatically.
11339 *
11340 * @param {boolean} [transition=false] Use a smooth transition
11341 * @chainable
11342 */
11343 OO.ui.PopupWidget.prototype.updateDimensions = function ( transition ) {
11344 var popupOffset, originOffset, containerLeft, containerWidth, containerRight,
11345 popupLeft, popupRight, overlapLeft, overlapRight, anchorWidth,
11346 widget = this;
11347
11348 if ( !this.$container ) {
11349 // Lazy-initialize $container if not specified in constructor
11350 this.$container = this.$( this.getClosestScrollableElementContainer() );
11351 }
11352
11353 // Set height and width before measuring things, since it might cause our measurements
11354 // to change (e.g. due to scrollbars appearing or disappearing)
11355 this.$popup.css( {
11356 width: this.width,
11357 height: this.height !== null ? this.height : 'auto'
11358 } );
11359
11360 // Compute initial popupOffset based on alignment
11361 popupOffset = this.width * ( { left: 0, center: -0.5, right: -1 } )[this.align];
11362
11363 // Figure out if this will cause the popup to go beyond the edge of the container
11364 originOffset = Math.round( this.$element.offset().left );
11365 containerLeft = Math.round( this.$container.offset().left );
11366 containerWidth = this.$container.innerWidth();
11367 containerRight = containerLeft + containerWidth;
11368 popupLeft = popupOffset - this.containerPadding;
11369 popupRight = popupOffset + this.containerPadding + this.width + this.containerPadding;
11370 overlapLeft = ( originOffset + popupLeft ) - containerLeft;
11371 overlapRight = containerRight - ( originOffset + popupRight );
11372
11373 // Adjust offset to make the popup not go beyond the edge, if needed
11374 if ( overlapRight < 0 ) {
11375 popupOffset += overlapRight;
11376 } else if ( overlapLeft < 0 ) {
11377 popupOffset -= overlapLeft;
11378 }
11379
11380 // Adjust offset to avoid anchor being rendered too close to the edge
11381 // $anchor.width() doesn't work with the pure CSS anchor (returns 0)
11382 // TODO: Find a measurement that works for CSS anchors and image anchors
11383 anchorWidth = this.$anchor[0].scrollWidth * 2;
11384 if ( popupOffset + this.width < anchorWidth ) {
11385 popupOffset = anchorWidth - this.width;
11386 } else if ( -popupOffset < anchorWidth ) {
11387 popupOffset = -anchorWidth;
11388 }
11389
11390 // Prevent transition from being interrupted
11391 clearTimeout( this.transitionTimeout );
11392 if ( transition ) {
11393 // Enable transition
11394 this.$element.addClass( 'oo-ui-popupWidget-transitioning' );
11395 }
11396
11397 // Position body relative to anchor
11398 this.$popup.css( 'margin-left', popupOffset );
11399
11400 if ( transition ) {
11401 // Prevent transitioning after transition is complete
11402 this.transitionTimeout = setTimeout( function () {
11403 widget.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
11404 }, 200 );
11405 } else {
11406 // Prevent transitioning immediately
11407 this.$element.removeClass( 'oo-ui-popupWidget-transitioning' );
11408 }
11409
11410 // Reevaluate clipping state since we've relocated and resized the popup
11411 this.clip();
11412
11413 return this;
11414 };
11415
11416 /**
11417 * Progress bar widget.
11418 *
11419 * @class
11420 * @extends OO.ui.Widget
11421 *
11422 * @constructor
11423 * @param {Object} [config] Configuration options
11424 * @cfg {number|boolean} [progress=false] Initial progress percent or false for indeterminate
11425 */
11426 OO.ui.ProgressBarWidget = function OoUiProgressBarWidget( config ) {
11427 // Configuration initialization
11428 config = config || {};
11429
11430 // Parent constructor
11431 OO.ui.ProgressBarWidget.super.call( this, config );
11432
11433 // Properties
11434 this.$bar = this.$( '<div>' );
11435 this.progress = null;
11436
11437 // Initialization
11438 this.setProgress( config.progress !== undefined ? config.progress : false );
11439 this.$bar.addClass( 'oo-ui-progressBarWidget-bar');
11440 this.$element
11441 .attr( {
11442 role: 'progressbar',
11443 'aria-valuemin': 0,
11444 'aria-valuemax': 100
11445 } )
11446 .addClass( 'oo-ui-progressBarWidget' )
11447 .append( this.$bar );
11448 };
11449
11450 /* Setup */
11451
11452 OO.inheritClass( OO.ui.ProgressBarWidget, OO.ui.Widget );
11453
11454 /* Static Properties */
11455
11456 OO.ui.ProgressBarWidget.static.tagName = 'div';
11457
11458 /* Methods */
11459
11460 /**
11461 * Get progress percent
11462 *
11463 * @return {number} Progress percent
11464 */
11465 OO.ui.ProgressBarWidget.prototype.getProgress = function () {
11466 return this.progress;
11467 };
11468
11469 /**
11470 * Set progress percent
11471 *
11472 * @param {number|boolean} progress Progress percent or false for indeterminate
11473 */
11474 OO.ui.ProgressBarWidget.prototype.setProgress = function ( progress ) {
11475 this.progress = progress;
11476
11477 if ( progress !== false ) {
11478 this.$bar.css( 'width', this.progress + '%' );
11479 this.$element.attr( 'aria-valuenow', this.progress );
11480 } else {
11481 this.$bar.css( 'width', '' );
11482 this.$element.removeAttr( 'aria-valuenow' );
11483 }
11484 this.$element.toggleClass( 'oo-ui-progressBarWidget-indeterminate', !progress );
11485 };
11486
11487 /**
11488 * Search widget.
11489 *
11490 * Search widgets combine a query input, placed above, and a results selection widget, placed below.
11491 * Results are cleared and populated each time the query is changed.
11492 *
11493 * @class
11494 * @extends OO.ui.Widget
11495 *
11496 * @constructor
11497 * @param {Object} [config] Configuration options
11498 * @cfg {string|jQuery} [placeholder] Placeholder text for query input
11499 * @cfg {string} [value] Initial query value
11500 */
11501 OO.ui.SearchWidget = function OoUiSearchWidget( config ) {
11502 // Configuration initialization
11503 config = config || {};
11504
11505 // Parent constructor
11506 OO.ui.SearchWidget.super.call( this, config );
11507
11508 // Properties
11509 this.query = new OO.ui.TextInputWidget( {
11510 $: this.$,
11511 icon: 'search',
11512 placeholder: config.placeholder,
11513 value: config.value
11514 } );
11515 this.results = new OO.ui.SelectWidget( { $: this.$ } );
11516 this.$query = this.$( '<div>' );
11517 this.$results = this.$( '<div>' );
11518
11519 // Events
11520 this.query.connect( this, {
11521 change: 'onQueryChange',
11522 enter: 'onQueryEnter'
11523 } );
11524 this.results.connect( this, {
11525 highlight: 'onResultsHighlight',
11526 select: 'onResultsSelect'
11527 } );
11528 this.query.$input.on( 'keydown', this.onQueryKeydown.bind( this ) );
11529
11530 // Initialization
11531 this.$query
11532 .addClass( 'oo-ui-searchWidget-query' )
11533 .append( this.query.$element );
11534 this.$results
11535 .addClass( 'oo-ui-searchWidget-results' )
11536 .append( this.results.$element );
11537 this.$element
11538 .addClass( 'oo-ui-searchWidget' )
11539 .append( this.$results, this.$query );
11540 };
11541
11542 /* Setup */
11543
11544 OO.inheritClass( OO.ui.SearchWidget, OO.ui.Widget );
11545
11546 /* Events */
11547
11548 /**
11549 * @event highlight
11550 * @param {Object|null} item Item data or null if no item is highlighted
11551 */
11552
11553 /**
11554 * @event select
11555 * @param {Object|null} item Item data or null if no item is selected
11556 */
11557
11558 /* Methods */
11559
11560 /**
11561 * Handle query key down events.
11562 *
11563 * @param {jQuery.Event} e Key down event
11564 */
11565 OO.ui.SearchWidget.prototype.onQueryKeydown = function ( e ) {
11566 var highlightedItem, nextItem,
11567 dir = e.which === OO.ui.Keys.DOWN ? 1 : ( e.which === OO.ui.Keys.UP ? -1 : 0 );
11568
11569 if ( dir ) {
11570 highlightedItem = this.results.getHighlightedItem();
11571 if ( !highlightedItem ) {
11572 highlightedItem = this.results.getSelectedItem();
11573 }
11574 nextItem = this.results.getRelativeSelectableItem( highlightedItem, dir );
11575 this.results.highlightItem( nextItem );
11576 nextItem.scrollElementIntoView();
11577 }
11578 };
11579
11580 /**
11581 * Handle select widget select events.
11582 *
11583 * Clears existing results. Subclasses should repopulate items according to new query.
11584 *
11585 * @param {string} value New value
11586 */
11587 OO.ui.SearchWidget.prototype.onQueryChange = function () {
11588 // Reset
11589 this.results.clearItems();
11590 };
11591
11592 /**
11593 * Handle select widget enter key events.
11594 *
11595 * Selects highlighted item.
11596 *
11597 * @param {string} value New value
11598 */
11599 OO.ui.SearchWidget.prototype.onQueryEnter = function () {
11600 // Reset
11601 this.results.selectItem( this.results.getHighlightedItem() );
11602 };
11603
11604 /**
11605 * Handle select widget highlight events.
11606 *
11607 * @param {OO.ui.OptionWidget} item Highlighted item
11608 * @fires highlight
11609 */
11610 OO.ui.SearchWidget.prototype.onResultsHighlight = function ( item ) {
11611 this.emit( 'highlight', item ? item.getData() : null );
11612 };
11613
11614 /**
11615 * Handle select widget select events.
11616 *
11617 * @param {OO.ui.OptionWidget} item Selected item
11618 * @fires select
11619 */
11620 OO.ui.SearchWidget.prototype.onResultsSelect = function ( item ) {
11621 this.emit( 'select', item ? item.getData() : null );
11622 };
11623
11624 /**
11625 * Get the query input.
11626 *
11627 * @return {OO.ui.TextInputWidget} Query input
11628 */
11629 OO.ui.SearchWidget.prototype.getQuery = function () {
11630 return this.query;
11631 };
11632
11633 /**
11634 * Get the results list.
11635 *
11636 * @return {OO.ui.SelectWidget} Select list
11637 */
11638 OO.ui.SearchWidget.prototype.getResults = function () {
11639 return this.results;
11640 };
11641
11642 /**
11643 * Generic selection of options.
11644 *
11645 * Items can contain any rendering. Any widget that provides options, from which the user must
11646 * choose one, should be built on this class.
11647 *
11648 * Use together with OO.ui.OptionWidget.
11649 *
11650 * @class
11651 * @extends OO.ui.Widget
11652 * @mixins OO.ui.GroupElement
11653 *
11654 * @constructor
11655 * @param {Object} [config] Configuration options
11656 * @cfg {OO.ui.OptionWidget[]} [items] Options to add
11657 */
11658 OO.ui.SelectWidget = function OoUiSelectWidget( config ) {
11659 // Configuration initialization
11660 config = config || {};
11661
11662 // Parent constructor
11663 OO.ui.SelectWidget.super.call( this, config );
11664
11665 // Mixin constructors
11666 OO.ui.GroupWidget.call( this, $.extend( {}, config, { $group: this.$element } ) );
11667
11668 // Properties
11669 this.pressed = false;
11670 this.selecting = null;
11671 this.onMouseUpHandler = this.onMouseUp.bind( this );
11672 this.onMouseMoveHandler = this.onMouseMove.bind( this );
11673
11674 // Events
11675 this.$element.on( {
11676 mousedown: this.onMouseDown.bind( this ),
11677 mouseover: this.onMouseOver.bind( this ),
11678 mouseleave: this.onMouseLeave.bind( this )
11679 } );
11680
11681 // Initialization
11682 this.$element.addClass( 'oo-ui-selectWidget oo-ui-selectWidget-depressed' );
11683 if ( $.isArray( config.items ) ) {
11684 this.addItems( config.items );
11685 }
11686 };
11687
11688 /* Setup */
11689
11690 OO.inheritClass( OO.ui.SelectWidget, OO.ui.Widget );
11691
11692 // Need to mixin base class as well
11693 OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupElement );
11694 OO.mixinClass( OO.ui.SelectWidget, OO.ui.GroupWidget );
11695
11696 /* Events */
11697
11698 /**
11699 * @event highlight
11700 * @param {OO.ui.OptionWidget|null} item Highlighted item
11701 */
11702
11703 /**
11704 * @event press
11705 * @param {OO.ui.OptionWidget|null} item Pressed item
11706 */
11707
11708 /**
11709 * @event select
11710 * @param {OO.ui.OptionWidget|null} item Selected item
11711 */
11712
11713 /**
11714 * @event choose
11715 * @param {OO.ui.OptionWidget|null} item Chosen item
11716 */
11717
11718 /**
11719 * @event add
11720 * @param {OO.ui.OptionWidget[]} items Added items
11721 * @param {number} index Index items were added at
11722 */
11723
11724 /**
11725 * @event remove
11726 * @param {OO.ui.OptionWidget[]} items Removed items
11727 */
11728
11729 /* Methods */
11730
11731 /**
11732 * Handle mouse down events.
11733 *
11734 * @private
11735 * @param {jQuery.Event} e Mouse down event
11736 */
11737 OO.ui.SelectWidget.prototype.onMouseDown = function ( e ) {
11738 var item;
11739
11740 if ( !this.isDisabled() && e.which === 1 ) {
11741 this.togglePressed( true );
11742 item = this.getTargetItem( e );
11743 if ( item && item.isSelectable() ) {
11744 this.pressItem( item );
11745 this.selecting = item;
11746 this.getElementDocument().addEventListener(
11747 'mouseup',
11748 this.onMouseUpHandler,
11749 true
11750 );
11751 this.getElementDocument().addEventListener(
11752 'mousemove',
11753 this.onMouseMoveHandler,
11754 true
11755 );
11756 }
11757 }
11758 return false;
11759 };
11760
11761 /**
11762 * Handle mouse up events.
11763 *
11764 * @private
11765 * @param {jQuery.Event} e Mouse up event
11766 */
11767 OO.ui.SelectWidget.prototype.onMouseUp = function ( e ) {
11768 var item;
11769
11770 this.togglePressed( false );
11771 if ( !this.selecting ) {
11772 item = this.getTargetItem( e );
11773 if ( item && item.isSelectable() ) {
11774 this.selecting = item;
11775 }
11776 }
11777 if ( !this.isDisabled() && e.which === 1 && this.selecting ) {
11778 this.pressItem( null );
11779 this.chooseItem( this.selecting );
11780 this.selecting = null;
11781 }
11782
11783 this.getElementDocument().removeEventListener(
11784 'mouseup',
11785 this.onMouseUpHandler,
11786 true
11787 );
11788 this.getElementDocument().removeEventListener(
11789 'mousemove',
11790 this.onMouseMoveHandler,
11791 true
11792 );
11793
11794 return false;
11795 };
11796
11797 /**
11798 * Handle mouse move events.
11799 *
11800 * @private
11801 * @param {jQuery.Event} e Mouse move event
11802 */
11803 OO.ui.SelectWidget.prototype.onMouseMove = function ( e ) {
11804 var item;
11805
11806 if ( !this.isDisabled() && this.pressed ) {
11807 item = this.getTargetItem( e );
11808 if ( item && item !== this.selecting && item.isSelectable() ) {
11809 this.pressItem( item );
11810 this.selecting = item;
11811 }
11812 }
11813 return false;
11814 };
11815
11816 /**
11817 * Handle mouse over events.
11818 *
11819 * @private
11820 * @param {jQuery.Event} e Mouse over event
11821 */
11822 OO.ui.SelectWidget.prototype.onMouseOver = function ( e ) {
11823 var item;
11824
11825 if ( !this.isDisabled() ) {
11826 item = this.getTargetItem( e );
11827 this.highlightItem( item && item.isHighlightable() ? item : null );
11828 }
11829 return false;
11830 };
11831
11832 /**
11833 * Handle mouse leave events.
11834 *
11835 * @private
11836 * @param {jQuery.Event} e Mouse over event
11837 */
11838 OO.ui.SelectWidget.prototype.onMouseLeave = function () {
11839 if ( !this.isDisabled() ) {
11840 this.highlightItem( null );
11841 }
11842 return false;
11843 };
11844
11845 /**
11846 * Get the closest item to a jQuery.Event.
11847 *
11848 * @private
11849 * @param {jQuery.Event} e
11850 * @return {OO.ui.OptionWidget|null} Outline item widget, `null` if none was found
11851 */
11852 OO.ui.SelectWidget.prototype.getTargetItem = function ( e ) {
11853 var $item = this.$( e.target ).closest( '.oo-ui-optionWidget' );
11854 if ( $item.length ) {
11855 return $item.data( 'oo-ui-optionWidget' );
11856 }
11857 return null;
11858 };
11859
11860 /**
11861 * Get selected item.
11862 *
11863 * @return {OO.ui.OptionWidget|null} Selected item, `null` if no item is selected
11864 */
11865 OO.ui.SelectWidget.prototype.getSelectedItem = function () {
11866 var i, len;
11867
11868 for ( i = 0, len = this.items.length; i < len; i++ ) {
11869 if ( this.items[i].isSelected() ) {
11870 return this.items[i];
11871 }
11872 }
11873 return null;
11874 };
11875
11876 /**
11877 * Get highlighted item.
11878 *
11879 * @return {OO.ui.OptionWidget|null} Highlighted item, `null` if no item is highlighted
11880 */
11881 OO.ui.SelectWidget.prototype.getHighlightedItem = function () {
11882 var i, len;
11883
11884 for ( i = 0, len = this.items.length; i < len; i++ ) {
11885 if ( this.items[i].isHighlighted() ) {
11886 return this.items[i];
11887 }
11888 }
11889 return null;
11890 };
11891
11892 /**
11893 * Toggle pressed state.
11894 *
11895 * @param {boolean} pressed An option is being pressed
11896 */
11897 OO.ui.SelectWidget.prototype.togglePressed = function ( pressed ) {
11898 if ( pressed === undefined ) {
11899 pressed = !this.pressed;
11900 }
11901 if ( pressed !== this.pressed ) {
11902 this.$element
11903 .toggleClass( 'oo-ui-selectWidget-pressed', pressed )
11904 .toggleClass( 'oo-ui-selectWidget-depressed', !pressed );
11905 this.pressed = pressed;
11906 }
11907 };
11908
11909 /**
11910 * Highlight an item.
11911 *
11912 * Highlighting is mutually exclusive.
11913 *
11914 * @param {OO.ui.OptionWidget} [item] Item to highlight, omit to deselect all
11915 * @fires highlight
11916 * @chainable
11917 */
11918 OO.ui.SelectWidget.prototype.highlightItem = function ( item ) {
11919 var i, len, highlighted,
11920 changed = false;
11921
11922 for ( i = 0, len = this.items.length; i < len; i++ ) {
11923 highlighted = this.items[i] === item;
11924 if ( this.items[i].isHighlighted() !== highlighted ) {
11925 this.items[i].setHighlighted( highlighted );
11926 changed = true;
11927 }
11928 }
11929 if ( changed ) {
11930 this.emit( 'highlight', item );
11931 }
11932
11933 return this;
11934 };
11935
11936 /**
11937 * Select an item.
11938 *
11939 * @param {OO.ui.OptionWidget} [item] Item to select, omit to deselect all
11940 * @fires select
11941 * @chainable
11942 */
11943 OO.ui.SelectWidget.prototype.selectItem = function ( item ) {
11944 var i, len, selected,
11945 changed = false;
11946
11947 for ( i = 0, len = this.items.length; i < len; i++ ) {
11948 selected = this.items[i] === item;
11949 if ( this.items[i].isSelected() !== selected ) {
11950 this.items[i].setSelected( selected );
11951 changed = true;
11952 }
11953 }
11954 if ( changed ) {
11955 this.emit( 'select', item );
11956 }
11957
11958 return this;
11959 };
11960
11961 /**
11962 * Press an item.
11963 *
11964 * @param {OO.ui.OptionWidget} [item] Item to press, omit to depress all
11965 * @fires press
11966 * @chainable
11967 */
11968 OO.ui.SelectWidget.prototype.pressItem = function ( item ) {
11969 var i, len, pressed,
11970 changed = false;
11971
11972 for ( i = 0, len = this.items.length; i < len; i++ ) {
11973 pressed = this.items[i] === item;
11974 if ( this.items[i].isPressed() !== pressed ) {
11975 this.items[i].setPressed( pressed );
11976 changed = true;
11977 }
11978 }
11979 if ( changed ) {
11980 this.emit( 'press', item );
11981 }
11982
11983 return this;
11984 };
11985
11986 /**
11987 * Choose an item.
11988 *
11989 * Identical to #selectItem, but may vary in subclasses that want to take additional action when
11990 * an item is selected using the keyboard or mouse.
11991 *
11992 * @param {OO.ui.OptionWidget} item Item to choose
11993 * @fires choose
11994 * @chainable
11995 */
11996 OO.ui.SelectWidget.prototype.chooseItem = function ( item ) {
11997 this.selectItem( item );
11998 this.emit( 'choose', item );
11999
12000 return this;
12001 };
12002
12003 /**
12004 * Get an item relative to another one.
12005 *
12006 * @param {OO.ui.OptionWidget|null} item Item to start at, null to get relative to list start
12007 * @param {number} direction Direction to move in, -1 to move backward, 1 to move forward
12008 * @return {OO.ui.OptionWidget|null} Item at position, `null` if there are no items in the menu
12009 */
12010 OO.ui.SelectWidget.prototype.getRelativeSelectableItem = function ( item, direction ) {
12011 var currentIndex, nextIndex, i,
12012 increase = direction > 0 ? 1 : -1,
12013 len = this.items.length;
12014
12015 if ( item instanceof OO.ui.OptionWidget ) {
12016 currentIndex = $.inArray( item, this.items );
12017 nextIndex = ( currentIndex + increase + len ) % len;
12018 } else {
12019 // If no item is selected and moving forward, start at the beginning.
12020 // If moving backward, start at the end.
12021 nextIndex = direction > 0 ? 0 : len - 1;
12022 }
12023
12024 for ( i = 0; i < len; i++ ) {
12025 item = this.items[nextIndex];
12026 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
12027 return item;
12028 }
12029 nextIndex = ( nextIndex + increase + len ) % len;
12030 }
12031 return null;
12032 };
12033
12034 /**
12035 * Get the next selectable item.
12036 *
12037 * @return {OO.ui.OptionWidget|null} Item, `null` if there aren't any selectable items
12038 */
12039 OO.ui.SelectWidget.prototype.getFirstSelectableItem = function () {
12040 var i, len, item;
12041
12042 for ( i = 0, len = this.items.length; i < len; i++ ) {
12043 item = this.items[i];
12044 if ( item instanceof OO.ui.OptionWidget && item.isSelectable() ) {
12045 return item;
12046 }
12047 }
12048
12049 return null;
12050 };
12051
12052 /**
12053 * Add items.
12054 *
12055 * @param {OO.ui.OptionWidget[]} items Items to add
12056 * @param {number} [index] Index to insert items after
12057 * @fires add
12058 * @chainable
12059 */
12060 OO.ui.SelectWidget.prototype.addItems = function ( items, index ) {
12061 // Mixin method
12062 OO.ui.GroupWidget.prototype.addItems.call( this, items, index );
12063
12064 // Always provide an index, even if it was omitted
12065 this.emit( 'add', items, index === undefined ? this.items.length - items.length - 1 : index );
12066
12067 return this;
12068 };
12069
12070 /**
12071 * Remove items.
12072 *
12073 * Items will be detached, not removed, so they can be used later.
12074 *
12075 * @param {OO.ui.OptionWidget[]} items Items to remove
12076 * @fires remove
12077 * @chainable
12078 */
12079 OO.ui.SelectWidget.prototype.removeItems = function ( items ) {
12080 var i, len, item;
12081
12082 // Deselect items being removed
12083 for ( i = 0, len = items.length; i < len; i++ ) {
12084 item = items[i];
12085 if ( item.isSelected() ) {
12086 this.selectItem( null );
12087 }
12088 }
12089
12090 // Mixin method
12091 OO.ui.GroupWidget.prototype.removeItems.call( this, items );
12092
12093 this.emit( 'remove', items );
12094
12095 return this;
12096 };
12097
12098 /**
12099 * Clear all items.
12100 *
12101 * Items will be detached, not removed, so they can be used later.
12102 *
12103 * @fires remove
12104 * @chainable
12105 */
12106 OO.ui.SelectWidget.prototype.clearItems = function () {
12107 var items = this.items.slice();
12108
12109 // Mixin method
12110 OO.ui.GroupWidget.prototype.clearItems.call( this );
12111
12112 // Clear selection
12113 this.selectItem( null );
12114
12115 this.emit( 'remove', items );
12116
12117 return this;
12118 };
12119
12120 /**
12121 * Select widget containing button options.
12122 *
12123 * Use together with OO.ui.ButtonOptionWidget.
12124 *
12125 * @class
12126 * @extends OO.ui.SelectWidget
12127 *
12128 * @constructor
12129 * @param {Object} [config] Configuration options
12130 */
12131 OO.ui.ButtonSelectWidget = function OoUiButtonSelectWidget( config ) {
12132 // Parent constructor
12133 OO.ui.ButtonSelectWidget.super.call( this, config );
12134
12135 // Initialization
12136 this.$element.addClass( 'oo-ui-buttonSelectWidget' );
12137 };
12138
12139 /* Setup */
12140
12141 OO.inheritClass( OO.ui.ButtonSelectWidget, OO.ui.SelectWidget );
12142
12143 /**
12144 * Select widget containing radio button options.
12145 *
12146 * Use together with OO.ui.RadioOptionWidget.
12147 *
12148 * @class
12149 * @extends OO.ui.SelectWidget
12150 *
12151 * @constructor
12152 * @param {Object} [config] Configuration options
12153 */
12154 OO.ui.RadioSelectWidget = function OoUiRadioSelectWidget( config ) {
12155 // Parent constructor
12156 OO.ui.RadioSelectWidget.super.call( this, config );
12157
12158 // Initialization
12159 this.$element.addClass( 'oo-ui-radioSelectWidget' );
12160 };
12161
12162 /* Setup */
12163
12164 OO.inheritClass( OO.ui.RadioSelectWidget, OO.ui.SelectWidget );
12165
12166 /**
12167 * Overlaid menu of options.
12168 *
12169 * Menus are clipped to the visible viewport. They do not provide a control for opening or closing
12170 * the menu.
12171 *
12172 * Use together with OO.ui.MenuOptionWidget.
12173 *
12174 * @class
12175 * @extends OO.ui.SelectWidget
12176 * @mixins OO.ui.ClippableElement
12177 *
12178 * @constructor
12179 * @param {Object} [config] Configuration options
12180 * @cfg {OO.ui.InputWidget} [input] Input to bind keyboard handlers to
12181 * @cfg {OO.ui.Widget} [widget] Widget to bind mouse handlers to
12182 * @cfg {boolean} [autoHide=true] Hide the menu when the mouse is pressed outside the menu
12183 */
12184 OO.ui.MenuSelectWidget = function OoUiMenuSelectWidget( config ) {
12185 // Configuration initialization
12186 config = config || {};
12187
12188 // Parent constructor
12189 OO.ui.MenuSelectWidget.super.call( this, config );
12190
12191 // Mixin constructors
12192 OO.ui.ClippableElement.call( this, $.extend( {}, config, { $clippable: this.$group } ) );
12193
12194 // Properties
12195 this.flashing = false;
12196 this.visible = false;
12197 this.newItems = null;
12198 this.autoHide = config.autoHide === undefined || !!config.autoHide;
12199 this.$input = config.input ? config.input.$input : null;
12200 this.$widget = config.widget ? config.widget.$element : null;
12201 this.$previousFocus = null;
12202 this.isolated = !config.input;
12203 this.onKeyDownHandler = this.onKeyDown.bind( this );
12204 this.onDocumentMouseDownHandler = this.onDocumentMouseDown.bind( this );
12205
12206 // Initialization
12207 this.$element
12208 .hide()
12209 .attr( 'role', 'menu' )
12210 .addClass( 'oo-ui-menuSelectWidget' );
12211 };
12212
12213 /* Setup */
12214
12215 OO.inheritClass( OO.ui.MenuSelectWidget, OO.ui.SelectWidget );
12216 OO.mixinClass( OO.ui.MenuSelectWidget, OO.ui.ClippableElement );
12217
12218 /* Methods */
12219
12220 /**
12221 * Handles document mouse down events.
12222 *
12223 * @param {jQuery.Event} e Key down event
12224 */
12225 OO.ui.MenuSelectWidget.prototype.onDocumentMouseDown = function ( e ) {
12226 if (
12227 !OO.ui.contains( this.$element[0], e.target, true ) &&
12228 ( !this.$widget || !OO.ui.contains( this.$widget[0], e.target, true ) )
12229 ) {
12230 this.toggle( false );
12231 }
12232 };
12233
12234 /**
12235 * Handles key down events.
12236 *
12237 * @param {jQuery.Event} e Key down event
12238 */
12239 OO.ui.MenuSelectWidget.prototype.onKeyDown = function ( e ) {
12240 var nextItem,
12241 handled = false,
12242 highlightItem = this.getHighlightedItem();
12243
12244 if ( !this.isDisabled() && this.isVisible() ) {
12245 if ( !highlightItem ) {
12246 highlightItem = this.getSelectedItem();
12247 }
12248 switch ( e.keyCode ) {
12249 case OO.ui.Keys.ENTER:
12250 this.chooseItem( highlightItem );
12251 handled = true;
12252 break;
12253 case OO.ui.Keys.UP:
12254 nextItem = this.getRelativeSelectableItem( highlightItem, -1 );
12255 handled = true;
12256 break;
12257 case OO.ui.Keys.DOWN:
12258 nextItem = this.getRelativeSelectableItem( highlightItem, 1 );
12259 handled = true;
12260 break;
12261 case OO.ui.Keys.ESCAPE:
12262 if ( highlightItem ) {
12263 highlightItem.setHighlighted( false );
12264 }
12265 this.toggle( false );
12266 handled = true;
12267 break;
12268 }
12269
12270 if ( nextItem ) {
12271 this.highlightItem( nextItem );
12272 nextItem.scrollElementIntoView();
12273 }
12274
12275 if ( handled ) {
12276 e.preventDefault();
12277 e.stopPropagation();
12278 return false;
12279 }
12280 }
12281 };
12282
12283 /**
12284 * Bind key down listener.
12285 */
12286 OO.ui.MenuSelectWidget.prototype.bindKeyDownListener = function () {
12287 if ( this.$input ) {
12288 this.$input.on( 'keydown', this.onKeyDownHandler );
12289 } else {
12290 // Capture menu navigation keys
12291 this.getElementWindow().addEventListener( 'keydown', this.onKeyDownHandler, true );
12292 }
12293 };
12294
12295 /**
12296 * Unbind key down listener.
12297 */
12298 OO.ui.MenuSelectWidget.prototype.unbindKeyDownListener = function () {
12299 if ( this.$input ) {
12300 this.$input.off( 'keydown' );
12301 } else {
12302 this.getElementWindow().removeEventListener( 'keydown', this.onKeyDownHandler, true );
12303 }
12304 };
12305
12306 /**
12307 * Choose an item.
12308 *
12309 * This will close the menu when done, unlike selectItem which only changes selection.
12310 *
12311 * @param {OO.ui.OptionWidget} item Item to choose
12312 * @chainable
12313 */
12314 OO.ui.MenuSelectWidget.prototype.chooseItem = function ( item ) {
12315 var widget = this;
12316
12317 // Parent method
12318 OO.ui.MenuSelectWidget.super.prototype.chooseItem.call( this, item );
12319
12320 if ( item && !this.flashing ) {
12321 this.flashing = true;
12322 item.flash().done( function () {
12323 widget.toggle( false );
12324 widget.flashing = false;
12325 } );
12326 } else {
12327 this.toggle( false );
12328 }
12329
12330 return this;
12331 };
12332
12333 /**
12334 * @inheritdoc
12335 */
12336 OO.ui.MenuSelectWidget.prototype.addItems = function ( items, index ) {
12337 var i, len, item;
12338
12339 // Parent method
12340 OO.ui.MenuSelectWidget.super.prototype.addItems.call( this, items, index );
12341
12342 // Auto-initialize
12343 if ( !this.newItems ) {
12344 this.newItems = [];
12345 }
12346
12347 for ( i = 0, len = items.length; i < len; i++ ) {
12348 item = items[i];
12349 if ( this.isVisible() ) {
12350 // Defer fitting label until item has been attached
12351 item.fitLabel();
12352 } else {
12353 this.newItems.push( item );
12354 }
12355 }
12356
12357 // Reevaluate clipping
12358 this.clip();
12359
12360 return this;
12361 };
12362
12363 /**
12364 * @inheritdoc
12365 */
12366 OO.ui.MenuSelectWidget.prototype.removeItems = function ( items ) {
12367 // Parent method
12368 OO.ui.MenuSelectWidget.super.prototype.removeItems.call( this, items );
12369
12370 // Reevaluate clipping
12371 this.clip();
12372
12373 return this;
12374 };
12375
12376 /**
12377 * @inheritdoc
12378 */
12379 OO.ui.MenuSelectWidget.prototype.clearItems = function () {
12380 // Parent method
12381 OO.ui.MenuSelectWidget.super.prototype.clearItems.call( this );
12382
12383 // Reevaluate clipping
12384 this.clip();
12385
12386 return this;
12387 };
12388
12389 /**
12390 * @inheritdoc
12391 */
12392 OO.ui.MenuSelectWidget.prototype.toggle = function ( visible ) {
12393 visible = ( visible === undefined ? !this.visible : !!visible ) && !!this.items.length;
12394
12395 var i, len,
12396 change = visible !== this.isVisible(),
12397 elementDoc = this.getElementDocument(),
12398 widgetDoc = this.$widget ? this.$widget[0].ownerDocument : null;
12399
12400 // Parent method
12401 OO.ui.MenuSelectWidget.super.prototype.toggle.call( this, visible );
12402
12403 if ( change ) {
12404 if ( visible ) {
12405 this.bindKeyDownListener();
12406
12407 // Change focus to enable keyboard navigation
12408 if ( this.isolated && this.$input && !this.$input.is( ':focus' ) ) {
12409 this.$previousFocus = this.$( ':focus' );
12410 this.$input[0].focus();
12411 }
12412 if ( this.newItems && this.newItems.length ) {
12413 for ( i = 0, len = this.newItems.length; i < len; i++ ) {
12414 this.newItems[i].fitLabel();
12415 }
12416 this.newItems = null;
12417 }
12418 this.toggleClipping( true );
12419
12420 // Auto-hide
12421 if ( this.autoHide ) {
12422 elementDoc.addEventListener(
12423 'mousedown', this.onDocumentMouseDownHandler, true
12424 );
12425 // Support $widget being in a different document
12426 if ( widgetDoc && widgetDoc !== elementDoc ) {
12427 widgetDoc.addEventListener(
12428 'mousedown', this.onDocumentMouseDownHandler, true
12429 );
12430 }
12431 }
12432 } else {
12433 this.unbindKeyDownListener();
12434 if ( this.isolated && this.$previousFocus ) {
12435 this.$previousFocus[0].focus();
12436 this.$previousFocus = null;
12437 }
12438 elementDoc.removeEventListener(
12439 'mousedown', this.onDocumentMouseDownHandler, true
12440 );
12441 // Support $widget being in a different document
12442 if ( widgetDoc && widgetDoc !== elementDoc ) {
12443 widgetDoc.removeEventListener(
12444 'mousedown', this.onDocumentMouseDownHandler, true
12445 );
12446 }
12447 this.toggleClipping( false );
12448 }
12449 }
12450
12451 return this;
12452 };
12453
12454 /**
12455 * Menu for a text input widget.
12456 *
12457 * This menu is specially designed to be positioned beneath the text input widget. Even if the input
12458 * is in a different frame, the menu's position is automatically calculated and maintained when the
12459 * menu is toggled or the window is resized.
12460 *
12461 * @class
12462 * @extends OO.ui.MenuSelectWidget
12463 *
12464 * @constructor
12465 * @param {OO.ui.TextInputWidget} input Text input widget to provide menu for
12466 * @param {Object} [config] Configuration options
12467 * @cfg {jQuery} [$container=input.$element] Element to render menu under
12468 */
12469 OO.ui.TextInputMenuSelectWidget = function OoUiTextInputMenuSelectWidget( input, config ) {
12470 // Configuration initialization
12471 config = config || {};
12472
12473 // Parent constructor
12474 OO.ui.TextInputMenuSelectWidget.super.call( this, config );
12475
12476 // Properties
12477 this.input = input;
12478 this.$container = config.$container || this.input.$element;
12479 this.onWindowResizeHandler = this.onWindowResize.bind( this );
12480
12481 // Initialization
12482 this.$element.addClass( 'oo-ui-textInputMenuSelectWidget' );
12483 };
12484
12485 /* Setup */
12486
12487 OO.inheritClass( OO.ui.TextInputMenuSelectWidget, OO.ui.MenuSelectWidget );
12488
12489 /* Methods */
12490
12491 /**
12492 * Handle window resize event.
12493 *
12494 * @param {jQuery.Event} e Window resize event
12495 */
12496 OO.ui.TextInputMenuSelectWidget.prototype.onWindowResize = function () {
12497 this.position();
12498 };
12499
12500 /**
12501 * @inheritdoc
12502 */
12503 OO.ui.TextInputMenuSelectWidget.prototype.toggle = function ( visible ) {
12504 visible = visible === undefined ? !this.isVisible() : !!visible;
12505
12506 var change = visible !== this.isVisible();
12507
12508 if ( change && visible ) {
12509 // Make sure the width is set before the parent method runs.
12510 // After this we have to call this.position(); again to actually
12511 // position ourselves correctly.
12512 this.position();
12513 }
12514
12515 // Parent method
12516 OO.ui.TextInputMenuSelectWidget.super.prototype.toggle.call( this, visible );
12517
12518 if ( change ) {
12519 if ( this.isVisible() ) {
12520 this.position();
12521 this.$( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
12522 } else {
12523 this.$( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
12524 }
12525 }
12526
12527 return this;
12528 };
12529
12530 /**
12531 * Position the menu.
12532 *
12533 * @chainable
12534 */
12535 OO.ui.TextInputMenuSelectWidget.prototype.position = function () {
12536 var $container = this.$container,
12537 pos = OO.ui.Element.static.getRelativePosition( $container, this.$element.offsetParent() );
12538
12539 // Position under input
12540 pos.top += $container.height();
12541 this.$element.css( pos );
12542
12543 // Set width
12544 this.setIdealSize( $container.width() );
12545 // We updated the position, so re-evaluate the clipping state
12546 this.clip();
12547
12548 return this;
12549 };
12550
12551 /**
12552 * Structured list of items.
12553 *
12554 * Use with OO.ui.OutlineOptionWidget.
12555 *
12556 * @class
12557 * @extends OO.ui.SelectWidget
12558 *
12559 * @constructor
12560 * @param {Object} [config] Configuration options
12561 */
12562 OO.ui.OutlineSelectWidget = function OoUiOutlineSelectWidget( config ) {
12563 // Configuration initialization
12564 config = config || {};
12565
12566 // Parent constructor
12567 OO.ui.OutlineSelectWidget.super.call( this, config );
12568
12569 // Initialization
12570 this.$element.addClass( 'oo-ui-outlineSelectWidget' );
12571 };
12572
12573 /* Setup */
12574
12575 OO.inheritClass( OO.ui.OutlineSelectWidget, OO.ui.SelectWidget );
12576
12577 /**
12578 * Switch that slides on and off.
12579 *
12580 * @class
12581 * @extends OO.ui.Widget
12582 * @mixins OO.ui.ToggleWidget
12583 *
12584 * @constructor
12585 * @param {Object} [config] Configuration options
12586 * @cfg {boolean} [value=false] Initial value
12587 */
12588 OO.ui.ToggleSwitchWidget = function OoUiToggleSwitchWidget( config ) {
12589 // Parent constructor
12590 OO.ui.ToggleSwitchWidget.super.call( this, config );
12591
12592 // Mixin constructors
12593 OO.ui.ToggleWidget.call( this, config );
12594
12595 // Properties
12596 this.dragging = false;
12597 this.dragStart = null;
12598 this.sliding = false;
12599 this.$glow = this.$( '<span>' );
12600 this.$grip = this.$( '<span>' );
12601
12602 // Events
12603 this.$element.on( 'click', this.onClick.bind( this ) );
12604
12605 // Initialization
12606 this.$glow.addClass( 'oo-ui-toggleSwitchWidget-glow' );
12607 this.$grip.addClass( 'oo-ui-toggleSwitchWidget-grip' );
12608 this.$element
12609 .addClass( 'oo-ui-toggleSwitchWidget' )
12610 .append( this.$glow, this.$grip );
12611 };
12612
12613 /* Setup */
12614
12615 OO.inheritClass( OO.ui.ToggleSwitchWidget, OO.ui.Widget );
12616 OO.mixinClass( OO.ui.ToggleSwitchWidget, OO.ui.ToggleWidget );
12617
12618 /* Methods */
12619
12620 /**
12621 * Handle mouse down events.
12622 *
12623 * @param {jQuery.Event} e Mouse down event
12624 */
12625 OO.ui.ToggleSwitchWidget.prototype.onClick = function ( e ) {
12626 if ( !this.isDisabled() && e.which === 1 ) {
12627 this.setValue( !this.value );
12628 }
12629 };
12630
12631 }( OO ) );