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