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