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