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