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