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