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