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