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