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