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