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